본문 바로가기
빌드노트 검색

빌드노트

Bash 함수에서 명령은 실패했는데 local 다음 상태는 0이다

Bash 함수에서 local value=$(command) 뒤의 상태 0은 내부 명령의 성공을 보장하지 않는다. 같은 출력과 종료 상태 7을 내는 명령으로 선언·대입을 합친 경우와 분리한 경우를 비교하고, 대입 직후 실패를 저장해 반환하는 방법을 설명한다.

Bash 함수에서 local value=$(command) 바로 뒤의 $?가 0이어도 command가 성공했다고 판단하면 안 된다. 이때 읽는 것은 local 내장 명령의 종료 상태다. 변수 선언을 local value로 먼저 하고, value=$(command)를 별도 줄에서 실행한 뒤 그 상태를 즉시 검사한다.

같은 출력인데 상태가 달라진다

Bash가 있는 터미널에서 아래 입력을 실행하면 선언과 대입을 합친 함수와 분리한 함수를 비교할 수 있다. 두 함수 모두 draft를 출력한 뒤 상태 7로 끝나는 합성 명령을 사용한다. set +e는 중간 실패에서 자동 종료하지 않고 두 결과를 읽기 위한 설정이다.

bash --noprofile --norc <<'BASH'
set +e
combined() {
    local rc
    local value=$(printf 'draft\n'; exit 7)
    rc=$?
    printf 'combined value=<%s> status=%s\n' "$value" "$rc"
}
separated() {
    local value rc
    value=$(printf 'draft\n'; exit 7)
    rc=$?
    printf 'separated value=<%s> status=%s\n' "$value" "$rc"
}
combined
separated
BASH

Windows 11의 Git for Windows에 포함된 GNU Bash 5.2.26(1)-release에서 다음 출력을 관찰했다. status는 각 줄의 실행 직후 rc=$?로 저장한 값이며, 마지막 printf까지 실행한 함수의 반환값과는 구분한다.

combined value=<draft> status=0
separated value=<draft> status=7

두 경우 모두 변수에 draft가 들어갔다. 명령이 실패하기 전에 출력한 내용도 명령 치환으로 받을 수 있으므로, 값이 비어 있지 않다는 사실은 성공의 증거가 아니다. 차이는 상태에 있다. local을 붙인 줄에서는 0이 나왔고, 대입만 한 줄에서는 합성 명령의 실패 상태 7이 나왔다.

위 입력의 두 exit 7을 모두 exit 0으로 바꿔 다시 실행한 대조에서는 값은 그대로이고 두 상태가 모두 0이었다. 선언과 대입을 합친 쪽의 0만으로는 이 성공과 앞선 실패를 구별할 수 없었다. 결과가 다르면 먼저 bash --version과 실행한 줄을 확인한다. local 자체의 오류가 표준 오류에 나왔다면 여기의 정상 선언 조건과 다르다.

local의 성공과 내부 명령의 성공은 다르다

Debian이 제공하는 GNU Bash 5.2 매뉴얼의 local 설명 (새 탭에서 열림)은 함수 안에서 정상적으로 지역 변수를 선언하면 상태 0을 반환한다고 설명한다. 함수 밖에서 사용하거나 잘못된 이름을 주는 등의 오류는 별개다. 따라서 모든 local이 항상 0을 반환한다는 뜻은 아니다.

한편 같은 매뉴얼의 단순 명령 확장 규칙 (새 탭에서 열림)에 따르면, 명령 이름 없이 대입만 실행하고 명령 치환이 포함돼 있다면 마지막 명령 치환의 상태를 반환한다. 대입이나 리다이렉션 자체의 오류가 없는 조건이다.

두 규칙을 위 입력에 적용하면 차이를 설명할 수 있다. local value=$(...)에는 실행할 내장 명령 local이 있다. 반면 이미 선언한 변수에 쓰는 value=$(...)에는 명령 이름이 없다. 앞쪽은 정상 선언의 0을, 뒤쪽은 명령 치환의 7을 읽은 것이다. 여기서는 유효한 지역 변수 이름과 명령 치환 하나를 사용했다. export나 readonly를 섞은 선언·옵션 조합은 비교하지 않았다.

값을 쓰기 전에 실패를 반환한다

실제 함수에서는 local과 대입을 분리하는 데서 그치지 않고, 실패하면 값을 사용하는 코드까지 도달하지 않게 해야 한다. 다음 입력은 대입 직후 상태를 저장하고, 0이 아니면 오류를 알린 뒤 그 상태로 함수를 끝낸다.

bash --noprofile --norc <<'BASH'
set +e
read_value() {
    local value rc
    value=$(printf 'draft\n'; exit 7)
    rc=$?
    if (( rc != 0 )); then
        printf 'read_value failed: %s\n' "$rc" >&2
        return "$rc"
    fi
    printf 'use=<%s>\n' "$value"
}
read_value
rc=$?
printf 'caller status=%s\n' "$rc"
BASH

실행 결과 표준 오류에는 read_value failed: 7, 표준 출력에는 caller status=7이 나왔고, use=<draft>는 나오지 않았다. 이 입력의 exit 7만 exit 0으로 바꾼 대조에서는 표준 오류 없이 use=<draft>와 caller status=0이 출력됐다.

자기 함수에 적용할 때는 printf 'draft\n'; exit 7 자리를 출력을 받아야 할 명령으로 바꾸고, 대입과 rc=$? 사이에 로그 출력이나 다른 명령을 넣지 않는다. $?는 바로 앞 명령의 상태이므로 중간 명령을 실행하면 검사할 값이 바뀐다. 오류 메시지도 상태를 저장한 다음에 출력한다. return "$rc"는 저장한 실패 상태로 함수를 끝내는 처리다. Bash return 설명 (새 탭에서 열림)

위 실습은 호출자가 함수의 상태를 출력하고 계속한다. 마지막 출력이 성공하므로 실습용 Bash 프로세스 자체의 최종 상태는 0이다. 실제 스크립트 전체도 중단해야 한다면 호출자에서 반환값을 검사해 종료하도록 처리한다.

직접 확인한 실행 범위는 새 비대화형 Bash의 기본 모드, 일반 지역 변수, 합성 명령의 상태 0과 7이다. Linux 런타임에서의 재실행과 다른 셸·POSIX 모드·트랩·실제 외부 명령의 내부 실패 처리는 확인하지 않았다.

← 전체 글 보기