exit code와 종료 상태
명령어 하나를 실행할 때마다 Bash는 그 결과를 숫자 하나로 기록합니다. 0이면 성공, 0이 아니면 실패입니다. 이 숫자가 exit code(종료 코드)입니다.
겉보기에는 단순해 보이지만, exit code를 제대로 다루는 것이 견고한 스크립트의 출발점입니다. 어떤 명령어가 왜 실패했는지, 스크립트 전체가 성공으로 끝났는지 실패로 끝났는지를 exit code로 판단합니다.
$? — 직전 명령어의 종료 코드
$?는 가장 최근에 실행된 명령어의 종료 코드를 담고 있는 특수 변수입니다.
# 파일: exit_code_basic.shls /tmpecho "ls 종료 코드: $?" # 성공이면 0 출력ls /존재하지않는경로echo "ls 종료 코드: $?" # 실패이면 2 출력grep "없는문자열" /etc/passwdecho "grep 종료 코드: $?" # 매치 없으면 1 출력
실행 결과입니다.
/tmp
ls 종료 코드: 0
ls: /존재하지않는경로: No such file or directory
ls 종료 코드: 2
grep 종료 코드: 1
$?는 다음 명령어가 실행되는 순간 덮어써집니다. 나중에 참조할 계획이라면 즉시 변수에 저장해야 합니다.
some_commandexit_code=$? # 즉시 저장# ... 다른 명령어들 ...if [[ $exit_code -ne 0 ]]; then echo "some_command가 실패했습니다. 코드: $exit_code"fi
관례적 종료 코드
모든 프로그램이 같은 코드로 같은 의미를 전달하지는 않지만, 오랫동안 사용되어 온 관례가 있습니다.
| 코드 | 의미 | 예시 |
|---|---|---|
| 0 | 성공 | 명령어 정상 완료 |
| 1 | 일반적인 오류 | 문법 오류, 잘못된 인수 |
| 2 | 잘못된 사용법 | 옵션 오류, 인수 누락 |
| 126 | 명령어 실행 불가 | 실행 권한 없음 |
| 127 | 명령어를 찾을 수 없음 | PATH에 없는 명령어 |
| 128+N | 시그널로 종료 | Ctrl+C(SIGINT)면 128+2=130 |
# 파일: exit_code_demo.sh# 127: 존재하지 않는 명령어nonexistent_command 2>/dev/nullecho "존재하지 않는 명령어: $?" # 127# 126: 실행 권한 없음touch /tmp/no_exec.shbash /tmp/no_exec.sh 2>/dev/nullecho "실행 권한 없음: $?" # 126 또는 1 (버전에 따라 다름)# 130: Ctrl+C (SIGINT, 128+2)# sleep 10 실행 중 Ctrl+C를 누르면 종료 코드가 130
grep은 매치가 있으면 0, 없으면 1, 오류면 2를 반환합니다. diff는 파일이 같으면 0, 다르면 1, 오류면 2입니다. 도구마다 코드 의미가 다르므로 man 페이지의 EXIT STATUS 섹션을 확인하는 습관을 들이는 것이 좋습니다.
자체 exit code 정의
스크립트 안에서 의미 있는 종료 코드를 직접 정의할 수 있습니다. 숫자를 그대로 쓰면 코드 읽기가 어려우므로 readonly로 상수를 선언하는 것이 좋습니다.
# 파일: custom_exit_codes.sh#!/usr/bin/env bashset -uo pipefail# 종료 코드 상수 정의readonly EXIT_OK=0readonly EXIT_ERROR=1readonly EXIT_USAGE=2readonly EXIT_NOT_FOUND=3readonly EXIT_PERMISSION=4readonly EXIT_TIMEOUT=5usage() { echo "사용법: $0 <파일경로>" exit "$EXIT_USAGE"}process_file() { local file="$1" if [[ ! -e "$file" ]]; then echo "오류: 파일을 찾을 수 없습니다: $file" >&2 exit "$EXIT_NOT_FOUND" fi if [[ ! -r "$file" ]]; then echo "오류: 파일을 읽을 수 없습니다: $file" >&2 exit "$EXIT_PERMISSION" fi echo "처리 중: $file" wc -l "$file" exit "$EXIT_OK"}# 인수 검사if [[ $# -ne 1 ]]; then usagefiprocess_file "$1"
이렇게 정의해두면 스크립트를 호출하는 쪽에서 종료 코드를 보고 무슨 일이 있었는지 알 수 있습니다.
./custom_exit_codes.sh /etc/passwdecho "종료 코드: $?" # 0./custom_exit_codes.sh /없는파일echo "종료 코드: $?" # 3 (NOT_FOUND)./custom_exit_codes.shecho "종료 코드: $?" # 2 (USAGE)
return vs exit
return과 exit는 둘 다 종료 코드를 반환하지만 적용 범위가 다릅니다.
return은 함수 내에서만 사용합니다. 함수를 호출한 곳으로 제어권을 돌려줍니다. exit는 스크립트 전체를 종료합니다. 함수 안에서 exit를 호출하면 함수만 끝나는 게 아니라 스크립트 전체가 종료됩니다.
# 파일: return_vs_exit.sh#!/usr/bin/env bashcheck_file() { local file="$1" if [[ ! -f "$file" ]]; then echo "파일 없음: $file" >&2 return 1 # 함수만 종료, 스크립트 계속 실행 fi echo "파일 확인: $file" return 0}# 함수 호출 후 결과 확인if check_file "/etc/passwd"; then echo "passwd 파일 존재"fiif check_file "/없는파일"; then echo "이 줄은 출력되지 않음"else echo "파일 없음 처리됨"fiecho "스크립트는 계속 실행됩니다."
실행 결과입니다.
파일 확인: /etc/passwd
passwd 파일 존재
파일 없음: /없는파일
파일 없음 처리됨
스크립트는 계속 실행됩니다.
함수 안에서 exit를 쓰면 이야기가 달라집니다.
fatal_error() { echo "치명적 오류: $1" >&2 exit 1 # 스크립트 전체 종료}fatal_error "데이터베이스 연결 실패"echo "이 줄은 절대 실행되지 않습니다."
용도에 맞게 구분해야 합니다. 복구 가능한 오류는 return으로 처리해 호출자가 결정하게 하고, 더 이상 진행이 불가능한 치명적 오류는 exit로 스크립트를 종료합니다.
실습 — 종료 코드 활용 스크립트
파일을 백업하는 스크립트를 작성합니다. 원본 파일이 없으면 3, 백업 디렉토리가 없으면 4, 성공이면 0을 반환합니다.
#!/usr/bin/env bash# 새 파일: backup.shset -uo pipefailreadonly EXIT_OK=0readonly EXIT_USAGE=2readonly EXIT_SRC_NOT_FOUND=3readonly EXIT_DST_NOT_FOUND=4readonly EXIT_COPY_FAILED=5usage() { echo "사용법: $0 <원본파일> <백업디렉토리>" echo "" echo "종료 코드:" echo " 0 성공" echo " 2 잘못된 사용법" echo " 3 원본 파일 없음" echo " 4 백업 디렉토리 없음" echo " 5 복사 실패" exit "$EXIT_USAGE"}[[ $# -ne 2 ]] && usageSRC_FILE="$1"BACKUP_DIR="$2"if [[ ! -f "$SRC_FILE" ]]; then echo "오류: 원본 파일을 찾을 수 없습니다: $SRC_FILE" >&2 exit "$EXIT_SRC_NOT_FOUND"fiif [[ ! -d "$BACKUP_DIR" ]]; then echo "오류: 백업 디렉토리가 없습니다: $BACKUP_DIR" >&2 exit "$EXIT_DST_NOT_FOUND"fiTIMESTAMP=$(date '+%Y%m%d_%H%M%S')BACKUP_NAME="$(basename "$SRC_FILE").${TIMESTAMP}.bak"BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}"if cp "$SRC_FILE" "$BACKUP_PATH"; then echo "백업 완료: $BACKUP_PATH" exit "$EXIT_OK"else echo "오류: 복사에 실패했습니다." >&2 exit "$EXIT_COPY_FAILED"fi
스크립트를 실행하고 종료 코드를 확인합니다.
chmod +x backup.sh./backup.sh /etc/passwd /tmp/backupsecho "종료 코드: $?" # 4 (백업 디렉토리 없음)mkdir /tmp/backups./backup.sh /etc/passwd /tmp/backupsecho "종료 코드: $?" # 0 (성공)./backup.sh /없는파일 /tmp/backupsecho "종료 코드: $?" # 3 (원본 없음)
실행 결과입니다.
오류: 백업 디렉토리가 없습니다: /tmp/backups
종료 코드: 4
백업 완료: /tmp/backups/passwd.20260424_093012.bak
종료 코드: 0
오류: 원본 파일을 찾을 수 없습니다: /없는파일
종료 코드: 3
이제 이 스크립트를 호출하는 상위 스크립트에서 종료 코드를 보고 적절히 대응할 수 있습니다. 단순한 숫자 하나가 스크립트 간의 소통 수단이 됩니다.