set -e, set -u, set -o pipefail
방어적인 스크립트를 작성하는 가장 빠른 방법이 있습니다. 스크립트 맨 첫 줄에 이것을 넣는 것입니다.
set -euo pipefail
세 글자처럼 보이지만 실제로는 세 가지 옵션의 조합입니다. 각각이 무엇을 하고, 왜 필요한지, 그리고 어떤 함정이 있는지 하나씩 살펴봅니다.
set -e — 에러 발생 시 즉시 종료
기본 상태의 Bash는 관대합니다. 명령어가 실패해도 다음 줄을 실행합니다. set -e를 켜면 명령어가 0이 아닌 종료 코드를 반환하는 순간 스크립트 전체가 종료됩니다.
# 파일: without_set_e.sh#!/usr/bin/env bashcp /없는파일 /tmp/ # 실패echo "이 줄도 실행됩니다." # set -e 없이는 실행됨echo "끝"
# 파일: with_set_e.sh#!/usr/bin/env bashset -ecp /없는파일 /tmp/ # 실패echo "이 줄은 실행되지 않습니다." # set -e로 여기 오기 전에 종료echo "끝"
set -e 없이 실행하면 에러 메시지가 나와도 두 번째, 세 번째 echo가 모두 출력됩니다. set -e를 켜면 cp가 실패하는 순간 스크립트가 멈춥니다.
set -e의 예외 상황
set -e가 항상 작동하는 것은 아닙니다. 몇 가지 중요한 예외가 있습니다.
#!/usr/bin/env bashset -e# 1. || 뒤에서는 set -e가 적용되지 않음 (의도적 실패 허용)grep "없는문자" /etc/passwd || echo "매치 없음" # 정상 실행# 2. && 체인에서도 마지막 결과만 확인false && echo "실행 안 됨" # 이 줄은 전체가 false로 평가# 3. if, while, until 조건 내부if ls /없는경로; then # ls가 실패해도 스크립트 종료 없음 echo "있음"else echo "없음"fi# 4. 파이프라인에서 마지막 명령어만 확인 (pipefail 없을 때)cat /없는파일 | sort # cat이 실패해도 sort가 성공이면 통과
||나 if 조건에서 실패가 허용되는 것은 사실 의도된 동작입니다. 명령어가 실패할 수 있다는 것을 명시적으로 다루는 코드에서는 set -e가 개입하지 않습니다.
set -e의 함정 — 함수와 서브쉘
함수 내부에서 실패한 명령어가 함수의 반환값으로 사용될 때는 set -e가 예상대로 작동하지 않을 수 있습니다.
#!/usr/bin/env bashset -echeck() { ls /없는경로 # 실패 return 0}# 함수 결과를 조건으로 사용하면 set -e가 작동하지 않음if check; then echo "성공"fiecho "스크립트 계속 실행됨" # 출력됨 (예상과 다름)
이런 이유로 set -e만으로는 완벽하지 않습니다. 중요한 명령어는 직접 exit code를 확인하는 방어 코드를 추가하는 것이 안전합니다.
set -u — 미정의 변수 사용 시 에러
$unset_var를 참조하면 Bash는 기본적으로 빈 문자열로 처리합니다. 오타로 잘못된 변수명을 쓰면 조용히 빈 값을 사용합니다. set -u를 켜면 정의되지 않은 변수를 사용할 때 오류로 처리합니다.
#!/usr/bin/env bashset -u# 오타로 잘못된 변수명BACKUP_DIR="/tmp/backups"rm -rf "$BACKUP_DIER" # 오타! set -u 없으면 rm -rf "" # set -u면 즉시 오류
set -u가 없는 경우 $BACKUP_DIER는 빈 문자열이 되고 rm -rf ""가 실행됩니다. 이것은 실제로 위험한 상황입니다.
${var:-default}와 함께 사용
set -u를 켜면 환경변수가 설정되지 않은 경우도 오류로 처리됩니다. 기본값이 있는 변수는 ${var:-default} 패턴을 사용합니다.
#!/usr/bin/env bashset -u# 환경변수가 없으면 오류echo "$LOG_LEVEL" # LOG_LEVEL이 없으면 오류# 기본값을 지정하면 안전LOG_LEVEL="${LOG_LEVEL:-INFO}"MAX_RETRY="${MAX_RETRY:-3}"TIMEOUT="${TIMEOUT:-30}"echo "로그 레벨: $LOG_LEVEL"echo "최대 재시도: $MAX_RETRY"echo "타임아웃: $TIMEOUT"
${var:-default}는 var가 설정되지 않았거나 빈 문자열일 때 default를 사용합니다. ${var:=default}는 같은 조건에서 var에 default를 할당하고 반환합니다. $@, $* 같은 위치 매개변수는 set -u 환경에서도 빈 배열일 때 에러가 나므로 "${@:-}" 또는 "$*" 앞에 인수 개수를 먼저 확인하는 것이 좋습니다.
set -o pipefail — 파이프라인 중간 실패 감지
파이프라인에서 Bash는 기본적으로 마지막 명령어의 종료 코드만 확인합니다. 앞쪽 명령어가 실패해도 마지막이 성공이면 전체 파이프라인이 성공으로 처리됩니다.
#!/usr/bin/env bashset -e # set -e만 켜고# /없는파일이 없어서 cat이 실패하지만# sort는 성공(빈 입력 처리)이므로 전체가 성공으로 처리됨cat /없는파일 | sort | uniq > /tmp/result.txtecho "파이프라인 종료 코드: $?" # 0 (잘못된 결과!)
set -o pipefail을 추가하면 파이프라인에서 어떤 명령어든 실패하면 전체 파이프라인을 실패로 처리합니다.
#!/usr/bin/env bashset -eo pipefailcat /없는파일 | sort | uniq > /tmp/result.txtecho "이 줄은 실행되지 않습니다."
pipefail을 켜면 파이프라인 중간의 실패를 잡을 수 있어 데이터 처리 스크립트의 안정성이 크게 높아집니다.
조합 — set -euo pipefail
세 옵션을 따로 쓰는 것보다 한 줄로 조합하는 것이 일반적입니다.
#!/usr/bin/env bashset -euo pipefail
-euo는 -e -u -o와 같습니다. pipefail은 -o의 인수로 전달됩니다. 이것이 방어적 Bash 스크립트의 첫 줄입니다.
실습 — set 옵션 유무에 따른 동작 차이 비교
set -euo pipefail이 있을 때와 없을 때 스크립트 동작이 어떻게 달라지는지 직접 비교합니다.
#!/usr/bin/env bash# 새 파일: compare_set_options.sh# 1. set 옵션 없이 실행echo "=== set 옵션 없음 ==="( TYPO_VAR="hello" echo "미정의 변수: $TYPO_VAP" # 오타 변수 → 빈 문자열 ls /없는경로 # 실패 → 무시 cat /없는파일 | sort # cat 실패 → sort는 성공 echo "파이프라인 종료 코드: $?" # 0 출력 echo "스크립트 계속 실행")echo ""# 2. set -euo pipefail로 실행echo "=== set -euo pipefail ==="( set -euo pipefail TYPO_VAR="hello" # echo "미정의 변수: $TYPO_VAP" # 주석 해제하면 즉시 오류 ls /없는경로 2>/dev/null || echo "ls 실패 (무시하고 계속)" # 파이프라인 실패 감지 if cat /없는파일 2>/dev/null | sort; then echo "파이프라인 성공" else echo "파이프라인 실패 감지됨 (종료 코드: $?)" fi echo "set -euo pipefail 구간 완료")echo ""echo "비교 완료"
실행 결과입니다.
=== set 옵션 없음 ===
미정의 변수:
ls: /없는경로: No such file or directory
파이프라인 종료 코드: 0
스크립트 계속 실행
=== set -euo pipefail ===
ls 실패 (무시하고 계속)
파이프라인 실패 감지됨 (종료 코드: 1)
set -euo pipefail 구간 완료
비교 완료
같은 상황에서 옵션 없이는 오류를 조용히 넘어가고, set -euo pipefail은 각 실패를 잡아냅니다. 이 차이가 새벽 3시에 혼자 돌아가는 배치 스크립트를 신뢰할 수 있게 만드는 핵심입니다.