[[ ]]는 Bash 내장 키워드로, 정규식 매칭(=~), 패턴 글로빙, &&/|| 연산을 지원한다.
특별한 이유가 없으면 [[ ]]를 쓴다.
case 문
여러 패턴에 대한 분기가 필요할 때 if/elif 체인보다 깔끔하다.
case "$input" in start) echo "Starting..." ;; stop|quit) echo "Stopping..." ;; restart) echo "Restarting..." ;; *) echo "Unknown: $input" ;;esac
2. 반복문
for 문
# 리스트 순회for item in apple banana cherry; do echo "$item"done# 배열 순회for item in "${arr[@]}"; do echo "$item"done# 범위 (Bash 4.0+)for i in {1..5}; do echo "$i"done# 범위 + 증가값for i in {0..20..5}; do echo "$i" # 0, 5, 10, 15, 20done# C-stylefor ((i = 0; i < 10; i++)); do echo "$i"done# 파일 순회for file in *.txt; do echo "Processing $file"done
while / until
# while — 조건이 참인 동안count=0while [[ $count -lt 5 ]]; do echo "$count" ((count++))done# until — 조건이 참이 될 때까지count=0until [[ $count -ge 5 ]]; do echo "$count" ((count++))done# 파일을 한 줄씩 읽기while IFS= read -r line; do echo "$line"done < input.txt
여기서 핵심은 FD 번호는 그 구멍의 번호일 뿐, 구멍이 어디에 연결될지는 고정이 아니다라는 점이다. 기본값은 셋 다 터미널이지만, 셸이 그 연결을 다른 곳으로 바꿔 끼울 수 있다. 리다이렉션과 파이프는 전부 “어느 구멍을 무엇에 꽂느냐” 하나로 설명된다.
문법
하는 일 (구멍 → 꽂는 대상)
cmd > f
stdout 출구 → 파일 f (덮어쓰기), >>는 이어쓰기
cmd < f
stdin 입구 → 파일 f
cmd 2> f
stderr 출구 → 파일 f (번호로 출구 지정)
cmd1 | cmd2
cmd1의 stdout 출구 → cmd2의 stdin 입구
cmd 2>&1
stderr 출구 → stdout이 현재 가리키는 곳 (출구를 출구에 합치기)
즉 |는 꽂는 상대가 다른 프로세스, <>는 파일일 뿐 같은 메커니즘이다. 셸이 내부적으로 하는 일은 dup2() 시스템 콜로 자식 프로세스의 FD 번호 칸이 가리키는 대상을 바꿔치기하는 것이고, 그게 여기서 말하는 “꽂기”다.
stderr를 파이프로 보내려면
기본 |는 stdout만 넘긴다(stderr는 터미널에 남는다). 에러까지 파이프로 보내려면 먼저 stderr를 stdout에 합친다.
cmd 2>&1 | grep something # stderr를 stdout에 합친 뒤 파이프cmd |& grep something # Bash 단축 문법 (위와 동일)
기본 리다이렉션
# stdout을 파일로echo "hello" > output.txt # 덮어쓰기echo "world" >> output.txt # 이어쓰기# stderr를 파일로command 2> error.log# stdout + stderr 모두 파일로command > all.log 2>&1command &> all.log # 위와 동일 (Bash 단축 표현)# stderr만 버리기command 2>/dev/null# 전부 버리기command &>/dev/null# stdin으로 파일 읽기command < input.txt
>>><> — 열 때의 플래그 차이
리다이렉션은 셸이 대신 파일을 열어 주는 일이다. <>>><>는 셸이 그 파일을 open()으로 열 때 거는 플래그가 다를 뿐이고, 그 조합이 동작을 가른다.
연산자
open() 플래그
효과
>
O_WRONLY+O_CREAT+O_TRUNC
쓰기 전용. 있으면 0바이트로 비우고 시작
>>
O_WRONLY+O_CREAT+O_APPEND
쓰기 전용. 항상 끝에 이어쓰기, 자르지 않음
<
O_RDONLY
읽기 전용
<>
O_RDWR+O_CREAT
읽기·쓰기 모두, 자르지 않음. 없으면 생성
>가 열 때마다 파일을 비우는 건 O_TRUNC 때문이다. 내용을 지우지 않고 열려면 >>로 끝에 덧붙인다. <>는 한 FD로 읽기·쓰기를 동시에 여는 드문 형태라 일상 스크립트에선 거의 안 보이고, 같은 파일을 읽으며 제자리 수정하거나 exec 3<>/dev/tcp/host/port처럼 양방향 소켓을 열 때 정도에 쓴다.
각 플래그의 의미는 이렇다.
플래그
의미
O_RDONLY / O_WRONLY / O_RDWR
접근 모드 — 읽기 / 쓰기 / 읽기+쓰기. 셋 중 하나는 필수
O_CREAT
파일이 없으면 만든다
O_EXCL
O_CREAT와 함께 쓰면, 이미 있을 때 실패(원자적 생성). set -o noclobber의 >가 이걸 쓴다
O_TRUNC
열 때 0바이트로 자른다
O_APPEND
매 쓰기를 항상 파일 끝에서 한다
open()에는 이 밖에도 O_CLOEXEC(exec 시 자동 닫기), O_NONBLOCK(논블로킹), O_SYNC(쓰기를 디스크까지 동기) 같은 플래그가 더 있지만, 셸 리다이렉션으로 직접 닿는 건 위 표 정도다.
한 번만 듣는 플래그, 계속 듣는 플래그
open() 플래그는 작용 시점이 둘로 갈린다.
생성 플래그(O_CREAT·O_EXCL·O_TRUNC 등): 열 때 한 번만 작용하고 끝. 나중에 바꿀 수 없다
상태 플래그(O_APPEND·O_NONBLOCK 등): 이후 입출력 동작을 바꾸고, 열려 있는 동안 fcntl로 나중에 켜고 끌 수 있다
그래서 O_TRUNC는 연 뒤 되돌릴 수 없지만, O_NONBLOCK은 이미 연 FD에 fcntl로 토글할 수 있다.
실무 로그 패턴
프로젝트 쉘 스크립트에서 자주 보는 패턴이다.
# stdout과 stderr를 모두 로그 파일에 appendcommand >> "$LOG_FILE" 2>&1
이 한 줄을 분해하면:
>> : stdout(FD 1)을 $LOG_FILE에 append 모드로 연결
2>&1 : stderr(FD 2)를 FD 1이 현재 가리키는 곳(= $LOG_FILE)으로 연결
순서가 중요하다.2>&1 >> "$LOG_FILE"로 쓰면 stderr가 아직 터미널을 가리키는 FD 1에 연결된 후 stdout만 파일로 가므로, stderr는 로그에 안 쌓인다.
tee — 터미널과 파일에 동시 출력
# stdout을 화면에도 보여주고 파일에도 저장command | tee output.log# append 모드 (-a)command | tee -a "$LOG_FILE"# stderr도 함께 tee로 보내기command 2>&1 | tee -a "$LOG_FILE"
tee의 실무 용도
스크립트 실행 로그를 파일에 쌓으면서 터미널에서도 실시간으로 확인하고 싶을 때 사용한다. -a(append) 옵션을 빠뜨리면 기존 로그가 날아가니 주의.
파이프와 stdout 연결
|를 사용하면 왼쪽 명령어의 stdout은 터미널이 아니라 오른쪽 명령어의 stdin으로 연결된다.
그래서 command 2>&1 | tee -a "$LOG_FILE"에서 2>&1은 stderr를 “현재 stdout”, 즉 tee로 이어지는 파이프로 보내는 의미가 된다.
5. Here Document / Here String
Here Document
여러 줄 텍스트를 명령의 stdin으로 전달한다.
# 기본 — 변수 치환됨cat <<EOFHello, $USERToday is $(date)EOF# 변수 치환 없이 — 따옴표로 감싸기cat <<'EOF'This $variable is not expanded$(this command too)EOF
<< EOF vs << 'EOF'
<< EOF : 내부에서 $변수와 $(명령)이 치환된다.
<< 'EOF' : 모든 것이 리터럴로 처리된다. 변수 치환 없음.
종료 마커 이름은 EOF 고정이 아니다. 시작과 끝이 같기만 하면 된다. QUERY, SQL, HELP 등 용도에 맞게 쓸 수 있다.
실무 SQL 패턴
쉘 스크립트에서 SQL을 깔끔하게 작성하는 데 Here Document가 유용하다.
SQL=$(cat <<EOFSELECT u.name, o.totalFROM users uJOIN orders o ON u.id = o.user_idWHERE o.created_at >= '${START_DATE}'ORDER BY o.total DESCLIMIT 100EOF)psql -h "$DB_HOST" -d "$DB_NAME" -c "$SQL"
Here Document를 $(cat << EOF ... EOF) 형태로 감싸면 여러 줄 SQL을 변수에 담을 수 있다. 이 패턴의 장점:
긴 SQL을 보기 좋게 들여쓰기 가능
따옴표가 많은 문자열 처리가 편함
변수 치환이 자연스럽게 동작
Here String
한 줄 입력을 stdin으로 전달한다.
grep "pattern" <<< "search in this string"# 변수를 stdin으로 전달할 때 유용while IFS=: read -r user _ uid _; do echo "$user ($uid)"done <<< "$(getent passwd root)"
# 이 코드는 동작하지 않는다count=0cat file.txt | while read -r line; do ((count++))doneecho "$count" # 항상 0 (서브쉘에서 증가한 값은 소멸)# 해결 방법 1: 리다이렉션 사용 (파이프 제거)count=0while read -r line; do ((count++))done < file.txtecho "$count" # 정상 동작# 해결 방법 2: 프로세스 치환count=0while read -r line; do ((count++))done < <(some_command)echo "$count" # 정상 동작
프로세스 치환
명령의 출력을 파일처럼 사용한다. (파일처럼 읽을 수 있는 경로로 제공하여 불필요한 파일 생기지 않음)
# 두 명령의 출력을 비교diff <(sort file1.txt) <(sort file2.txt)# 여러 소스를 하나로 합치기cat <(head -5 file1.txt) <(head -5 file2.txt)
기본적으로 파이프라인의 종료 코드는 마지막 명령의 것만 반영된다. set -o pipefail을 설정하면 파이프라인 중 하나라도 실패하면 전체가 실패로 처리된다.
7. 특수 변수
$0 # 스크립트 이름$1 ~ $9 # 위치 인자 (1번째 ~ 9번째)${10} # 10번째 이상은 중괄호 필요$# # 인자 개수$@ # 모든 인자 (개별 단어로)$* # 모든 인자 (하나의 문자열로)$? # 직전 명령의 종료 코드$$ # 현재 쉘의 PID$! # 마지막 백그라운드 프로세스 PID$_ # 직전 명령의 마지막 인자
$@ vs $*
따옴표로 감쌀 때 차이가 나타난다.
"$@" → 각 인자가 독립적인 단어로 유지된다: "arg1" "arg2" "arg3"
"$*" → 모든 인자가 하나로 합쳐진다: "arg1 arg2 arg3"
거의 모든 경우에 "$@"를 쓴다.
8. 인자 파싱
직접 파싱 (shift 사용)
while [[ $# -gt 0 ]]; do case "$1" in -n|--name) name="$2" shift 2 ;; -v|--verbose) verbose=true shift ;; -h|--help) echo "Usage: $0 [-n name] [-v]" exit 0 ;; *) echo "Unknown option: $1" exit 1 ;; esacdone
getopts (짧은 옵션만)
while getopts "n:vh" opt; do case "$opt" in n) name="$OPTARG" ;; v) verbose=true ;; h) echo "Usage: $0 [-n name] [-v]"; exit 0 ;; ?) exit 1 ;; esacdoneshift $((OPTIND - 1)) # 나머지 인자 처리
긴 옵션( --name)이 필요하면
getopts는 짧은 옵션만 지원한다. 긴 옵션이 필요하면 shift 패턴을 쓰거나 getopt(외부 명령)을 사용한다.
Footnotes
Internal Field Separator . Bash가 문자열을 쪼갤 때 기준으로 삼는 구분자 목록 ↩