Debugging

Syntax Check

-n | set -o noexec 옵션은 명령을 실행하지 않으므로 syntax errors 를 체크하는 용도로 사용할 수 있습니다. 단 syntax 만 체크합니다. 명령 이름에 typo 가 있는지 또는 명령 사용에 오류가 있는지는 각 명령에 해당하는 것이므로 체크되지 않습니다.

./test.sh: line 8: syntax error near unexpected token `fi'

Typo Check

-u | set -o nounset 옵션은 존재하지 않는 변수를 사용할 경우 에러로 간주하여 exit 합니다. 그러므로 typo check 용도로 사용할 수 있습니다. 다음의 경우 -u 옵션을 사용하지 않았다면 Workspace 디렉토리 전체가 삭제될 수 있습니다.

#!/bin/bash -u
...
TestDir=test2
...
rm -rf ~/Workspace/$Testdir           # d 가 소문자 
...
################## output ####################

./test.sh: line 15: Testdir: unbound variable

스크립트 실행 당시 어떤 변수가 존재할 수도 있고 아닐 수도 있을 경우 다음과 같은 매개변수 확장 기능을 이용하면 exit 되는 것을 방지할 수 있습니다.

# SOME_ENV_VAR 가 존재하지 않으면 null 을 리턴하게 되고 exit 되지 않습니다.

if [ "${SOME_ENV_VAR:-}" ]; then
    echo exist
else
    echo null or unset
fi

다음은 함수에 인수를 전달하지 않았을때 체크하는 방법과, 변수가 존재하지 않을때 대체값을 사용하는 방법입니다.

myfunc() 
{
    # 함수에 인수를 전달하지 않으면 $1 변수는 존재하지 않는 상태. 
    if [ -n "${1:-}" ]; then ...

    # flag 변수가 존재하지 않거나 null 값일 경우 1 을 사용
    res=$(( ${flag:-1} + 100 ))
}

Xtrace

Xtrace 의 가장 큰 장점은 매개변수확장, 명령치환, 산술확장이 완료된 명령문을 보여주고 실행한다는 점입니다. 그러므로 명령 실행시 변수값에 대한 정보를 알 수 있고, : (colon 명령) 을 이용하면 인수 부분에서 필요한 연산 결과를 얻어 낼수도 있습니다. -x | set -o xtrace 옵션으로 설정할 수 있으며 set -set +o xtrace 와 같은 의미로 스크립트 일부분을 trace 할때 간단히 사용할 수 있습니다..

다음은 xtrace 를 이용해 [ 명령의 'too many arguments' 오류를 trace 하는 예입니다.

### trace 에 사용된 스크립트 : xtrace.sh

#!/bin/bash

AA="foo bar"

if [ $AA = "foo bar" ]; then
    echo same !!!
else
    echo not same !!!
fi

### 실행결과 

$ ./xtrace.sh 
$ ./xtrace.sh: line 14: [: too many arguments
not same !!!

### xtrace 결과

$ bash -x ./xtrace.sh 
+ AA='foo bar'
+ '[' foo bar = 'foo bar' ']'                # AA 변수값이 foo bar 두개로 나온다.
./xtrace.sh: line 14: [: too many arguments
+ echo not same '!!!'
not same !!!

### 변수 $AA 를 "$AA" 로 quote 한 후 실행결과

$ bash -x ./xtrace.sh 
+ AA='foo bar'
+ '[' 'foo bar' = 'foo bar' ']'             # AA 변수값이 'foo bar' 하나로 나온다.
+ echo same '!!!'
same !!!                                    # 결과도 정상적으로 나옴.

xtrace 를 할때 변수이름 앞에 $ 문자가 없으면 값이 표시되지 않습니다. 이때 값을 보기 위해 echo 명령을 사용하게 되면 echo 명령도 함께 trace 가 돼서 보기가 안좋은데요. 이때 : 명령을 활용할 수 있습니다. : 도 명령이기 때문에 인수가 확장돼서 나온다는 점을 이용하는 것입니다.

#!/ bin/bash

set -x
for i in {25..28}; do
    (( i = i * 123 ))
    : i = $i, i / 2 = $(( i / 2 ))      # ':' 명령 실행 전에 인수확장이 됨
done
set - 

############## output ##############

+ for i in '{25..28}'
+ ((  i = i * 123  ))
+ : i = 3075, i / 2 = 1537
+ for i in '{25..28}'
+ ((  i = i * 123  ))
+ : i = 3198, i / 2 = 1599
+ for i in '{25..28}'
+ ((  i = i * 123  ))
+ : i = 3321, i / 2 = 1660
+ for i in '{25..28}'
+ ((  i = i * 123  ))
+ : i = 3444, i / 2 = 1722
+ set -

Xtrace 에서 stepping 을 하자

Xtrace 는 프로그래밍 언어에서 디버깅 할때처럼 break point 를 설정하거나 명령을 하나씩 stepping 할수가 없는데요. DEBUG trap 과 job control 을 이용하면 할 수 있습니다.

https://github.com/mug896/bash-stepping-xtrace

Trapping ERR

스크립트 실행시에 어떤 명령이 에러로 종료했다면 스크립트 실행을 중단해야 하지만 기본적으로 shell 은 중단없이 끝까지 진행을 합니다. 이러한 경우 스크립트 실행을 중지시키기 위해서 -e | set -o errexit 옵션을 사용할 수 있습니다. 이 옵션은 한가지 단점이 있는데 에러가 난 명령에서 오류 메시지를 출력하지 않으면 별다른 메시지가 표시되지 않는다는 점입니다. 이럴때 ERR trap 과 BASH_SOURCE, LINENO, BASH_COMMAND, $? 변수를 이용하면 에러가 발생한 명령의 위치와 종료 상태 값을 알 수 있습니다.

오류를 발생하는 명령으로 사용되는 스크립트 : error_command.sh

#!/bin/bash

echo --------- error_command.sh --------- start
exit 3
echo --------- error_command.sh --------- end

메인 스크립트 : trap_error.sh
-E | set -o errtrace 옵션은 function 과 subshell 에서도 ERR trap 을 가능하게 합니다.

#!/bin/bash -Ee
trap 'exitcode=$?; echo ERR!!! "${BASH_SOURCE[0]}": $LINENO: exit $exitcode: "$BASH_COMMAND"' ERR

echo --------- trap_err.sh --------- start
./error_command.sh
echo --------- trap_err.sh --------- end

실행결과

$ ./trap_err.sh
--------- trap_err.sh --------- start
--------- error_command.sh --------- start
ERR!!! ./trap_err.sh: 5: exit 3: ./error_command.sh

if, while, &&, || 에서는 ERR trap 이 되지 않는다.

오류가 나는 명령이 if, while, &&, || 에서 사용될 경우는 trap 이 되지 않습니다.

# &&, || 에서 사용되는 경우
./error_command.sh || true

# if 문에서 사용되는 경우
if ./error_command.sh; then
    :
else
    :
fi

이럴 경우 trap 을 사용하려면 다음과 같이 할 수 있습니다.

./error_command.sh 
[ $? -eq 0 ] && echo true

./error_command.sh
if [ $? -eq 0 ]; then
...

ERR trap 을 할때 문제점

산술연산 에서는 연산결과가 0 이면 false 이므로 종료 상태 값으로 1 을 리턴한다.

# let, (( )) 는 산술연산 표현식 

$ i=0                # i++ 는 postfix operator 로 연산결과는 0 이고
$ (( i++ ))          # 이후에 값이 증가하여 1 이 됩니다.
$ echo $?            # 그러므로 종료 상태 값으로 1 을 리턴
1
--------------

$ i=0
$ let i++            # 위와 마찬가지
$ echo $?
1
--------------

# expr 도 산술연산 명령으로 1 - 1 은 0 이므로 false 에 해당합니다.
# 그러므로 expr 명령의 종료 상태 값으로 1 을 리턴합니다. 
$ foo=$(expr 1 - 1)
$ echo $?
1

&& , || 에서는 ERR trap 이 되지 않지만 함수에서 마지막 명령으로 사용될 경우 trap 된다.

# 다음의 경우 test 명령이 실패했을때 && 메타문자로 인해 trap 이 되지 않는다.
test -d nosuchdir && ...

# 하지만 함수에서는 마지막 명령의 종료 상태 값을 리턴하므로 에러 trap 이 된다.
f1() { test -d nosuchdir && ... ;}

read 명령에서 -d 옵션 값으로 null 을 사용하거나 end-of-file 상태를 만났을때 에러를 리턴한다.