Exit Status

터미널에서 명령을 단계적으로 실행해서 작업을 완성할 때는 여태까지 실행한 명령들이 모두 정상적으로 완료되었기 때문에 가능한 것인데요. 가령 cd 를 하고, 인터넷에서 파일을 다운받고, 압축을 풀고 ... 하는 과정에서 하나라도 오류가 발생한다면 전체 작업이 완성될 수 없겠죠. 따라서 앞선 명령이 정상적으로 실행을 완료했는지, 아니면 오류가 발생했는지를 판단할 수 있는 방법이 필요한데 이것이 종료 상태 값이 하는 역할입니다. 실제 shell 에서 제일 중요한 개념중 하나가 종료 상태 값 입니다.

shell 에서 실행되는 모든 명령은 빠짐없이 종료 상태 값을 반환합니다. if, while, until, &&, || 모두 종료 상태 값을 사용해서 참, 거짓을 판단합니다. 기본적으로 명령이 정상 종료하였을 경우는 0 을 반환하고 그 이외의 값들은 오류를 분류하는 용도로 사용됩니다. 프로그래밍 언어에서처럼 특정값 ( 스트링 또는 예약어 ) 를 사용하지 않습니다.

명령의 종료 상태 값은 $? 변수를 통해 알아볼 수 있습니다.

$ date
Wed Feb 20 10:54:04 KST 2019

$ echo $?                      # 정상 종료 상태 값은 '0'
0

$ date -@
date: invalid option -- '@'
Try 'date --help' for more information.

$ echo $?                      # '0' 이외의 값은 오류를 나타냄
1
..........................................................

curl -O ftp.kaist.ac.kr/ubuntu-cd/bionic/ubuntu-desktop-amd64.iso

if test $? = 0; then 
    echo "download succeeded"
else 
    echo "download failed"
fi

종료코드 0 이 참이라고 해서 다음과 같이 사용할 수 없습니다.

$ if 0; then echo 111; else echo 222; fi
0: command not found

$ 0 && echo 111
0: command not found

앞선 명령이 정상 종료되어야 할 경우

앞서 실행된 명령이 오류로 종료하였을 경우 뒤에 이어지는 명령이 실행되면 안될 경우가 있습니다. 이럴 때는 다음과 같이 && 연산자를 이용해 명령을 연결하거나 test 명령을 이용해 직접 $? 값을 체크합니다.

$ command1 ... && command2 ... && command3 ...

----------------------------------------------

command1 ....

if test $? != 0; then
    echo command failed
    exit 1
fi

앞선 명령의 종료 상태 값과 상관없이 이후 명령이 실행되려면

$ command1 ...; command2 ...; command3 ...

종료 상태 값 지정

스크립트 파일이나 subshell 은 프로세스가 새로 생성되는 것이므로 종료 상태 값을 지정할때 exit 명령으로 합니다. function 이나 source 명령으로 읽어들이는 경우에는 return 명령으로 합니다. 종료 상태 값을 지정하지 않으면 마지막으로 실행된 명령의 종료 상태 값이 사용됩니다. 따라서 스크립트 작성시 직접 $? 변수를 사용해야 되는 경우는 많지 않습니다.

# 마지막 명령의 종료 상태 값이 사용되므로 따로 return 명령을 사용할 필요가 없다.
command_exist () {
    which "$1" > /dev/null;
    # return $?
}

if command_exist vim; then
    ...
fi
--------------------------------------------------------------------------------

# 다음의 경우는 함수 중간에서 return 명령을 사용하였지만 종료 상태 값을 지정하지 않았는데요.
# 이때도 자동으로 마지막 명령 builtin cd "$@" 의 종료 상태 값이 사용됩니다. 
function cd() {
    ...
    builtin cd "$@"
    return
    ...
    ...
}
---------------------------------------

$ func1() { true && return; echo end... ;}   
$ func1
$ echo $?     # 마지막에 실행된 true 명령의 종료 상태 값이 된다.
0

$ func1() { false || return; echo end... ;}  
$ func1
$ echo $?     # 마지막에 실행된 false 명령의 종료 상태 값이 된다.
1

$ ( true && exit; echo end... ); echo $?
0

$ ( false || exit; echo end... ); echo $?
1

만약에 명령이 오류로 종료하였는데 종료 상태 값으로는 0 을 반환하려면 이때는 직접 지정을 해야겠죠

error_command ... || return 0

error_command ... || exit 0

종료코드에는 1 byte 가 사용되므로 0 ~ 255 번을 사용할 수 있습니다. 그중에 0 만 정상 종료를 나타내고 나머지는 오류를 분류해 나타내는데 사용됩니다. shell 에서는 일반적으로 다음과 같은 방식으로 오류를 분류하고 있습니다.

0

정상종료 ( Success )

1

일반적인 에러

let "var = 1 / 0" : division by 0

2

Syntax error, 잘못 사용된 builtin 명령

test.sh: line 6: syntax error near unexpected token 'fi'
exit: 3.14: numeric argument required

126

명령을 실행할 수 없음

명령은 존재하지만 excutable 이 아니거나 퍼미션 문제
bash: ./mylogfile.txt: Permission denied

127

명령 (파일) 이 존재하지 않음

typo 또는 $PATH 문제
asdfg: command not found

128 + N

Signal N 에의한 종료.

가령 kill -9 PID 로 종료 됐다면 $? 값은 128 + 9 = 137

참, 거짓 판단에 복수개의 명령이 사용될 경우

참, 거짓의 판단에 사용되는 명령이 위치하는 자리에는 꼭 하나의 명령만 올수 있는 것은 아닙니다. &&, || 또는 { ;}, ( ) 를 이용한 명령 그룹이 올 수도 있고 ; 를 이용해서 여러개의 명령을 사용할 수도 있습니다. 또한 | 파이프를 이용해서 여러 명령이 연결될 수도 있는데 어떤 경우이건 모두 마지막으로 실행되는 명령의 종료 상태 값이 참, 거짓의 판단에 사용됩니다.

# 마지막 명령인 test -z 의 종료상태 값이 참, 거짓 판단에 사용된다.
until read -r line
    line=$(echo "$line" | tr -d '\r\n')
    test -z "$line"
do
    ...
done

pipe 로 연결된 명령의 종료 상태 값

pipe 로 여러 명령이 연결되어 실행될 때는 마지막 명령의 종료 상태 값이 사용됩니다. 따라서 다음 첫번째 명령문은 항상 종료 상태 값으로 0 을 반환합니다. 왜냐하면 command1 의 실패에 상관없이 sed 명령은 항상 참을 반환하기 때문입니다. command1 명령이 실패하였을 때 비정상 종료 상태 값이 반환되려면 두 번째와 같이 pipefail 옵션을 사용해야 합니다. pipefail 옵션은 파이프에 연결된 명령들 중에 하나라도 오류가 생길 경우 비정상 종료 상태 값을 반환합니다.


    command1 arg1 arg2 | sed -n '/<main>:/,/^$/p'

..................................................

# 옵션 설정이 현재 shell 에 적용되지 않도록 subshell 을 사용.
(
    set -o pipefail
    command1 arg1 arg2 | sed -n '/<main>:/,/^$/p'
)

sh 의 경우는 pipefail 옵션을 사용할 수 없으므로 다음과 같이 명령을 분리해 작성하거나 redirections 예제 와 같은 방법을 사용할 수 있습니다.

# 명령을 두개로 분리해서 작성
command1 arg1 arg2 > tmpfile

status=$?

sed -n '/<main>:/,/^$/p' tmpfile

echo $status

null 값인 변수는 참일까 거짓일까?

존재하지 않는 변수나 null 값인 변수는 quote 하지 않을경우 참이 되므로 주의해야 합니다.

# $asdfgh 는 존재하지 않는 변수
$ if $asdfgh; then echo true; else echo false; fi
true

$ asdfgh=""
$ $asdfgh && echo true     # null 값인 변수도 참이된다.
true

$ unset -v asdfgh
$ $asdfgh && echo true     # unset 했을 경우도 참이된다.
true

대입 연산의 종료 상태 값은?

= , += 메타문자를 이용한 식은 명령문이 아닙니다. 그래서 ; 로 구분없이 한줄에 여러개를 쓸 수도 있습니다. 종료 상태 값은 기본적으로 항상 0 이지만 명령치환과 함께 사용되면 명령치환 종료 상태 값이 적용됩니다.

# 대입연산은 명령문이 아니므로 한줄에 여러개를 쓸수 있다.
$ AA=11 BB=22 CC=33

$ echo $AA $BB $CC
11 22 33

---------------------------------------

$ test 1 = 2
$ echo $?
1

$ test 1 = 2
$ AA=100
$ echo $?        # 대입연산의 종료 상태 값은 기본적으로 0 이다
0

-------------------------------------------------

# 명령치환과 사용될 경우 명령치환 종료 상태 값을 따른다.
$ readlink -e asdfg; echo $?
1
$ AA=$( readlink -e asdfg ); echo $?    
1

$ readlink -e test.sh; echo $?
/home/mug896/tmp/test.sh
0
$ AA=$(readlink -e test.sh); echo $?
0
$ echo $AA
/home/mug896/tmp/test.sh

# 그러므로 if 문에서 사용할 수도 있다.
if AA=$( readlink -e test.sh ); then ...

if ! options=$(getopt -o a:f:c -- "$@"); then
    usage
    exit 1
fi

# 단 local, declare 와 함께 사용될 경우는 적용되지 않습니다.
$ AA=$( readlink -e asdfg ); echo $?
1
$ declare AA=$( readlink -e asdfg ); echo $?
0

# 이때는 먼저 local, declare 로 변수를 선언하면 됩니다.
$ declare AA
$ AA=$( readlink -e asdfg ); echo $?
1

Quiz

ftp 명령을 사용해 보면 작업 성공에 상관없이 항상 종료 상태 값으로 0 을 반환하는 것을 볼 수 있습니다. 이것은 ftp 작업 실패시 다음 실행할 명령을 컨트롤하는데 문제가 되는데요. 어떻게 하면 작업 실패를 구분할 수 있을까요?

$ ftp -in ftp.kaist.ac.kr <<\END 
user anonymous 1234
cd /ubuntu-cd/bionic
get MD5SUMS_XXX          # MD5SUMS_XXX 은 존재하지 않는 파일
bye
END

Failed to open file.

$ echo $?                # 작업에 실패하였지만  종료 상태 값으로 '0' 이 반환된다.
0

ftp 명령의 -v (verbose) 옵션을 사용하면 ftp 서버에 접속해서 실행하는 각각의 명령에 대해서 서버 응답을 받을 수 있습니다. 이 응답 메시지를 grep 명령을 이용해 작업 성공 여부를 판단할 수 있습니다.

# login 에 성공했을 때 
230 Login successful.

# cd 에 성공했을 때
250 Directory successfully changed.

# get file 에 성공했을 때
226 Transfer complete.
-----------------------------------

# grep 명령을 이용해 파일전송 성공 여부를 체크
# '|' 파이프로 명령이 연결될 경우 마지막 명령의 종료 상태 값이 사용된다.
$ ftp -v -in <<\END | grep -q '226 Transfer complete'
open ftp.kaist.ac.kr
user anonymous 1234
cd /ubuntu-cd/bionic
get MD5SUMS_XXX  
bye
END

$ echo $?    # 실패
1
.....................................................

$ ftp -v -in <<\END | grep -q '226 Transfer complete'
open ftp.kaist.ac.kr
user anonymous 1234
cd /ubuntu-cd/bionic
get MD5SUMS
bye
END

$ echo $?    # 성공
0

2 .

대입 연산시 ; 를 사용하지 않으면 이전 명령의 종료 상태 값을 설정할 수 있습니다.

$ test 1 = 2
$ echo $?
1

$ test 1 = 2
$ AA=100; exit=$?
$ echo $exit          # 이전 명령의 종료 상태 값을 설정할 수 없다.
0

$ test 1 = 2
$ AA=100 exit=$?      # ';' 를 사용하지 않으면 된다.
$ echo $exit
1