Bash 기본 동작은 명령 하나가 실패해도 스크립트가 멈추지 않고 다음 줄로 넘어간다. mkdir가 실패했는데 그다음 cd와 rm -rf *가 엉뚱한 디렉터리에서 실행되는 식의 사고가 여기서 나온다. set -euo pipefail은 이 세 옵션을 한 번에 켜서 “뭔가 실패하면 그 자리에서 멈춘다”는 전제를 스크립트 맨 위에 박아두는 관용구다. 이 글에서는 세 옵션 각각의 동작을 실제로 확인하고, -e가 겉보기와 달리 작동하지 않는 흔한 함정 두 가지도 직접 재현한다.
핵심 개념
| 옵션 | 동작 |
|---|---|
set -e | 명령이 0이 아닌 종료 코드를 반환하면 스크립트를 즉시 종료 |
set -u | 선언되지 않은 변수를 참조하면 에러로 처리하고 종료 |
set -o pipefail | 파이프라인의 마지막 명령이 아니라, 중간에 실패한 명령이 있으면 그 종료 코드를 파이프라인 전체의 종료 코드로 사용 |
실전: 옵션별 동작 확인
set -e 없이는 실패한 명령 다음 줄이 그대로 실행된다.
#!/bin/bash
false
echo "이 줄이 실행됨 (set -e 없음)"$ bash no_e.sh; echo "exit=$?"
이 줄이 실행됨 (set -e 없음)
exit=0
set -e를 켜면 false에서 즉시 종료된다.
#!/bin/bash
set -e
false
echo "이 줄은 실행 안 됨"$ bash with_e.sh; echo "exit=$?"
exit=1
set -u는 오타로 변수명을 잘못 쓰거나 인자를 안 받은 경우를 바로 잡아낸다.
#!/bin/bash
set -u
echo "이름: $USERNAME"$ bash with_u.sh; echo "exit=$?"
with_u.sh: line 3: USERNAME: unbound variable
exit=1
pipefail 없이는 파이프라인 마지막 명령의 종료 코드만 보므로 앞 단계가 실패해도 드러나지 않는다.
#!/bin/bash
false | true
echo "pipefail 없이 exit: $?"
set -o pipefail
false | true
echo "pipefail 켠 뒤 exit: $?"pipefail 없이 exit: 0
pipefail 켠 뒤 exit: 1
흔히 놓치는 함정: -e가 안 걸리는 경우
if 조건에 들어간 명령의 실패는 set -e가 무시한다 — 조건 판단이 원래 목적이라 당연한 동작이지만 실수로 기대하기 쉽다.
#!/bin/bash
set -e
if false; then
echo "안 들어옴"
fi
echo "여기까지 정상 도달 (set -e가 if 조건의 실패는 무시함)"$ bash if_gotcha.sh; echo "exit=$?"
여기까지 정상 도달 (set -e가 if 조건의 실패는 무시함)
exit=0
더 위험한 건 local result=$(fail_cmd) 패턴이다. local 자체의 종료 코드가 $(...) 안 명령의 실패를 덮어써서 set -e가 실패를 못 본다.
#!/bin/bash
set -e
fail_cmd() { return 1; }
get_value() {
local result=$(fail_cmd)
echo "함수는 여기까지 왔음: result=[$result]"
}
get_value
echo "get_value 호출 뒤에도 스크립트가 계속 실행됨"$ bash local_gotcha.sh; echo "exit=$?"
함수는 여기까지 왔음: result=[]
get_value 호출 뒤에도 스크립트가 계속 실행됨
exit=0
fail_cmd가 1을 반환했는데도 get_value는 끝까지 실행됐다. 선언과 대입을 분리해 local result; result=$(fail_cmd)로 쓰면 local이 아니라 대입문 자체의 종료 코드가 살아남아 set -e가 정상적으로 잡는다.
주의사항
set -e는if/while/until조건,&&/||의 좌변,!가 붙은 명령의 실패는 무시한다 — 실패 여부를 판단하는 게 그 명령의 목적인 위치이기 때문이다.local var=$(cmd)처럼 선언과 대입을 한 줄에 쓰면 명령 실패가 가려진다. 실패를set -e로 잡아야 하는 대입이라면 선언과 대입을 분리한다.- 파이프라인 안에서 실패를 잡으려면
pipefail이 반드시 필요하다.curl ... | jq ...처럼 앞 단계가 실패해도jq가 빈 입력에 종료 코드 0을 낼 수 있는 조합은pipefail없이는 절대 못 잡는다. - bash 4.4 이전에서는 인자 없이 실행됐을 때
"$@"참조도set -u에 걸린다. 4.4부터는"$@"/"$*"가 예외로 처리된다 — 오래된 시스템 bash 버전을 확인해둘 것. - 실패 시 어디서 멈췄는지 바로 알고 싶다면
trap 'echo "실패: 줄 $LINENO"' ERR를set -euo pipefail바로 아래 추가하면 좋다.
마무리
set -euo pipefail 세 줄은 스크립트가 조용히 절반만 실행된 채 다음 단계로 넘어가는 사고 대부분을 막아준다. 다만 if 조건이나 local var=$(cmd) 같은 자리에서는 여전히 실패가 새어나갈 수 있다는 걸 알고, 실패 여부가 중요한 대입은 선언과 분리하는 습관을 들이는 게 안전하다.