Shell에서의 Exit Status에 관하여

Shell에서의 Exit Status에 관하여
Page content

Exit Status에 대하여 알아보자.

개요

지금은 소프트웨어프로그램이라고 하면 사람들은 거의 그래픽 UI를 가지는 프로그램을 생각 할 것이다. 하지만 Linux와 같은 UNIX계열의 운영체제에서는 쉘(SHELL)에서 많은 일을 수행한다

리눅스에서 어떤 프로그램을 설치 하기위해 구글링 하면 거의 대부분은 쉘에서 패키기 관리자(apt, dnf, pacman 등)를 실행하여 프로그램을 설치 할 것이다.

복잡한 여러 명령을 반복적으로 수행해야 할때 쉘 스크립트를 작성하면 이러한 반복적인 작업을 매우 쉽고 빠르게 할 수 있다.

우리는 쉘에서 명령어를 실행 할때 해당 명령의 출력을 통해 프로그램이 정상적으로 실행되었는지를 판단 할 수 있다. 실행 도중 에러가 발생 하였다면 화면에 에러가 출력 될것이다.

전통적으로 UNIX 계열 프로그램은 ls와 같이 명시적으로 정보를 출력을 요구 하는 명령이 아닌 경우 정상적으로 동작 하였을 경우 아무런 출력이 없는 경우가 많다. 이는 침묵은 금이라는 유닉스의 설계 철학에 따르는 것이다.

Speech is silver, but silence is gold.

그렇다면 쉘에서 실행한 명령이 정상적으로 수행되었는지 어떻게 판단 할까?

우리가 일반적으로 사용하는 방법은 명령이 실행되면서 출력되는 출력 값을 경험적으로 알고 있기 때문에 이 출력값을 기반으로 명령이 성공적으로 수행되었는지 확인한다. 하지만 이 방법은 사람에게는 매우 효율적인 방법이지만 쉘스크립트를 짜는 프로그래머 입장에서는 매우 번거로운 방법이다.

더 일반적으로 사용할 수 있는 방법을 찾아 보자.

모든 명령은 종료될 때 waitpid 시스템 콜(이나 이와 동등항 함수)에서 Exit Status를 반환하는데 (명시적으로 지정하지 않을 경우 기본값(0)을 반환한다.) 프로그래머는 이 Exit Status를 가지고 자신의 실행한 명령이 정상적으로 실행 되었는지 판단 할 수 있다.

Exit Status

Exit Status는 O 에서 255 사이의 값을 가지고 쉘의 경우 125 이상의 값을 사용한다.

앞서 언급 했듯이 침묵은 금이기 때문에 많은 명령이 정상적으로 종료 되었다면 Exit Status0을 반환한다.

사실 완전히 같은 의미는 아니지만 침묵은 금이라는 UNIX의 철학을 어느정도 따른다고 볼수 있다.

쉔에 요청한 명령이 존재 하지 않는 경우 Exit Status 127을 반환한다. 명령이 존재 하지만 실행 가능하지 않은 경우 126을 반환한다. 명령이 SIGINTSIGSEGV와 같은 Fatal Signal $N$에 의해 종료 된 경우 Exit Status는 $128 + N$ 이 된다.

그렇다며 이 0 ~ 125 까지의 Exit Status는 어떻게 정의 되는가? 많은 프로그램들이 POSIX에 정의된 errno.h를 따른다.

errno.h에는 자주 발생하는 에러를 정의해 놓았다. 프로그램이 비정상 적으로 종료 될때 errno.h를 참고 하여 반환값(Exit Status)을 결정 하도록 하자.

CC++ 개열의 함수도 마찬가지다. 함수의 수행 상태를 정수값으로 반환하는 함수의 경우 반환값을 errno.h를 참고 하여 반환 하도록 하는 것이 좋다.

메모리 비교 함수인 memcmp와 문자열 비교함수인 strcmp 계열의 함수를 보자. C에서는 0 이 아닌 값이 참(TRUE) 이지만 위 함수들은 비교 대상이 일치 할때 0을 반환한다.

Shell Command(쉘 명령)

쉘 명령(Shell Command)명령(Command) 과 그에 따르는 공백 문자로 구분된 인자(Argument) 들로 구성된다.

사실 쉘에서 사용되는 상수, 문자열과 몇몇 예약어를 제외한 모든 특수 문자와 문자의 조합은 쉘에 내장된 명령또는 일발적은 명령이다.

심지어 test를 대신해서 쓰이는 [ 까지 Shell에 내장된 명령어이다.

아래 명령을 실행 해보자

which [

BASH의 경우 다음과 같은 출력을 얻을 수 있다.

/usr/bin/[

ZSH의 경우 다음과 같은 결과를 얻을 수 있다.

[: shell built-in command

errno

유닉스 계열 시스템을 사용한다면 errno 명령으로 확인 할 수 있다.

errno -l

아래는 위 명령의 출력중 일부를 나타낸 것이다.

EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call
EIO 5 Input/output error
ENXIO 6 No such device or address
E2BIG 7 Argument list too long
ENOEXEC 8 Exec format error
EBADF 9 Bad file descriptor
ECHILD 10 No child processes
EAGAIN 11 Resource temporarily unavailable
ENOMEM 12 Cannot allocate memory
.
.
.

errno 명령은 moreutils의 일부 이므로 이 명령이 없다면 해당 패키지를 설치 한다.

Debian 계열 (Debian, Ubunut, Mint…)

sudo apt install moreutils

Arch Linux 계열 (ArchLinux, Manjaro…)

sudo pacman -S moreutils

Redhat 계열 (Redhat, Fedora, Centos…)

sudo yum install moreutils
sudo dnf install moreutils

프로그래밍에서의 Exit Status, Return Code

아래와 같이 간단한 프로그램을 만든다고 가정해 보자. main() 함수에서 반환 하는 값이 Exit Status 가 되는데 앞서 설명 했듯이 0은 에러가 없는 상태를 나타내기 때문에 아래 예제에서는 0을 반환 하였다.

#include <stdio.h>

int main(int argc, char **argv)
{
    fprintf(stdout, "Hello!!");

    return 0;
}

하지만 프로그램 실행중 문제 가 발생하여 프로그램을 종료 해야 하는 경우 0을 반환 한하였다면 소스코드를 볼수 없는 프로그램 사용자들은 Exit Status0이기 때문에 프로램이 정상적으로 종료되었다고 착각 할 수 있다.

따라서 프로그램이나 쉘에서 수행되는 스크립트를 을 작성하는 경우 등으로 함수를 작성할때 반환되는 값을 POSIX에 정의된 errno를 참고하도록 하자.

리눅스에서 많이 사용되는 주요 프로그램들 대부분이 errno의 규칙을 따르지만 그렇지 않은 프로그램도 있다. 해당 프로그램의 매뉴얼을 읽어보자.(RTFM)

쉘에서의 Exit Status

쉘에서 실행한 명령의 종료 상태는 $? 변수에 저장된다.

아래 명령을 수행 해보자.

ls 
echo $?

화면에 0이 출력된다. ls 명령이 정상적으로 종료 된 것이다.

그렇다면 아래와 같이 ls 명령 다음에 존재 하지 않는 파일이나 디렉터리의 이름을 넣어 보자.

ls asdfasdfasdf
echo $?

화면에 2가 출력된다.

POSIX errno.h에는 errno 2ENOENT로 정의 되어 있으며 파일이나 디렉토리가 없을 경우 반환 하는 값니다.(No such file or directory)

Lists

리스트라는 개념이 있다. 우리가 알고 있는 리스트와는 조금 다르다. 아래는 BASH 메뉴얼에 있는 리스트에 대한 정의 이다.

A List is a sequence of one or more pipelines separated by one of the operators ;, &, &&, or ||‚ and optionally terminated by one of ;, &, or <newline>.

리스트는 ;, &, &&, 또는 || 연산자중 하나로 구분되고 선택적으로 ;, &, 또는 <newline> 으로 종료되는 하나 이상의 파이프라인 시퀀스다.

&& 연산자

Logical AND 연산과 같다.

command-one && command-two

command-one 이 참일 경우에만 command-two가 실행된다.

|| 연산자

Logical OR 와 같다.

command-one || command-two

command-one 이 거짓 일 경우에만 command-two가 실행된다.

&&|| 연산자를 활용하면 간단히 if-else문과 같은 효과를 볼 수 있다.

디렉터리가 존재 할 경우 디렉터리를 삭제

[ -d /home/test/aaa ] && rm -rf /home/test/aaa

디렉터리가 존재하지 않을 경우 디렉터리를 생성

[ -d /home/test/aaa ] || mkdir -p /home/test/aaa

파일이 존재 하고 실행 권한이 있는 경우 파일을 실행

[ -x /bin/errno ] && errno -l

파일이 존재 하지 않을 경우 파일 생성

[ -f /home/test/aaa ] || touch /home/test/aaa

&& 연산자 사용시 주의점

[ -d /home/test/aaa ] && rm -rf /home/test/aaa

위 명령은 [ -d /home/test/aaa ] 이 참일 경우 rm -rf /home/test/aaa 명령을 실행하라는 뜻이다.

/home/test/aaa 디렉터리가 존재 하지 않을 경우 [ -d /home/test/aaa ] 명령이 거짓이 되어 rm -rf /home/test/aaa 명령이 실행 되지 않게 된다. 그리고 이 List의 Exit Status[ -d /home/test/aaa ]의 결과인 1 이 된다.

Makefile 등에서 이 명령을 사용하였을 때 Exit 가 참이 아니기 때문에 다음 명령이 수행되지 않는다.

/home/test/aaa 가 디렉토리 일 경우 이 디렉토리를 삭제하고 빈 파일 /home/test/aaa를 생성하고자 아래와 같이 Makefile을 작성 했다면


all:
    [ -d /home/test/aaa ] && rm -rf /home/test/aaa
    touch /home/test/aaa

[ -d /home/test/aaa ] && rm -rf /home/test/aaa의 결과가 거짓이기 때문에 다음 명령인 touch /home/test/aaa 수행 되지 않는다.

명령 다음에 || 연산자와 true 명령을 추가 하자.

true 명령은 Exit Code 0를 반환한다.

Makefile


all:
    [ -d /home/test/aaa ] && rm -rf /home/test/aaa || true
    touch /home/test/aaa

References

https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html