[[ ]]는 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
outer() { local x=10 inner() { local x=20 echo "inner: $x" # 20 } inner echo "outer: $x" # 10}
함수 내 변수는 항상 local로 선언하자
local 없이 선언하면 전역 변수가 되어 의도치 않은 부작용이 생길 수 있다.
local var=$(cmd) 주의점
local이 명령어 치환의 종료 코드를 가린다. set -e를 쓰고 있다면 오류가 무시될 수 있다.
# 나쁜 예: $(cmd) 실패해도 local이 성공(0)을 반환local result=$(failing_command)# 좋은 예: 선언과 할당을 분리local resultresult=$(failing_command)
4. 리다이렉션과 파일 디스크립터
파일 디스크립터 기초
모든 프로세스는 기본 3개의 파일 디스크립터(FD)를 가진다.
FD
이름
설명
0
stdin
표준 입력
1
stdout
표준 출력
2
stderr
표준 에러
기본 리다이렉션
# 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
실무 로그 패턴
프로젝트 쉘 스크립트에서 자주 보는 패턴이다.
# 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가 문자열을 쪼갤 때 기준으로 삼는 구분자 목록 ↩