Alert

이 글은 Claude Code의 도움을 받아 작성되었습니다

TL;DR

  • Bash에서 명령어가 해석되는 과정(단어 분리, 명령어 5가지 타입)을 이해하면 디버깅이 쉬워짐
  • 특수 문자($, (), {}, <>, | 등)의 역할을 알아야 스크립트를 읽을 수 있음
  • 변수, 따옴표 규칙, 파라미터 확장, 문자열 조작은 모든 스크립트의 기본 도구
  • 배열(인덱스/연관)과 glob/정규식 패턴 매칭까지 다루면 1편 완료

1. 시작하기

Shebang

스크립트 첫 줄에 인터프리터를 지정한다.

#!/bin/bash

env를 사용하면 bash가 어디 설치되어 있든 PATH에서 찾아 실행한다.

#!/usr/bin/env bash
실행 방법
# 실행 권한 부여 후 직접 실행
chmod +x script.sh
./script.sh
 
# bash로 명시적 실행 (권한 불필요)
bash script.sh
 
# 현재 쉘에서 실행 (변수/함수가 현재 쉘에 남음)
source script.sh

./script.sh vs source script.sh

  • ./script.sh는 서브쉘에서 실행된다. 스크립트 내 변수가 현재 쉘에 영향을 주지 않는다.
  • source script.sh는 현재 쉘에서 직접 실행된다. 환경변수 설정, alias 등록 등에 사용한다.

2. 명령어와 인자

Bash는 입력을 공백 기준으로 단어(word)로 분리한다. 첫 번째 단어가 명령어 이름이 되고, 나머지가 인자가 된다.

cp -r src/ dest/
# cp    → 명령어
# -r    → 첫 번째 인자
# src/  → 두 번째 인자
# dest/ → 세 번째 인자

이 “단어 분리(word splitting)” 메커니즘이 Bash에서 따옴표가 중요한 이유다. 공백이 포함된 파일명이 의도치 않게 쪼개지기 때문이다.

file="my report.txt"
cat $file    # cat my report.txt → 인자 2개로 쪼개짐 (오류)
cat "$file"  # cat "my report.txt" → 인자 1개로 유지 (정상)
명령어의 5가지 타입

Bash는 명령어를 찾을 때 아래 순서로 탐색한다.

순서타입설명예시
1Alias텍스트 치환 단축어. 인터랙티브 쉘에서만 동작alias ll='ls -la'
2Function사용자 정의 명령 묶음greet() { echo "hi"; }
3BuiltinBash 자체 내장 명령cd, echo, read, export
4KeywordBash 문법의 일부로 특수하게 파싱됨if, for, while, [[
5ExecutablePATH에서 찾는 외부 프로그램/usr/bin/grep, /bin/ls
# 명령어 타입 확인
type cd       # cd is a shell builtin
type ls       # ls is /bin/ls
type [[       # [[ is a shell keyword
type -a echo  # builtin과 executable 모두 표시

which vs type

which는 PATH에서 실행 파일만 찾는다. type은 alias, function, builtin, keyword까지 모두 보여주므로 더 유용하다.


3. 특수 문자

Bash에서 특별한 의미를 가지는 문자들을 알아야 스크립트를 읽고 쓸 수 있다.

공백과 줄바꿈

공백, 탭, 줄바꿈은 단어 구분자다. 여러 공백은 하나와 동일하게 취급된다.

확장과 치환 관련
문자용도예시
$변수/파라미터 확장$HOME, ${var}
$()명령어 치환$(date +%Y)
$(())산술 확장$((a + b))
` `명령어 치환 (레거시)`date`
{}중괄호 확장 / 변수 구분{1..5}, ${var}_suffix
리다이렉션과 파이프
문자용도예시
>stdout을 파일로 (덮어쓰기)echo hi > out.txt
>>stdout을 파일로 (이어쓰기)echo hi >> out.txt
<파일을 stdin으로sort < data.txt
|파이프 (stdout → 다음 stdin)ls | grep txt
2>stderr를 파일로cmd 2> err.log
&>stdout+stderr 모두 파일로cmd &> all.log
제어와 실행
문자용도예시
;명령어 순차 실행cd dir; ls
&&앞 명령 성공 시 실행make && make install
||앞 명령 실패 시 실행cd dir || exit 1
&백그라운드 실행sleep 10 &
()서브쉘에서 실행(cd /tmp; ls)
{}현재 쉘에서 그룹 실행{ echo a; echo b; }
글로빙과 패턴
문자용도예시
*0개 이상의 임의 문자*.txt
?정확히 1개의 임의 문자file?.log
[]문자 클래스[abc], [0-9]
~홈 디렉토리~/Documents
#주석# 이건 주석
\이스케이프 (특수 문자 무효화)echo \$HOME

() vs {}

  • (commands) — 서브쉘에서 실행. 내부 변수 변경이 밖에 영향을 주지 않는다.
  • { commands; } — 현재 쉘에서 실행. 변수 변경이 유지된다. 닫는 } 앞에 ;이 필요하다.
x=1; (x=2; echo "inside: $x"); echo "outside: $x"
# inside: 2, outside: 1
 
x=1; { x=2; echo "inside: $x"; }; echo "outside: $x"
# inside: 2, outside: 2

4. 변수

기본 변수
# 선언 (= 앞뒤에 공백 없어야 함)
name="world"
count=42
 
# 사용
echo "Hello, $name"
echo "Count is ${count}"

$var vs ${var}

대부분의 경우 $var로 충분하지만, 변수명 바로 뒤에 문자가 붙을 때는 ${var}로 구분해줘야 한다.

file="report"
echo "${file}_2026.txt"  # report_2026.txt
echo "$file_2026.txt"    # 빈 문자열 (file_2026이라는 변수를 찾음)
따옴표 규칙
name="Bash"
 
echo "Hello $name"   # Hello Bash (변수 치환됨)
echo 'Hello $name'   # Hello $name (그대로 출력)
echo "It's $name"    # It's Bash
따옴표변수 치환용도
"쌍따옴표"O변수를 포함한 문자열
'홑따옴표'X리터럴 문자열
없음O단순 값 (공백 있으면 단어 분리 발생)

따옴표 사용 원칙

“확신이 없으면 쌍따옴표를 쓴다.”
변수를 쌍따옴표 없이 사용하면 단어 분리(word splitting)와 글로빙(pathname expansion)이 발생할 수 있다.

명령어 치환
# $() 방식 (권장)
today=$(date +%Y-%m-%d)
 
# 백틱 방식 (레거시)
today=`date +%Y-%m-%d`

$()는 중첩이 가능하다.

echo "Files: $(ls $(pwd))"
산술 연산
a=10
b=3
 
# $(( )) 사용
echo $((a + b))     # 13
echo $((a / b))     # 3 (정수 나눗셈)
echo $((a % b))     # 1
echo $((a ** 2))    # 100
 
# 변수에 할당
result=$((a * b))
 
# 증감
((a++))
((b += 5))

Bash는 정수 연산만 지원한다

소수점 연산이 필요하면 bcawk를 사용한다.

echo "10 / 3" | bc -l    # 3.33333333333333333333
awk "BEGIN {printf \"%.2f\", 10/3}"  # 3.33
환경 변수 vs 지역 변수
# 환경 변수 — 자식 프로세스에 상속됨
export API_KEY="abc123"
 
# 지역 변수 — 현재 쉘에서만 유효
local_var="only here"
 
# 특정 명령에만 환경 변수 전달
DEBUG=1 ./app.sh

변수 이름 컨벤션

환경 변수는 대문자(PATH, HOME), 스크립트 내부 변수는 소문자(count, file_name)를 사용하는 것이 관례다. 대문자를 쓰면 시스템 환경 변수와 충돌할 수 있다.


5. 파라미터 확장

변수 값을 꺼내면서 동시에 가공하는 Bash의 강력한 기능이다.

문자열 조작
str="Hello, World!"
 
# 길이
echo ${#str}               # 13
 
# 부분 문자열 (offset:length)
echo ${str:7}              # World!
echo ${str:7:5}            # World
 
# 치환
echo ${str/World/Bash}     # Hello, Bash! (첫 번째만)
echo ${str//l/L}           # HeLLo, WorLd! (전체 치환)
 
# 삭제 패턴
file="archive.tar.gz"
echo ${file#*.}            # tar.gz  (앞에서 최소 매칭 삭제)
echo ${file##*.}           # gz      (앞에서 최대 매칭 삭제)
echo ${file%.*}            # archive.tar (뒤에서 최소 매칭 삭제)
echo ${file%%.*}           # archive     (뒤에서 최대 매칭 삭제)
 
# 대소문자 변환 (Bash 4.0+)
echo ${str^^}              # HELLO, WORLD!
echo ${str,,}              # hello, world!

#% 기억법

키보드에서 #$ 왼쪽(앞), %$ 오른쪽(뒤)에 있다.

기본값 설정
# 변수가 비어있으면 기본값 사용 (변수는 변경 안 됨)
echo ${name:-"default"}
 
# 변수가 비어있으면 기본값 대입까지 수행
echo ${name:="default"}
 
# 변수가 비어있으면 에러 메시지 출력 후 종료
echo ${name:?"name is required"}
 
# 변수가 설정되어 있으면 대체값 사용
echo ${name:+"exists"}
문법비어있을 때설정되어 있을 때
${var:-val}val 반환$var 반환
${var:=val}val 대입 후 반환$var 반환
${var:?msg}에러 출력, 종료$var 반환
${var:+val}빈 문자열val 반환

6. 배열

일반 배열 (인덱스 배열)
# 선언
fruits=("apple" "banana" "cherry")
 
# 접근
echo ${fruits[0]}      # apple
echo ${fruits[-1]}     # cherry (마지막 원소)
 
# 전체 원소
echo ${fruits[@]}      # apple banana cherry
 
# 길이
echo ${#fruits[@]}     # 3
 
# 추가
fruits+=("date")
 
# 삭제
unset fruits[1]
 
# 슬라이스
echo ${fruits[@]:1:2}  # 인덱스 1부터 2개
반복
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

배열 반복 시 "${arr[@]}" 형태로 써야 한다

따옴표 없이 ${arr[@]}를 쓰면 공백이 포함된 원소가 쪼개진다.

연관 배열 (딕셔너리)

Bash 4.0 이상에서 사용 가능하다.

declare -A user
user[name]="kim"
user[age]=30
 
echo ${user[name]}     # kim
echo ${!user[@]}       # 모든 키: name age
echo ${user[@]}        # 모든 값: kim 30

7. 패턴 매칭

Glob 패턴

Bash는 파일명을 매칭할 때 glob 패턴을 사용한다. 정규식과 다르니 혼동하지 않아야 한다.

ls *.txt          # .txt로 끝나는 모든 파일
ls file?.log      # file + 한 글자 + .log
ls [abc]*.sh      # a, b, c로 시작하는 .sh 파일
ls [!0-9]*        # 숫자로 시작하지 않는 파일
Glob의미정규식 대응
*0개 이상의 임의 문자.*
?정확히 1개의 임의 문자.
[abc]a, b, c 중 하나[abc]
[!abc]a, b, c가 아닌 문자[^abc]
[0-9]숫자 범위[0-9]
확장 Glob (Extended Glob)

shopt -s extglob을 활성화하면 더 강력한 패턴을 사용할 수 있다.

shopt -s extglob
 
ls !(*.log)          # .log가 아닌 모든 파일
ls @(*.txt|*.md)     # .txt 또는 .md 파일
ls +([0-9])          # 숫자로만 이루어진 이름
패턴의미
?(pattern)0 또는 1회 매칭
*(pattern)0회 이상 매칭
+(pattern)1회 이상 매칭
@(pattern)정확히 1회 매칭
!(pattern)매칭되지 않는 것
Recursive Glob

Bash 4.0 이상에서 **는 하위 디렉토리까지 재귀 탐색한다.

shopt -s globstar
 
ls **/*.py         # 모든 하위 폴더의 .py 파일
[[ ]] 안에서의 패턴 매칭
file="report_2026.csv"
 
# glob 패턴 (== 오른쪽을 따옴표 없이)
[[ "$file" == *.csv ]] && echo "CSV 파일"
 
# 정규식 (=~ 연산자)
[[ "$file" =~ ^report_[0-9]{4}\.csv$ ]] && echo "매칭됨"
 
# 정규식은 변수에 담아서 쓰는 것이 안전
pattern='^[0-9]+$'
[[ "12345" =~ $pattern ]] && echo "숫자만"

Glob vs 정규식

  • Glob은 파일명 매칭에 사용. *이 “아무 문자열”을 의미한다.
  • 정규식은 텍스트 패턴 매칭에 사용. *이 “앞 문자 0회 이상 반복”을 의미한다.
  • [[ ]] 안에서 ==는 glob, =~는 정규식을 사용한다.

8. 중괄호 확장

Glob과 별개로, 중괄호 확장(brace expansion)은 Bash가 문자열을 생성하는 기능이다. 파일 존재 여부와 무관하게 동작한다.

echo {A,B,C}        # A B C
echo file{1,2,3}    # file1 file2 file3
echo {1..5}         # 1 2 3 4 5
echo {a..z}         # a b c ... z
echo {01..10}       # 01 02 03 ... 10 (제로패딩)
echo {0..20..5}     # 0 5 10 15 20 (증가값 지정)
 
# 실용적 활용
mkdir -p project/{src,test,docs}
cp config.yml{,.bak}    # config.yml을 config.yml.bak으로 복사

중괄호 확장은 변수보다 먼저 처리된다

n=5
echo {1..$n}    # {1..5} 그대로 출력 (확장 안 됨)

중괄호 확장은 변수 치환보다 먼저 일어나기 때문에 $n이 아직 5로 바뀌지 않은 상태에서 처리된다. 변수로 범위를 만들려면 C-style for 루프 for ((i=1; i<=n; i++))를 사용한다.