Special Commands

Shell 은 기본적으로 command arg1 arg2 ... 형식의 명령을 다루지만 그 외에 shell 자체에서 제공하는 명령이 있습니다. 이것은 shell 에의해 직접 해석되고 실행되기 때문에 사용방법에 있어서 연산자를 escape 해야 한다든지 변수를 quote 해서 사용해야 하는 제약없이 편리하게 사용할 수 있습니다.

(( )) , let 는 산술연산에 특화된 기능을, [[ ]][ 명령과 같이 테스트에 특화된 기능을 제공합니다. special commands 는 bash 에서 제공되는 기능으로 sh 에서는 사용할 수 없습니다.

(( ... ))

먼저 이 명령은 산술연산을 위한 명령으로 스트링은 다루지 않고 숫자만 다룹니다. 따라서 식 안에서 알파벳으로 된 단어가 오면 그것은 변수명이라는 것 외에 다른 의미가 없습니다. 그래서 변수 이름에는 보통 $ 문자를 붙이지 않아도 됩니다. $var, var 모두 같은 의미입니다. 매개변수 확장과 명령 치환도 사용할 수 있는데 이때는 $ 문자를 붙여야 합니다. test 명령에서 사용되는 -eq -ne -le -ge -lt -gt 산술 연산자가 싫을땐 이 명령을 사용하면 됩니다.

변수값이 숫자가 아니거나 존재하지 않는 변수를 사용할 경우는 0 과 같습니다.

$ A=10
$ (( A = $A + A ))               # 변수 이름에 '$' 를 붙이지 않아도 된다.
$ echo $A
20

$ A=(5 6 7)
$ (( A[0] = A[1] + ${#A[@]} ))   # { } 를 이용하는 매개변수 확장은 '$' 를 붙여야한다.
$ echo ${A[0]}
9

# 명령 치환도 사용할 수 있다
$ B=1000
$ (( B += `date | awk '{print $4}'` ))
$ echo $B
3020

다음과같이 indirection 을 이용한 변수값 대입에 (( )) 를 활용할 수 있습니다.

$ hello=200
$ linux=hello

$ echo ${!linux}
200

$ a=he b=llo

$ (( foo=hello, bar=he$b, zoo=$a$b, baz=$linux ))

$ echo $foo $bar $zoo $baz
200 200 200 200

(( )) 는 생긴건 소괄호로 생겼지만 subshell 이 아닌 현재 shell 에서 실행됩니다. 그래서 현재 shell 의 변수값을 직접 변경할 수 있습니다. 식을 작성할 때 사용할수 있는 연산자들은 C 언어와 같습니다. var++, --var, a ? b : c, ( ) 을 이용한 연산자 우선순위 조절, , 연산자 등 모두 사용할 수 있습니다. ( 거듭제곱은 ** )

정리하면 (( )) 내에서는 그냥 프로그래밍 언어를 한다고 생각하면 됩니다. 참, 거짓 판단도 동일하게 0 이 거짓이고 그외 숫자가 참입니다. ( 산술연산 이기 때문에 )

$ (( 1 < 2 )); echo $?       # (( )) 안에서 참으로 종료됐으므로 종료상태 값은 0
0                            

$ (( 1 > 2 )); echo $?       # (( )) 안에서 거짓으로 종료됐으므로 1
1                            

$ (( 0 )); echo $?           # 산술연산에서 0 은 거짓 이므로 종료상태 값은 1
1                            

$ (( 1 )); echo $?           # 산술연산에서 0 이외의 값은 참 이므로 종료상태 값은 0
0                            

$ (( var = 0, res = (var ? 3 : 4) ))
$ echo $res
4

$ (( var = 1, res = (var ? 3 : 4) ))
$ echo $res
3

$ AA=10
$ while (( AA > 5 )); do                  $ for (( i = 0; i < 10; i++ )); do
    echo $AA                                    echo $i
    (( AA-- ))                              done
done

예를들어 opt1, opt2, opt3 세 개의 변수의 합이 5 보다 클 경우를 조건문에서 사용할 경우 기존의 test 명령을 사용하는 것보다 (( )) 를 사용하면 식이 더 간단해집니다.

let "res = opt1 + opt2 + opt3"                if (( opt1 + opt2 + opt3 > 5 )); then 
if [ "$res" -gt 5 ]; then                         . . .
    . . .                                     fi
fi

let

Shell 에서는 기본적으로 모든 값이 스트링으로 처리되므로 숫자로 다루어야 할 경우 문제가 됩니다.
이때 let 을 이용하면 산술연산을 할 수 있습니다.

$ res=100
$ res+=20
$ echo $res         # 숫자가 스트링으로 처리되어 concatenation 이 된다.
10020

$ res=100
$ let res+=20       # let 을 이용하면 산술연산을 할 수 있습니다.
$ echo $res
120

let 은 한줄에 여러 개의 산술식을 사용할 수 있습니다. 이때 하나의 식은 하나의 인수와 매칭이 되어야 하므로 식을 쓸때는 공백 없이 붙여 써야 합니다. 하지만 quotes 을 이용하면 공백을 사용할 수 있습니다.

$ let var = 1 + 2
bash: let: =: syntax error: operand expected (error token is "=")
$ let var += 1
bash: let: +=: syntax error: operand expected (error token is "+=")
$ let var=1+2      # 식을 쓸땐 공백없이 붙여서 하나의 인수가 되어야한다.
OK

$ i=2 val=200
$ let aa1=100 aa$i=val res=aa1+aa$i          # 한줄에 여러개의 식을 사용할 수 있다.
$ echo $res
300

$ let var=1+3 "res = (var == 5 ? 10 : 20)"   # quote 을하면 공백을 사용할 수 있다.
$ echo $res
20
$ let "var++, res = (var == 5 ? 10 : 20)" 
$ echo $res
10
$ let "2 < 1"; echo $?
1

help let 하면 사용할 수 있는 연산자들을 한눈에 볼 수 있습니다.

[[ ... ]]

이건 생긴 모양에서 알수있듯이 [ 명령의 확장 버전입니다. 그래서 [ 명령에서 사용할 수 있는 것은 동일하게 사용할 수 있습니다. 그리고 더해서 스트링의 pattern 매칭과 regex 매칭 기능도 제공합니다.

[ 명령에서 스트링 연산자를 이용하여 두 값을 비교할 때는 스트링 대 스트링 비교입니다. 하지만 [[ ]] 명령에서는 스트링 대 스트링( == ) , 스트링 대 pattern( == ) , 스트링 대 regex( =~ ) 이 가능합니다. 그래서 오른쪽에 pattern 이나 regex 이 올때는 스트링과 구분하기 위해서 quote 을 해주면 안됩니다 ( 정확히는 glob 문자나 regex 문자를 quote 에서 제외해야 됩니다 ). 왜냐하면 pattern 이나 regex 를 quote 하면 스트링 대 스트링 비교가 되기 때문입니다.

연산자를 중심으로 비교되는 스트링은 왼쪽에, pattern 이나 regex 은 오른쪽에 위치합니다. 비교되는 스트링을 변수로 사용할 때는 quote 을 생략할 수 있습니다. 사용되는 regex 형식은 Extended Regular Expressions 입니다. sh 에서는 regex 매칭을 하기 위해서 expr 명령을 사용하면 됩니다.

#####################  pattern matching  #####################

$ pat='*llo wor*'

$ [[ "hello world" == *llo\ wor* ]]; echo $?         # pattern 매칭
0
# glob 문자만 quote 에서 제외해도 된다.
$ [[ "hello world" == *"llo wor"* ]]; echo $?        # pattern 매칭
0
$ [[ "hello world" == $pat ]]; echo $?               # pattern 매칭
0

$ [[ "hello world" == "*llo wor*" ]]; echo $?        # 스트링 매칭
1
# 패턴에 해당하는 변수를 quote 하였으므로 스트링 매칭
$ [[ "hello world" == "$pat" ]]; echo $?             # 스트링 매칭
1

######################  regex matching  #######################

$ regex='.*llo wor.*'

$ [[ "hello world" =~ .*llo\ wor.* ]]; echo $?      # regex 매칭
0
# regex 문자만 quote 에서 제외해도 된다.
$ [[ "hello world" =~ .*"llo wor".* ]]; echo $?     # regex 매칭
0
$ [[ "hello world" =~ $regex ]]; echo $?            # regex 매칭
0

$ [[ "hello world" =~ ".*llo wor.*" ]]; echo $?     # 스트링 매칭
1
# regex 에 해당하는 변수를 quote 하였으므로 스트링 매칭
$ [[ "hello world" =~ "$regex" ]]; echo $?          # 스트링 매칭
1
................................................................

# 공백, <, >, |  같은 문자는 모두 escape 해줘야 한다.
$ str="delete|unset <uuid|vmname>"
$ [[ $str =~ delete[^\ ]*\ \<uuid\|vmname\> ]]; echo $?
0

# regex 만 남기고 나머지는 quote 을하면 간단해진다.
$ [[ $str =~ delete[^\ ]*" <uuid|vmname>" ]]; echo $?
0

# regex 을 변수에 대입해서 사용하면 더 간단해진다.
$ regex='delete[^ ]* <uuid\|vmname>'
$ [[ $str =~ $regex ]]; echo $?
0

# 또는 다음과 같이 ( ) 괄호를 사용할 수도 있는데 이경우도 >(...) or <(...) 와같이
# 프로세스 치환 형태가 될경우는 >, < 를 escape 해줘야 됩니다.  예) ((.*)<[^>]+\>(.*))
[[ $str =~ (delete[^ ]* <uuid\|vmname>) ]]; echo $?
0

# escape 문자를 사용하려면 다음과 같이 $' ' 를 사용하면 됩니다.
[[ $str == *foo$'\n'bar* ]]

[[ $str =~ foo[$' \t\n']+bar ]]     # 또는 foo[$IFS]+bar

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

# regex 의 스트링 매칭은 부분만 매칭이 돼도 참이 됩니다.
$ [[ "hello world" =~ "wor" ]]; echo $?    # regex 매칭
0

$ [[ "hello world" == "wor" ]]; echo $?    # 스트링 매칭
1

regex 매칭시 [[ ... ]] 안에서는 \s , \w , \b 같은 확장 기능 은 사용할 수 없습니다.

str='foo bar zoo'

$ [[ $str =~ ^\w+\s+ ]] && echo yes             # 사용할 수 없다.
$
$ [[ $str =~ ^[[:alnum:]_]+[[:space:]]+ ]] && echo yes
yes

$ regex='^\w+\s+'                               # 변수를 사용할 경우는 가능
$ [[ $str =~ $regex ]] && echo yes
yes
$ [[ $str =~ $(echo '^\w+\s+') ]] && echo yes
yes

비교되는 값이 array 변수일 경우는 ( $@, $* 포함 ) 전체 원소가 하나의 값으로 사용됩니다.

$ arr=( foo bar zoo )

$ [[ ${arr[@]} =~ ^bar$ ]] && echo yes        # 원소 단위 매칭이 안됨
$

$ [[ ${arr[@]} =~ ^foo\ bar ]] && echo yes
yes
$ [[ ${arr[@]} == foo\ bar* ]] && echo yes
yes

$ [[ ${arr[@]} =~ bar ]] && echo yes
yes
$ [[ ${arr[@]} == *bar* ]] && echo yes
yes

다음은 [[ ]] 명령을 사용할때 주의할 점입니다.

# 스트링 비교시 값에 escape 할때 사용하는 `\` 문자가 포함될경우 quote 을 해줘야한다
$ val='\"aaa bbb\"'

$ [[ $val == $val ]] && echo yes
$
$ [[ $val == "$val" ]] && echo yes      # quote 을 해줘야한다.
yes

$ [[ $val =~ $val ]] && echo yes
$
$ [[ $val =~ "$val" ]] && echo yes      # quote 을 해줘야한다.
yes
---------------------------------------------------------------

# test 명령에서 사용되는 옵션이 스트링 비교에 사용될경우 quote 을 해줘야 한다.
$ val1="-e|-f|-g|-h"

$ [[ -f == @($val1) ]] && echo yes       # ERROR

$ [[ "-f" == @($val1) ]] && echo yes     # OK
yes

$ val2="-f"                              # 변수를 사용할 경우는 괜찮다.

$ [[ $val2 == @($val1) ]] && echo yes    # OK
yes

$ [[ $val2 ]] && echo yes                # OK
yes

AND, OR

[[ ]] 에서는 자체 && , || 연산자를 제공합니다. 따라서 [ ] 명령에서 사용되는 -a (and), -o (or) 은 사용할 수 없습니다. shell 메타문자가 아니므로 우선순위는 프로그래밍 언어와 같이 && 가 높습니다. 또한 ( ) 를 이용해 우선순위 조절을 할 때도 escape 할 필요가 없습니다.

$ [[ A == A -a B == B ]] && echo YES               # -a, -o 연산자는 사용할 수 없다.
bash: syntax error in conditional expression
bash: syntax error near '-a'

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

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

if [[ A == A && B == B || C == C ]]; then ...      # OK
------------------------------------------

$ [[ 1 -eq 1 || 1 -eq 2 && 1 -eq 2 ]]; echo $?     # && 연산자가 우선순위가 높다
0
# '( )' 를 이용해 우선순위 조절을 할때는 '[' 명령과 달리 escape 할 필요가 없습니다.
$ [[ ( 1 -eq 1 || 1 -eq 2 ) && 1 -eq 2  ]]; echo $?
1
$ [[ 1 -eq 1 ]] || [[ 1 -eq 2 ]] && [[ 1 -eq 2 ]]; echo $?
1

BASH_REMATCH

=~ 연산자를 이용한 regex 매칭시에 소괄호 ( ) 를 이용해 캡처를 할 수 있습니다. 이때 전체매칭은 $BASH_REMATCH[0] 에 첫번째 ( ) 매칭은 $BASH_REMATCH[1] ... 에 각각 저장됩니다.

#!/bin/bash

string=$1 
regex=$2
if [[ $string =~ $regex ]]; then
    echo "$string matches"
    for (( i = 0; i < ${#BASH_REMATCH[@]}; i++ )); do
        echo "  capture[$i]: ${BASH_REMATCH[i]}"
    done
else
    echo "$string does not match"
fi

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

$ ./test.sh aabbxcc 'a(b{2,3})([xyz])c'
aabbxcc matches
  capture[0]: abbxc
  capture[1]: bb
  capture[2]: x

regex 에 .* 를 사용하지 않을 경우 앞에서부터 매칭이 됩니다.

$ foo='aaa 123= bbb 456= ccc 789= ddd'

$ [[ $foo =~ ([0-9]+=) ]]

$ echo ${BASH_REMATCH[1]}           # 첫번째 매칭
123=

$ [[ $foo =~ .*" "([0-9]+=) ]]      # ".*" 를 사용

$ echo ${BASH_REMATCH[1]}           # 마지막 매칭
789=

사용예 )

#!/bin/bash
while read -r line; do
    if [[ $line =~ ^([0-9]+)\ 번서버$ ]]; then     # ([0-9]+) 를 이용해 숫자를 캡춰해서
        ofile=${BASH_REMATCH[1]}_server.txt       # ofile 파일명을 설정하는데 사용.
        echo "set output file as $ofile"
    elif [[ -n $line ]]; then
        echo "$line" >> "$ofile"
    fi
done

입력값에서 <...>[...] 형태의 스트링을 모두 제거합니다.

string='VBoxManage cloudprofile <--provider=name> <--profile=name> 
update [--clouduser=unique id] [--fingerprint=MD5 string] [--keyfile=path]'

while [[ $string =~ (.*)\<[^\>]+\>(.*) ]]; do
    string=${BASH_REMATCH[1]}${BASH_REMATCH[2]}
done
while [[ $string =~ (.*)\[[^]]+](.*) ]]; do
    string=${BASH_REMATCH[1]}${BASH_REMATCH[2]}
done
echo $string

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

VBoxManage cloudprofile update

--[no-]foo 형태를 --foo--no-foo 로 확장합니다.

string='--[no-]foo  --[dont-]bar  --zoo'
options=( $string )

for ((i = 0; i < ${#options[*]}; i++)); do
    option=${options[i]}
    if [[ $option =~ (\[(no-|dont-)]) ]]; then
        option1=${option/${BASH_REMATCH[1]/[/\\[}/}    # --foo
        option2=${option//[][]/}                       # --no-foo
        echo "$option1 $option2"
    else
        echo "$option"
    fi
done

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

--foo --no-foo
--bar --dont-bar
--zoo

확장 패턴

[ ] 일반 명령과 달리 [[ ]] 에서는 확장패턴 을 사용할 수 있습니다.

$ COLOR="RED"

$ [[ $COLOR == @(RED|GREEN|BLUE) ]] && echo yes     # @(   )
yes
$ [[ $COLOR =~ RED|GREEN|BLUE ]] && echo yes        # =~
yes
--------------------------------------------

$ COLOR="RED color"

$ [[ $COLOR == @(RED|GREEN|BLUE) ]] && echo yes
$
$ [[ $COLOR =~ RED|GREEN|BLUE ]] && echo yes
yes

Quiz

특정 변수에 값이 존재하는지 체크할 때 다음과 같이 [[ ]] 를 활용할 수 있습니다.

$ SOME_OPTIONS="-a -b"
                                             # 변수가 존재하지 않거나 값이 empty 일 경우
$ [[ $SOME_OPTIONS ]] && echo yes            $ [[ ! $NOT_EXIST ]] && echo yes
yes                                          yes

# 위 문장은 다음과 같은것 입니다.                  # 위 문장은 다음과 같은것 입니다.
$ test -n "$SOME_OPTIONS" && echo yes        $ test -z "$NOT_EXIST" && echo yes
yes                                          yes

2 .

다음 테스트문은 어디가 잘못되었을까요?

$ AA=y

$ [ "$AA" = [Yy] ] && echo yes
$

[ 는 일반 명령이므로 [Yy][ 명령의 인수가 되고 globbing 대상입니다. 따라서 현재 디렉토리에 파일 y 가 존재한다면 아래의 첫 번째 명령문과 같게되겠지만 그렇지 않을 경우는 두 번째 명령문과 같게 됩니다.

$ [ "y" = "y" ] && echo yes
yes

$ [ "y" = "[Yy]" ] && echo yes
$

sh 에서는 [[ ]] 명령을 사용할 수 없으므로 다음과 같은 방법을 사용하면 됩니다.

sh$ AA=y

sh$ case $AA in ([Yy]) echo yes; esac      # case 문에서는 pattern 사용이 가능하므로
yes

sh$ expr "x${AA}" : "x[Yy]$" > /dev/null && echo yes
yes

sh$ [ -n "$(echo "$AA" | grep '^[Yy]$')" ] && echo yes
yes