Debugging

여기서 소개되는 내용은 기본적으로 디버깅을 위한 기능들입니다. 프로그래머에 따라서 스크립트 작성시 -e ( errexit ), -u ( nounset ) 와 같은 옵션을 항상 사용하는 사람도 있지만 꼭 그렇게 해야 되는 것은 아닙니다.

오류 메시지 작성시 $LINENO 변수의 활용

오류 메시지 작성시 $LINENO 변수를 활용하면 오류가 발생한 위치를 알 수 있습니다.

#!/bin/bash

error () {
    echo "ERROR: line $1: exit $2: $3"
    exit $2
} >&2

echo start .....
# asdfgh 는 존재하지 않는 디렉토리
cd asdfgh 2> /dev/null || { error $LINENO $? "cd $_" ;}

echo end....
-------------------------------------------------

$ ./test.sh
start .....
ERROR: line 14: exit 1: cd asdfgh

Syntax Check

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

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

존재하지 않는 변수 사용시 스크립트 종료

-u | set -o nounset 옵션은 존재하지 않는 변수를 사용할 경우 에러로 간주하여 스크립트를 종료합니다. 스크립트 파일을 실행중일 경우 EXIT trap 이 설정되어 있다면 실행됩니다. 함수의 경우는 RETURN trap 이 설정되어 있어도 실행되지 않고 바로 종료됩니다. 종료상태 값으로는 둘 다 1 을 반환합니다.

다음의 경우 -u 옵션을 사용하지 않았다면 Workspace 디렉토리 전체가 삭제될 수 있습니다.

#!/bin/bash -u
...
TestDir=test2
...
rm -rf ~/Workspace/$Testdir           # d 가 소문자 
...
################## output ####################
                                                   # 존재하지 않는 변수를 사용하여
./test.sh: line 15: Testdir: unbound variable      # 실행이 종료된다.

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

# 매개변수 확장을 이용하면 exit 되지 않는다. ( [[  ]] 명령도 동일 )

if [ "${SOME_VAR-x}" = x ]; then
    echo SOME_VAR is unset
fi
---------------------------------------

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

다음은 nounset 옵션 사용에 따른 exit 을 방지하면서 함수에 인수를 전달하지 않았을때 체크하는 방법과, 변수값이 존재하지 않을때 대체값을 사용하는 방법입니다. 이것은 특히 bash-completion 패키지 같이 라이브러리로 사용되는 함수를 작성할때 다음과 같이 처리를 해주어야 해당 함수를 사용하는 스크립트가 종료되지 않습니다.

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

    if [ -z "${2-}" ]; then ...

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

    # CUR 변수가 존재하지 않거나 null 값일 경우 '*' 문자를 사용하게 되고 exit 되지 않습니다.
    case ${CUR:-*} in
        --*) command ... ;;
          *) default command ... ;;
    esac    
}

오류 발생시 스크립트 종료

스크립트 실행 중에 앞선 명령이 오류로 종료하였을 경우 다음 이어지는 명령들은 실행돼서는 안되겠죠. 그렇다고 모든 명령을 && 연산자로 연결할 수는 없는데 이때 사용할 수 있는 옵션이 -e | set -o errexit 입니다. 이 옵션을 사용하면 스크립트 실행 중에 어떤 명령이 오류로 종료하였을 경우 자동으로 스크립트가 종료됩니다.

오류가 나는 명령이 &&, ||, if, while 와 같은 분기 문의 condition 에서 사용되는 경우는 예외로 종료되지 않습니다.

#!/bin/bash -e      # errexit 옵션 설정

echo start ...
cd xxx              # 'xxx' 는 존재하지 않는 디렉토리
echo end ...

$ ./test.sh
start ...           # cd 명령에서 오류가 발생하여 스크립트 실행이 종료된다.
./test.sh: line 4: cd: xxx: No such file or directory

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

# subshell 에서도 동일하게 적용됩니다.
#!/bin/bash -e

echo start ...
( echo 111; cd xxx; echo 222 )         # cd 명령에서 실행이 중단되고
echo end ...

$ ./test.sh 
start ...
111                                    # 스크립트가 종료된다.
./test.sh: line 4: cd: xxx: No such file or directory

분기문의 condition 에서 오류가 발생하는 경우는 예외로 종료되지 않습니다.

# if 문에서 사용되는 경우 스크립트는 exit 되지 않는다.
if error_command ...; then
    :
else
    :
fi

# 1. command1 이 오류로 종료해도 스크립트는 exit 되지 않습니다.
# 2. command1 는 정상 종료, command2 는 오류로 종료할 경우는 exit 됩니다.
command1 ... && command2 ...

# 1. command1 는 오류 종료, command2 는 정상 종료할 경우 exit 되지 않습니다.
# 2. command1, command2 둘 다 오류로 종료하게 되면 exit 됩니다.
command1 ... || command2 ...

위에서 주의할 점은 && 연산자인데요. 예를 들어 false && true 의 경우 && 연산자에 의해 exit 되지 않습니다. 아래 오른쪽과 같이 마지막 명령이 오류로 종료되어야 exit 됩니다. 따라서 만약에 왼쪽과 같은 경우에도 종료되기를 원하면 마지막에 || false 를 추가해 주어야 합니다.

# 종료되지 않는 경우                                # 종료되는 경우
false && true                                   true && false
false && false

# 다음과 같이 처리를 해주면 스크립트가 exit 됩니다.
false && true  || false
false && false || false

반대로 특정 명령이 오류로 종료할 경우 스크립트가 종료되지 않게 하려면 다음과 같이 하면 됩니다. 이것은 특히 shell 함수를 작성할때 다음과 같이 처리를 해주어야 해당 함수를 사용하는 스크립트가 종료되지 않습니다.

error_command ... || true

파이프 subshell 의 경우

# 파이프 subshell 의 경우는 test 1 = 2 명령에서 실행이 중단되기는 하지만
# 파이프 전체 명령의 종료 상태 값은 '0' 이 되므로 스크립트가 종료되지는 않습니다.
#!/bin/bash -e

echo start ...
{ echo 111; test 1 = 2; echo 222 ;} | cat
echo end ...

$ ./test.sh 
start ...
111
end ...           # 스크립트가 종료되지는 않는다.

# 이때는 pipefail 옵션을 사용하면 파이프 전체 명령이 오류가 되어 스크립트가 종료됩니다.
#!/bin/bash -e

set -o pipefail

echo start ...
{ echo 111; test 1 = 2; echo 222 ;} | cat
echo end ...

$ ./test.sh 
start ...
111              # 스크립트가 종료된다.

기본적으로 명령 치환 subshell 에는 적용되지 않습니다.

#!/bin/bash -e

echo start ...
: $( test 1 = 2 )                                   # 마지막 echo 222 가 정상 종료되어
AA=$( echo 111 >&2; test 1 = 2; echo 222 >&2 )      # 대입 연산도 오류가 되지 않는다.
echo end ... 

$ ./test.sh
start ...
111
222
end ...

# 다음과 같은 경우는 대입 연산의 종료 상태 값이 오류가 되므로 스크립트가 종료됩니다.
#!/bin/bash -e

echo start ...
AA=$( test 1 = 2 )
echo end ...

$ ./test.sh 
start ...

redirection 이 키워드에 연결되어 사용될 경우

redirection 이 { ;} 나 compound commands 의 키워드에 연결되어 사용될 경우, redirection 처리에서 오류가 발생해도 종료되지 않습니다. 따라서 이때는 뒤에 || exit 를 붙여주어야 합니다.

이것은 sh 에서도 동일하게 동작합니다. 그런데 다시 생각해 보아도 이것은 종료되는 것이 맞는것 같아서 버그 리포트를 보냈는데 bash 5.1 버전에서 수정된다고 합니다.

they're artifacts from when the standard said `set -e' worked only on simple commands.
This will be fixed in the next devel branch push.

#!/bin/bash -e                   # errexit 옵션 설정

cat < not_exist.txt              # 존재하지 않는 파일

echo end ....

######  실행 결과  ######

$ ./test.sh         # errexit 옵션 설정에 따라 정상적으로 스크립트가 종료된다.
./test.sh: line 3: not_exist.txt: No such file or directory
...........................................................

#!/bin/bash -e

{ cat ;} < not_exist.txt    # redirection 이 { ;} 키워드에 연결된 경우

echo "exitcode : $?"
echo end ....

######  실행 결과  ######

$ ./test.sh         
exitcode : 1        # errexit 옵션을 설정하고 종료 상태 값도 1 이 됐지만
./test.sh: line 3: not_exist.txt: No such file or directory
end ....            # 스크립트가 종료되지 않고 끝까지 실행된다.
                    # 따라서 { cat ;} < not_exist.txt || exit
                    # 또는   ( cat ) < not_exist.txt 형태로 변경 해야합니다.
...........................................................

#!/bin/bash -e           

while read line; do 
    echo "$line"
done < not_exist.txt     # redirection 이 done 키워드에 연결된 경우

echo "exitcode : $?"
echo end.....

######  실행 결과  ######

$ ./test.sh
./test.sh: line 5: not_exist.txt: No such file or directory
exitcode : 1      # while 문에 진입하기 전에 redirection 처리에서 오류가 나는 상태로
end.....          # 스크립트가 종료되지 않고 끝까지 실행된다.
                  # 따라서 done < not_exist.txt || exit 로 변경해야 합니다.

errexit 옵션 사용시 주의할 점

산술연산 에서는 연산결과가 0 이면 false 이므로 종료 상태 값으로 1 을 리턴하게 되어 스크립트가 exit 됩니다.

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

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

$ i=0
$ let i++            # 위와 마찬가지. 따라서 let ++i 을 사용해야 합니다.
$ echo $?
1
--------------

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

다음 첫 번째 경우는 error_command 가 오류로 종료해도 && 연산자에 의해 스크립트는 exit 되지 않지만 두 번째와 같이 함수의 마지막 명령으로 실행이 된다면 foo 명령의 종료 상태 값은 오류가 되므로 exit 됩니다.

error_command ... && command2 ...                        # 첫번째

foo() { ...; error_command ... && command2 ... ;}        # 두번째

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

#!/bin/bash -e                                 #!/bin/bash -e

echo start...                                  echo start...
if test 1 = 1; then                            foo() {
    test 1 = 3 && echo yes                         if test 1 = 1; then
fi                                                     test 1 = 3 && echo yes
echo "exit status :" $?                            fi
echo end...                                    }
                                               foo    # foo 함수실행
-------------------------                      echo "exit status :" $?
                                               echo end...
$ ./test.sh
start...                                       ---------------------------
exit status : 1
end...          종료되지 않고                     $ ./test.sh
                끝까지 실행된다.                   start...        # 종료된다.

다음과 같이 read 명령이 사용될 경우 에러를 리턴한다.

$ read -r -d '' whole < somefile.txt        # -d '' 사용 
$ echo $?
1

$ read -r name      # 입력대기 상태에서 ctrl-d 입력
$ echo $?
1

ERR trap

errexit 옵션의 경우 오류가 발생하면 바로 스크립트가 종료돼버리지만 ERR trap 을 사용하면 필요한 뒤처리 작업을 설정하거나 오류와 관련해서 메시지를 출력할 수 있습니다. BASH_SOURCE, LINENO, BASH_COMMAND 변수를 활용하면 오류가 발생한 명령과 위치를 알 수 있습니다.

ERR trap 은 sh 에서는 사용할 수 없습니다.

#!/bin/bash -e

func1() {
    echo 222; test 1 = 2
}

echo start ....
echo 111; func1 
echo end....
-------------------------

$ ./test.sh
start ....
111
222            # 스크립트가 오류로 종료하였지만 아무런 메시지도 출력되지 않는다.

$ echo $?
1

다음은 ERR trap 을 설정합니다.

#!/bin/bash
# $LINENO 값이 올바르게 표시되려면 trap 첫 라인에 위치해야 함
trap 'exitcode=$? lineno=$LINENO
echo "ERROR: $BASH_SOURCE: $lineno: exit $exitcode: $BASH_COMMAND"
exit $exitcode
' ERR

func1() {
    echo 222; test 1 = 2
}

echo start ....
echo 111; func1 
echo end....
-------------------------

$ ./test.sh
start ....
111
222     # test.sh 스크립트 13 번라인 'test 1 = 2' 명령에서 오류가 발생했고 exit 값은 1 
ERROR: ./test.sh: 13: exit 1: test 1 = 2

$ echo $?
1

-E | set -o errtrace 옵션을 사용하면 subshell, 명령치환, shell function 에서도 ERR trap 이 됩니다. 따라서 ERR trap 메시지가 프로세스 별로 출력되는 것을 볼 수 있습니다

#!/bin/bash -E

trap 'exitcode=$? lineno=$LINENO
echo "ERROR: $BASH_SOURCE: $lineno: exit $exitcode: $BASH_COMMAND"
exit $exitcode
' ERR

func1() {
    ( echo 222; test 1 = 2 )   # subshell
}

echo start ....
( echo 111; func1 )   # subshell
echo end....
------------------------------

./test.sh
start ....
111
222
ERROR: ./test.sh: 9: exit 1: test 1 = 2      # ERR trap 메시지가 프로세스 별로 출력된다.
ERROR: ./test.sh: 9: exit 1: ( echo 222; test 1 = 2 )
ERROR: ./test.sh: 13: exit 1: ( echo 111; func1 )

명령 치환은 메인 명령 실행전에 명령문을 만드는데 사용되는 것으로 명령 치환에서 발생하는 오류는 오류로 취급되지 않습니다. 하지만 대입 연산에서 사용될 경우는 오류로 취급됩니다.

#!/bin/bash -E
trap 'exitcode=$? lineno=$LINENO
echo "ERROR: $BASH_SOURCE: $lineno: exit $exitcode: $BASH_COMMAND"
exit $exitcode
' ERR

echo start ....
echo $( test 1 = 2 )      # 명령치환
echo end....
--------------------------

$ ./test.sh 
start ....
ERROR: ./test.sh: 8: exit 1: test 1 = 2
end....       # 스크립트가 종료되지는 않는다.

$ echo $?
0

##############  대입 연산 #################

#!/bin/bash -E
trap 'exitcode=$? lineno=$LINENO
echo "ERROR: $BASH_SOURCE: $lineno: exit $exitcode: $BASH_COMMAND"
exit $exitcode
' ERR

echo start ....
AA=$( test 1 = 2 )      # 오류가 발생하는 명령치환이 대입연산에 사용될 경우
echo end....
------------------------------------------------------------

$ ./test.sh 
start ....
ERROR: ./test.sh: 8: exit 1: AA=$( test 1 = 2 )   # 스크립트가 종료된다.

$ echo $?
1

만약에 명령이 분기문에서 사용되어 오류로 종료하였음에도 불구하고 trap 이 되지 않을 경우는 다음과 같이 명령을 분리해서 작성하면 됩니다.

error_command ... && command ...      # ERR trap 이 되지 않는다.

error_command ...
if [ $? = 0 ]; then ...

DEBUG trap

DEBUG trap 은 명령이 실행되기 전에 발생합니다.

#!/bin/bash

trap 'echo ----------$((++i))' DEBUG

echo 111
echo 222
echo 333

#####  실행 결과  #####

$ ./test.sh 
----------1
111
----------2
222
----------3
333

-T | functrace 옵션은 subshell, 명령치환, shell function 에서도 DEBUG trap 을 가능하게 합니다.

#!/bin/bash                                  #!/bin/bash -T

trap 'echo ----------$((++i))' DEBUG         trap 'echo ----------$((++i))' DEBUG

echo 111                                     echo 111
( echo 222-1; echo 222-2; echo 222-3 )       ( echo 222-1; echo 222-2; echo 222-3 )
echo 333                                     echo 333

######  실행 결과  ######                      ######  실행 결과  ######

$ ./test.sh                                  $ ./test.sh 
----------1                                  ----------1
111                                          111
222-1                                        ----------2
222-2                                        222-1
222-3                                        ----------3
----------2                                  222-2
333                                          ----------4
                                             222-3
                                             ----------5
                                             333

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

#!/bin/bash                                  #!/bin/bash -T

trap 'echo ----------$((++i))' DEBUG         trap 'echo ----------$((++i))' DEBUG

func() { echo 333 ;}                         func() { echo 333 ;}

echo 111                                     echo 111
echo 222                                     echo 222
func                                         func
echo 444                                     echo 444

######  실행 결과  ######                      ######  실행 결과  ######

$ ./test.sh                                  $ ./test.sh
----------1                                  ----------1
111                                          111
----------2                                  ----------2
222                                          222
----------3                                  ----------3
333                                          ----------4
----------4                                  ----------5
444                                          333
                                             ----------6
                                             444

shopt -s extdebug 옵션을 사용하면 다음과 같은 확장 기능을 사용할 수 있습니다.

  • DEBUG trap 함수가 0 이 아닌 값을 리턴하면 다음에 오는 명령을 스킵하고 실행하지 않습니다.
  • DEBUG trap 함수에서 2 를 리턴하면 현재 실행 중인 함수나 source 한 스크립트에서 리턴합니다.
  • declare -F 함수이름 을 사용하면 해당 함수가 위치한 파일 이름과 라인 번호를 알려줍니다.
  • BASH_ARGCBASH_ARGV 값을 사용할 수 있습니다.
#!/bin/bash

shopt -s extdebug 

debug_trap() {
    echo DEBUG MESSAGE
    return 0         # return 값을 0, 1 로 바꾸어가며 테스팅
}

trap 'debug_trap' DEBUG

echo HELLO 1
echo HELLO 2
echo HELLO 3

##################  return 0 실행  #################
DEBUG MESSAGE
HELLO 1       # 정상적으로 debug message 와함께 다음 명령이 실행됨
DEBUG MESSAGE
HELLO 2
DEBUG MESSAGE
HELLO 3

##################  return 1 실행  #################
DEBUG MESSAGE
DEBUG MESSAGE  # 다음에 오는 명령 실행이 스킵됨
DEBUG MESSAGE

RETURN trap

RETURN trap 은 기본적으로 source 한 파일에서 return 할 때 발생합니다.

$ cat test2.sh                            $ cat test3.sh 

echo "test2.sh : 222"                     echo "test3.sh : 333"

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

#!/bin/bash

trap 'echo "---------------- RETURN trap"' RETURN

func4 () { echo "func4 : 444" ;}
func5 () { echo "func5 : 555" ;}    

echo start

source test2.sh
source test3.sh
func4
func5

echo end

###########  실행 결과  ###########

$ ./test.sh 
start
test2.sh : 222
---------------- RETURN trap
test3.sh : 333
---------------- RETURN trap
func4 : 444                       # 함수가 return 할 때는 발생하지 않는다.
func5 : 555
end

함수가 return 할 때도 발생하려면 함수 이름에 trace 속성을 설정해 주면 됩니다.

#!/bin/bash

trap 'echo "---------------- RETURN trap"' RETURN

func4 () { echo "func4 : 444" ;}
func5 () { echo "func5 : 555" ;}    

declare -t -f func4 func5         # 함수에 trace 속성을 설정.

echo start

func4
func5

echo end

###########  실행 결과  ###########

$ ./test.sh 
start
func4 : 444
---------------- RETURN trap
func5 : 555
---------------- RETURN trap
end

다음과 같이 각 함수 내에서 직접 설정해 줄 수도 있습니다. 이 방법은 subshell 이나 명령 치환에서도 동작합니다. RETURN trap 에 의해 함수가 종료될 때는 함수에서 마지막으로 실행된 명령의 종료 상태 값이 자동으로 설정됩니다.

#!/bin/bash

func4 () { 
    trap 'echo "---------------- RETURN trap 1"' RETURN
    echo "func4 : 444" 
}
func5 () { 
    trap 'echo "---------------- RETURN trap 2"' RETURN
    echo "func5 : 555" 
}

echo start

func4
func5

echo end

###########  실행 결과  ###########

$ ./test.sh 
start
func4 : 444
---------------- RETURN trap 1
func5 : 555
---------------- RETURN trap 2
end

-T | functrace 옵션을 사용하면 source 한 파일뿐만 아니라 함수에서도 모두 RETURN trap 이 발생합니다. 또한 RETURN trap 을 함수내에 위치시킨 경우 해당 함수에서 호출하는 sub 함수들에서도 모두 trap 이 발생합니다 ( 이것은 extdebug 옵션 설정시도 마찬가지 ).

#!/bin/bash -T

trap 'echo "---------------- RETURN trap"' RETURN

func4 () { echo "func4 : 444" ;}
func5 () { echo "func5 : 555" ;}    

echo start

source test2.sh
source test3.sh
func4
func5

echo end

###########  실행 결과  ###########

$ ./test.sh 
start
test2.sh : 222
---------------- RETURN trap
test3.sh : 333
---------------- RETURN trap
func4 : 444
---------------- RETURN trap
func5 : 555
---------------- RETURN trap
end

xtrace

xtrace 의 가장 큰 장점은 매개변수확장, 명령치환, 산술확장이 완료된 명령문을 보여주고 실행한다는 점입니다. 그러므로 명령 실행시 변수값에 대한 정보를 알 수 있고, : 명령을 활용하면 인수 부분에서 필요한 연산 결과를 얻어 낼수도 있습니다.

set -x | set -o xtrace 옵션으로 설정하고 set - | set +o xtrace 으로 해제합니다.

다음은 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 !!!                                    # 결과도 same !!! 으로 나옴.

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

#!/bin/bash

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

############## 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

Quiz

다음 스크립트는 set -o errexit 옵션을 사용하고 있는데요. 그런데 실행을 해보면 정상적으로 동작하지 않고 계속 오류 종료 상태 값을 반환하면서 스크립트가 종료됩니다. 스크립트가 정상적으로 동작하게 하려면 어떻게 수정해야 될까요?

$ cat -n test.sh
     1  #!/bin/bash
     2
     3  set -o errexit
     4
     5  declare cfoo cbar
     6
     7  count_line() {
     8      ! test -t 0 && {
     9          while read -r line; do
    10              if [[ $line =~ foo ]]; then
    11                  let cfoo++
    12              else
    13                  [[ $line =~ bar ]] && let cbar++
    14              fi
    15          done
    16      }
    17  }
    18
    19  count_line
    20
    21  count_line <<\@
    22  111111 foo 111111
    23  22 bar 2222222222
    24  3333333 bar 33333
    25  44444444444444444
    26  @
    27
    28  echo "foo count: $cfoo"
    29  echo "bar count: $cbar"

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

1. 먼저 cfoo++, cbar++  에서 postfix 연산자가 사용되고 있습니다.
   위의 경우 제일 처음 실행시 연산 결과값이 0 이 되므로 종료 상태 값은 오류가 되어 종료하게 됩니다.
   따라서 ++cfoo, ++cbar 로 변경해야 합니다. 

2. 함수에서는 제일 마지막에 실행된 명령의 종료 상태 값이 함수의 종료 상태 값이 됩니다.
   8번 라인을 보면 '! test -t 0' 에서 '&&' 연산자가 사용되었으므로 19번 라인에서처럼
   입력 없이 함수가 실행될 경우 함수의 종료 상태 값은 오류가 되어 종료하게 됩니다.
   따라서 'if ! test -t 0; then ...' 로 변경해야 합니다.

3. while 문의 else 부분인 '[[ $line =~ bar ]] && ...' 에서도 '&&' 연산자가 사용되었습니다.
   count_line 함수에 입력된 4444444444444 라인이 중간에 위치하면 문제가 없지만 
   위와 같이 마지막에 위치하게 될경우 함수 종료전 마지막 실행문이 오류가 되어 결과적으로
   count_line 함수의 종료 상태 값도 오류가 됩니다.
   따라서 'if [[ $line =~ bar ]]; then ...' 로 변경해야 합니다.

다음은 최종 수정본입니다. 정상적으로 실행이 되는것을 볼 수 있습니다.

#!/bin/bash

set -o errexit

declare cfoo cbar

count_line() {
    if ! test -t 0; then
        while read -r line; do
            if [[ $line =~ foo ]]; then
                let ++cfoo
            else
                if [[ $line =~ bar ]]; then let ++cbar; fi
            fi
        done
    fi
}

count_line

count_line <<\@
111111 foo 111111
22 bar 2222222222
3333333 bar 33333
44444444444444444
@

echo "foo count: $cfoo"
echo "bar count: $cbar"

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

$ ./test.sh
foo count: 1
bar count: 2