배포 스크립트 맨 위에 set -e를 적어 두었습니다. 명령 하나가 실패하면 그 자리에서 멈추라는 뜻이니 안심했는데, 로그를 보면 grep이 파일을 찾지 못했다는 오류 바로 다음 줄에 "완료"가 찍혀 있습니다. set -e가 고장 난 것이 아니라, 이 옵션이 일부러 실패를 못 본 척하는 자리가 정해져 있기 때문입니다. 이 글을 읽고 나면 그 자리가 어디인지 짚을 수 있고, 스크립트를 어떻게 고쳐야 실패가 정말로 멈춤으로 이어지는지 알 수 있습니다.
예제는 2026-10-12에 macOS 27.0.1에 들어 있는 /bin/bash 3.2.57에서 실행했습니다. 각 경우를 별도의 bash 프로세스로 돌려 한 경우의 종료가 다음 경우를 막지 않게 했고, missing.log 같은 파일은 일부러 만들지 않았습니다. 동작 설명은 GNU Bash 5.3 매뉴얼(2025-05-18판)과 맞춰 보았습니다. Bash 4.4 이후에만 있는 옵션은 이 Mac에서 실행하지 못했으므로 문서로 확인한 내용이라고 따로 밝힙니다.
파이프라인은 마지막 명령의 결과만 본다
가장 흔한 경우부터 보겠습니다.
set -e
grep 'ERROR' missing.log | sort > errors.txt
echo "after pipeline: still running"
실행하면 grep: missing.log: No such file or directory가 나온 뒤에도 다음 줄이 찍히고 스크립트는 종료 코드 0으로 끝났습니다. 파이프라인의 종료 상태는 기본적으로 마지막 명령의 상태이고, 여기서 마지막 명령인 sort는 빈 입력을 받아 정상 종료했습니다. Bash 매뉴얼의 set -e 설명도 파이프라인의 마지막이 아닌 명령이 실패한 경우에는 셸이 끝나지 않는다고 적고 있습니다.
같은 스크립트에 set -o pipefail을 더하면 결과가 달라졌습니다. 다음 줄은 찍히지 않았고 종료 코드는 2였습니다. pipefail이 켜지면 파이프라인의 상태는 0이 아닌 상태로 끝난 명령 가운데 가장 오른쪽 것이 되고, 여기서는 grep이 파일을 못 찾았을 때 돌려준 2가 그대로 스크립트의 종료 코드가 됐습니다. 이 옵션은 기본으로 꺼져 있습니다.

if, &&, || 안에서는 실패가 '검사 결과'가 된다
두 번째 자리는 조건입니다. test -f config.env && echo "config found"에서 파일이 없으면 test가 실패하지만 스크립트는 다음 줄로 넘어갔습니다. 매뉴얼은 while·until 뒤의 조건, if의 검사 부분, &&·|| 목록에서 마지막이 아닌 명령, !로 결과를 뒤집은 명령은 실패해도 셸이 끝나지 않는다고 정합니다. 그 실패는 오류가 아니라 분기를 고르기 위한 답으로 취급됩니다.
문제는 이 예외가 함수 안까지 번진다는 점입니다. 아래의 load 함수는 첫 줄의 cp가 실패하는 함수입니다.
load() {
cp missing.conf copy.conf
echo "inside load: cp failed but kept going"
}
if load; then echo "if says load succeeded"; fi
load
if load로 불렀을 때는 cp의 오류가 나온 뒤에도 함수 안의 echo가 실행됐고, 함수의 마지막 명령인 echo가 성공했으므로 if는 "성공"으로 판단했습니다. 같은 함수를 조건 밖에서 그냥 부르자 이번에는 cp에서 바로 멈추고 종료 코드 1로 끝났습니다. 매뉴얼은 -e가 무시되는 맥락에서 실행되는 함수나 복합 명령 안의 명령에는 -e가 전혀 적용되지 않는다고 설명합니다. 함수 안에서 다시 set -e를 적어도 그 맥락이 끝날 때까지는 효과가 없습니다.
개발 노트
set -e가 실패를 못 본 척하는 자리
이 자리에서 실패한 명령은 스크립트를 멈추지 않는다
파이프라인의 앞쪽
grep … | sort- 마지막 명령의 상태만 봄
- pipefail로 바꿀 수 있음
if·while·until의 조건
if load; then …- 실패가 '거짓'으로 쓰임
- 분기를 고르는 답
&&·|| 목록의 앞쪽
test -f x && echo ok- 마지막 명령만 멈춤 대상
- !로 뒤집은 명령도 제외
그 안에서 부른 함수
if load → cp: 1 · echo: 0- 함수 본문 전체가 -e 없이 실행
- 안에서 set -e를 다시 써도 무효
GNU Bash 5.3 매뉴얼 The Set Builtin 기준. 예시는 macOS /bin/bash 3.2.57에서 2026-10-12에 실행했습니다.
local x=$(…)는 실패를 덮는다
세 번째 자리는 변수 선언입니다. 함수 안에서 local v=$(cat missing.txt)라고 쓰면 cat이 실패해도 다음 줄이 실행됐고 v는 빈 문자열이었습니다. 이 한 줄의 종료 상태는 cat이 아니라 local 명령의 것이기 때문입니다. 매뉴얼은 local의 반환 상태가 함수 밖에서 쓰였거나 이름이 잘못됐거나 읽기 전용 변수일 때만 0이 아니라고 적습니다. 명령 치환이 실패했는지는 여기에 들어 있지 않습니다.
선언과 대입을 나누면 달라집니다. local w로 먼저 선언하고 다음 줄에서 w=$(cat missing.txt)를 실행하자, 그 줄의 상태는 명령 치환의 상태가 되어 스크립트가 종료 코드 1로 멈췄습니다. export도 마찬가지여서, 같은 함수에서 export E=$(cat missing.txt)는 cat이 실패했는데도 다음 줄로 넘어갔습니다. 이것도 선언과 대입을 나누면 됩니다.
명령 치환 안쪽도 따로 봐야 합니다. out=$(false; echo "inner line after false")를 실행하자 false 뒤의 echo까지 실행되어 out에 그 문자열이 들어갔고, 마지막 명령이 성공했으므로 대입도 성공으로 끝났습니다. Bash는 POSIX 모드가 아니면 명령 치환을 실행하는 서브셸에서 -e를 끈다고 매뉴얼에 적혀 있습니다. Bash 4.4부터는 shopt -s inherit_errexit로 이 서브셸이 -e를 물려받게 할 수 있다고 Bash 4.4 NEWS와 매뉴얼이 설명합니다. macOS의 Bash 3.2.57에는 이 옵션이 없어 실행해 보지 못했습니다.
개발 노트
local v=$(…)는 왜 멈추지 않을까
한 줄의 종료 상태가 누구의 것인가
명령 치환
$(cat missing.txt)- cat이 실패해 상태 1
- 결과는 빈 문자열
local이 대입
local v=$(…) → 0- 줄의 상태는 local의 것
- 선언에 성공했으니 0
나눠서 쓰면
local w; w=$(…) → 1- 대입 줄의 상태 = 치환의 상태
- set -e가 여기서 멈춤
export E=$(…)도 실행해 보니 같은 방식으로 실패를 덮었습니다. 명령 치환 안쪽은 Bash가 POSIX 모드가 아니면 -e를 끈 채 실행합니다.
멈춰야 할 곳에서 멈추게 하려면
마지막 실행은 위의 세 가지를 고친 버전입니다. 맨 위를 set -euo pipefail로 바꾸고(-u는 정의되지 않은 변수를 오류로 만듭니다), 조건 안에서 불릴 수 있는 함수에서는 실패할 수 있는 명령마다 cp missing.conf … || return 1처럼 직접 실패를 돌려주게 했습니다. 변수는 local w와 w=$(…) 두 줄로 나눴습니다.
결과는 기대한 대로였습니다. if load는 이번에는 실패를 보고 else 쪽으로 가서 상태 1을 출력했고, read_version은 cat이 실패한 줄에서 멈춰 스크립트가 종료 코드 1로 끝났습니다. 그 뒤의 줄은 하나도 실행되지 않았습니다.

set -e는 "모든 실패에서 멈춘다"가 아니라 "검사에 쓰이지 않은 명령이 실패하면 멈춘다"에 가깝습니다. 그래서 이 옵션만 믿기보다, 실패하면 안 되는 단계에서 결과를 직접 확인하는 편이 안전합니다. 확인 순서를 정리하면 아래와 같습니다.
개발 노트
실패가 멈춤으로 이어지게 쓰기
스크립트에서 찾아볼 네 군데
맨 윗줄
set -euo pipefail- 파이프 앞쪽 실패도 잡기
- -u는 없는 변수를 오류로
조건에서 부르는 함수
cp a b || return 1- 실패할 단계마다 직접 반환
- if가 실패를 보게 됨
local·export와 $(…)
local w; w=$(cmd)- 선언과 대입을 두 줄로
- 대입 줄이 실패를 전함
$(…) 안쪽
shopt -s inherit_errexit- 마지막 명령만 결과가 됨
- Bash 4.4부터 쓸 수 있음
inherit_errexit는 Bash 4.4에 추가된 옵션이라 macOS 기본 Bash 3.2.57에는 없고, 이 글에서는 실행하지 않았습니다(매뉴얼·NEWS로 확인).
지금 가진 스크립트에서 바로 해 볼 것은 하나입니다. |가 들어간 줄, if나 &&에서 부르는 함수, local·export와 $(…)가 한 줄에 있는 곳을 찾아보세요. 그 세 군데가 set -e가 조용히 넘어가는 자리입니다.
이 기록에 대화를 더해 주세요.
궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.
로그인 상태 확인 중…
댓글을 불러오는 중…