set -euo pipefail로 안전한 Bash 스크립트 작성하기

Bash 기본 동작은 명령 하나가 실패해도 스크립트가 멈추지 않고 다음 줄로 넘어간다. mkdir가 실패했는데 그다음 cdrm -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 -eif/while/until 조건, &&/||의 좌변, !가 붙은 명령의 실패는 무시한다 — 실패 여부를 판단하는 게 그 명령의 목적인 위치이기 때문이다.
  • local var=$(cmd)처럼 선언과 대입을 한 줄에 쓰면 명령 실패가 가려진다. 실패를 set -e로 잡아야 하는 대입이라면 선언과 대입을 분리한다.
  • 파이프라인 안에서 실패를 잡으려면 pipefail이 반드시 필요하다. curl ... | jq ...처럼 앞 단계가 실패해도 jq가 빈 입력에 종료 코드 0을 낼 수 있는 조합은 pipefail 없이는 절대 못 잡는다.
  • bash 4.4 이전에서는 인자 없이 실행됐을 때 "$@" 참조도 set -u에 걸린다. 4.4부터는 "$@"/"$*"가 예외로 처리된다 — 오래된 시스템 bash 버전을 확인해둘 것.
  • 실패 시 어디서 멈췄는지 바로 알고 싶다면 trap 'echo "실패: 줄 $LINENO"' ERRset -euo pipefail 바로 아래 추가하면 좋다.

마무리

set -euo pipefail 세 줄은 스크립트가 조용히 절반만 실행된 채 다음 단계로 넘어가는 사고 대부분을 막아준다. 다만 if 조건이나 local var=$(cmd) 같은 자리에서는 여전히 실패가 새어나갈 수 있다는 걸 알고, 실패 여부가 중요한 대입은 선언과 분리하는 습관을 들이는 게 안전하다.

참고

답글 남기기