Test

Shell 에는 기본적으로 숫자와 스트링을 구분하는 데이터 타입이 없고 사칙연산을 위한 연산자도 없습니다. 명령문 상의 모든 문자는 스트링이며 산술연산은 별도의 확장 이나 명령 을 통해서 제공됩니다.

기본적으로 명령문에 사용되는 문자는 모두 스트링이다.

# 명령행의 인수를 스트링으로 받는 c 프로그램의 main 함수 
int main(int argc, char **argv)

# 명령행의 인수를 스트링으로 받는 java 프로그램의 main 함수
public static void main(String[] args)

32 는 숫자인가 스트링인가? "32" 는 스트링인가 숫자인가? 일반적으로 프로그래밍 언어에서는 숫자와 스트링의 구분이 명확하여 혼용할 경우 오류가 발생합니다. 하지만 shell 에서는 데이터 타입이 존재하지 않기 때문에 32 , "32" 는 둘 다 같은 값입니다. 스트링을 다루는 곳에서 사용되면 스트링으로 사용되고 산술연산을 하는 곳에서 사용되면 숫자로 사용되는 식입니다.

# 숫자가 들어갈 자리에 "16" 을 스트링이 들어갈 자리에 16 을 넣어도된다
$ printf "decimal: %d, string: %s\n" "16" 16
decimal: 16, string: 16

# '+' 는 expr 명령에 전달되는 인수로 그냥 문자
$ expr "-16" + 10
-6

# '*' 는 glob 문자이므로 escape
$ echo "10" + 2 \* 5 | bc
20

# '-gt' 는 숫자를 다루는 연산자인데 "150" 과 비교해도 된다.
$ [ "150" -gt 25 ]; echo $?
0

# 두개의 피연산자 모두 quote 해도 상관없다.
$ [ "150" -gt "25" ]; echo $?
0

$ AA=123
$ case $AA in (123) echo Y ;; esac
Y
$ case $AA in ("123") echo Y ;; esac
Y

데이터에 타입이 없기 때문에 shell script 의 특징 중에 하나가 두 종류의 연산자를 제공한다는 것입니다. 하나는 숫자로 취급할때 사용되는 연산자 또 하나는 스트링으로 취급할때 사용되는 연산자입니다. 아래는 두값을 비교할때 사용되는 test 명령의 help 내용중 일부인데 두 종류의 연산자를 제공하는 것을 볼수있습니다.

    String operators:  # 스트링 으로 취급할때 사용

      -z STRING      True if string is empty.

      -n STRING
         STRING      True if string is not empty.

      STRING1 = STRING2
                     True if the strings are equal.
      STRING1 != STRING2
                     True if the strings are not equal.
      STRING1 < STRING2
                     True if STRING1 sorts before STRING2 lexicographically.
      STRING1 > STRING2
                     True if STRING1 sorts after STRING2 lexicographically.


      # 숫자로 취급할때 사용
      arg1 OP arg2   Arithmetic tests.  OP is one of -eq, -ne,
                     -lt, -le, -gt, or -ge.

[     ] , test

스크립트를 작성할때 거의 빠짐없이 등장하는 표현식이 테스트 표현식인데요. 이때 사용하는 명령이 test, [ 입니다. test[ 는 동일한 명령입니다. [ 은 키워드같이 생겼지만 명령으로 사용법도 command arg1 arg2 ... 처럼 일반 명령과 같습니다. 따라서 위의 help 문서중에 나오는 연산자들은 [ 명령의 인수이고 마지막에 붙이는 ] 도 인수가 됩니다.

# test 와 '[' 는 같은 명령이다.

$ test -d /home/user/foo; echo $?
1

$ [ -d /home/user/foo ]; echo $?
1

여담으로 프로그래머 중에는 [ ] 형식을 안 쓰고 test 명령만 사용하는 사람도 있습니다.

null 이 아닌 값은 모두 true

[ 명령에서 연산자를 사용하지 않을 경우 존재하지 않거나 null 값인 경우는 false 그외는 모두 true 에 해당합니다.

########## 거짓인 경우 ##########

$ [  ]; echo $?
1
$ [ "" ]; echo $?
1
$ [ $asdfgh ]; echo $?     # 존재하지 않는 변수
1

$ AA=""
$ [ "$AA" ]; echo $?       # null 값 변수
1

####### null 이 아닌 값은 모두 true 이다 #######

$ [ 0 ]; echo $?
0
$ [ 1 ]; echo $?
0
$ [ a ]; echo $?
0
$ [ -n ]; echo $?
0

$ AA=" "
$ [ "$AA" ]; echo $?
0

false 값이 0 ?

다음 예에서는 true 와 false 모두 테스트 결과가 0 이 나오고 있습니다. 프로그래밍 언어에서 true 와 false 는 키워드나 예약어로 자체 값을 갖지만 shell 에서는 builtin 명령입니다. 그래서 실행이 돼야 true 는 0 을 반환하고 false 는 1 을 반환합니다. 그런데 아래서는 [ 명령의 인수로 사용되어 "false" 스트링과 같은 의미가 되었습니다.[ 에서 null 이 아닌 스트링은 항상 true 입니다.

역사적으로 sh 에는 true, false 명령이 없었고 true 는 :( colon 명령 ) 의 alias 였다고 합니다.

$ [ true ]; echo $?
0
$ [ false ]; echo $?
0

# if 문을 사용하면 true, false 명령이 실행됩니다.
$ isSet=false
$ if $isSet; then echo true; else echo false; fi
false

$ isSet=true
$ if $isSet; then echo true; else echo false; fi
true

비교하는 변수는 quote 해야합니다.

[ 명령은 인수로 사용하는 변수를 quote 하지 않으면 정상적인 결과가 나오지 않을 수 있습니다.

아래 첫번째 예는 값이 없는 변수 $AA 를 사용할때 quote 하지 않아서 결과적으로 [ -n $AA ] 명령은 [ -n ] 와 같게 되었습니다. 여기서 -n 는 스트링 "-n" 으로 해석되니까 항상 true 가 됩니다. 두번째는 quote 하였기 때문에 명령이 [ -n "" ] 와 같게 되어 정상적인 값이 나오게 됩니다.

$ AA=""                  # 변수 AA 값은 null

$ [ -n $AA ]; echo $?    # null 이 아니여야 true 인데 결과로 true 가 나왔습니다.
0    

$ [ -n "$AA" ]; echo $?  # 변수를 quote 해주니 정상정인 값이 나왔습니다.
1

다음 예에서는 변수 AA 값이 null 이 아니기 때문에 echo 명령이 실행돼야 하지만 오류가 발생하였습니다. -n 연산자는 하나의 피연산자를 갖는데 $AA 를 quote 하지 않아서 결과적으로 [ -n aa bb ] 가되어 두개의 피연산자를 갖게 되었습니다.

$ AA="aa bb"

$ if [ -n $AA ]; then
    echo "$AA"
fi
bash: [: too many arguments

# 다음 예도 마찬가지입니다.
$ AA="aa bb" BB="aa bb"

$ [ $AA = $BB ]; echo $?
bash: [: too many arguments

$ [ "$AA" = "$BB" ]; echo $?
0

위의 예를 통해서 알 수 있듯이 [ 명령을 사용할 때는 항상 변수를 quote 해줘야 합니다.

숫자를 비교할때 스트링 연산자를 사용하면 안된다.

< , > 연산자는 보통 프로그래밍 언어에서는 숫자를 비교할때 사용하지만 [ ] , [[ ]] 에서는 그와 달리 스트링을 비교하는데 사용합니다. 위의 help 문서중에 나왔듯이 스트링을 비교할 때는 사전적으로 (lexicographically) 비교를 합니다. 그러므로 "100" 보다는 "2" 가 큰수가 됩니다. 왜냐하면 사전적으로 볼 때 처음 비교되는 문자가 "1" 보다 "2" 가 크기 때문입니다.

$ [ 100 \> 2 ]; echo $?
1

$ [ 100 -gt 2 ]; echo $?
0

AND , OR

[ 명령에서는 and, or 연산자로 -a , -o 를 제공하지만, && || shell 메타문자를 이용해 분리해서 작성할 수도 있습니다. 두 연산자의 우선순위는 [ 명령에서 제공하는 연산자일 경우 일반 프로그래밍 언어와 같이 -a 가 높고, shell 메타문자를 이용할 경우는 두 메타문자의 우선순위를 같게 취급하므로 주의할 필요가 있습니다. ( 자세한 내용은 shell metacharacters precedence 참조 ).

# shell 메타문자를 이용해 분리해 사용

if [ A = A ] && [ B = B ]; then ...

if [ A = A ] || [ B = B ]; then ...

# { ;} 를 이용해 우선순위 조절
if [ A = A ] || { [ B = B ] && [ C = C ] ;} then ...

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

if [ A = A -a B = B ]; then ...

if [ A = A -o B = B ]; then ...

# 우선순위 조절을 위해 `( )` 를 사용할때는 shell 메타문자와 충돌하므로 escape 합니다.
if [ A = A -a \( B = B -o C = C \) ]; then ...

Logical NOT

다음과 같이 두가지 형태로 사용할 수 있습니다.

# test 명령에서 사용되는 '!'                  # shell logical NOT 키워드를 이용
if test ! A = A; then ...                 if ! test A = A; then ...

if [ ! A = A ]; then ...                  if ! [ A = A ]; then ...

Array 를 비교할때는 * 을 사용

Quote 을 하지 않을경우 ${array[@]}${array[*]} 는 차이가 없습니다. 어짜피 똑같이 IFS 값에 의해 단어 분리가 되기 때문입니다. 하지만 " " 하였을 경우는 @* 의미가 틀려집니다. @ 은 개개의 원소를 " " 하여 나열하는 것과 같고 * 은 모든 원소를 하나의 " " 안에 넣는 것과 같습니다. 그러므로 array 를 비교할 때는 * 을 사용해야 합니다.

$ AA=(11 22 33)
$ BB=(11 22 33)

$ [ "${AA[*]}" = "${BB[*]}" ]; echo $?       # [ "11 22 33" = "11 22 33" ]
0

$ [ "${AA[@]}" = "${BB[@]}" ]; echo $?       # [ "11" "22" "33" = "11" "22" "33" ]
bash: [: too many arguments

[[     ]]

이것은 생긴모양에서 알수있듯이 [ ] 의 기능확장 버전입니다. [ ] 와 가장 큰 차이점은 [ ] 은 명령이고 [[ ]] 은 shell keyword 라는 점입니다. 키워드이기 때문에 일반 명령들과 달리 shell 에서 자체적으로 해석을 해서 실행하기 때문에 [ 처럼 명령이라서 생기는 여러가지 제약사항 없이 편리하게 사용할 수 있습니다.
( 좀 더 자세한 내용은 Special Commands 참조 ).

# '<' , '>' 연산자를 escape 하지 않아도 되는 것을 알 수 있습니다.
$ [[ a  <  b ]]; echo $?
0
$ [[ a  >  b ]]; echo $?
1

# 사용되는 변수를 quote 하지 않아도 값을 올바르게 인식한다.
$ AA=""
$ [[ -n $AA ]]; echo $?
1
$ [[ -n "$AA" ]]; echo $?
1

Quiz

test 명령에서 값을 비교할 때 8 진수, 16 진수를 사용하려면 어떻게 할까요?

$(( ... )) 산술확장 을 사용하면 됩니다.

$ echo $(( 0xff )) $(( 16#ff ))         # 16 진수
255 255

$ echo $(( 0377 )) $(( 8#377 ))         # 8 진수
255 255

$ AA=0255; echo $(( 10#$AA ))           # 10 진수 ( 0 이 제거된다 )
255

$ test 255 -eq $(( 0xff )) && echo yes
yes

$ test 255 -eq $(( 0377 )) && echo yes
yes

$ test $(( 0377 )) -eq $(( 0xff )) && echo yes
yes
-----------------------------------------------------------

$ readelf -SW /bin/date | grep interp
  [ 1] .interp        PROGBITS      00000000000002a8 0002a8 00001c 00   A  0   0  1

$ dd skip=$((0x2a8)) bs=1 count=$((0x1c)) < /bin/date 2> /dev/null
/lib64/ld-linux-x86-64.so.2
-----------------------------------------------------------

$ readelf -SW /bin/date | grep ' .plt '
  [12] .plt           PROGBITS      0000000000003020 003020 0004c0 10  AX  0   0 16

$ dd skip=$((0x3020)) bs=1 count=$((0x4c0)) < /bin/date 2> /dev/null | ndisasm -b 64 - 
00000000  FF354A7D0100      push qword [rel 0x17d50]
00000006  FF254C7D0100      jmp [rel 0x17d58]
0000000C  0F1F4000          nop dword [rax+0x0]
00000010  FF254A7D0100      jmp [rel 0x17d60]
00000016  6800000000        push qword 0x0
0000001B  E9E0FFFFFF        jmp 0x0
00000020  FF25427D0100      jmp [rel 0x17d68]
00000026  6801000000        push qword 0x1
0000002B  E9D0FFFFFF        jmp 0x0
00000030  FF253A7D0100      jmp [rel 0x17d70]
00000036  6802000000        push qword 0x2
0000003B  E9C0FFFFFF        jmp 0x0
. . .
. . .
# rasm2 을 이용하면 x86 이외의 다른 아키텍쳐 binary 도 disassemble 할 수 있습니다.
$ dd skip= ..... | rasm2 -a arm -b 64 -B -D -f -