Arithmetic Expansion

Shell 에서 명령문 작성시 산술연산이 필요할 때 사용하는 표현식입니다. 사용 방식은 (( )) 특수명령 과 동일하지만 이것은 괄호 앞에 $ 문자가 있어서 명령으로는 사용할 수 없고 $( ) 명령치환과 같이 연산 결과를 변수에 대입하거나 명령의 인수 부분에서 사용됩니다. 명령치환 과는 달리 subshell 이 아닌 현재 shell 에서 실행되므로 현재 shell 의 변수값을 변경할 수 있습니다. 식을 작성할 때는 매개변수 확장과 명령 치환도 모두 사용할 수 있습니다.

같은 기능을 하는 $[ ] 표현식은 deprecated 라고 하니 사용하지 말아야겠습니다.
산술연산을 하는 외부 명령으로는 expr, bc 등이 있습니다.

# 연산 결과값을 변수에 대입시킬 수 있다.
$ AA=$(( 1 + 2 )); echo $AA
3

$ CC=$(( BB = AA += 2 ))

$ echo $AA $BB $CC
5 5 5

# 명령의 인수 부분에서 사용될 수 있다.
$ expr 100 + $(( AA + 2 ))
107

# 식 내에서는 매개변수 확장, 명령 치환도 모두 사용할 수 있다.
$ B=1000
$ echo $(( B += $(date | awk '{print $4}') ))
3020

변수 이름 활용

$(( ... )) 안에서는 알파벳 문자가 나오면 그것은 변수 이름이라는 것 외에 다른 뜻이 없습니다. 따라서 기본적으로 변수 이름 앞에 $ 문자를 붙이지 않아도 되는데 이것을 활용하면 다음과 같이 변수 이름을 만들어 사용할 수도 있습니다.

$ foo1=111 foo2=222

$ a=foo b=1 c=2

$ echo $(( foo1 + foo2 ))       # 변수 이름 앞에 '$' 문자를 붙이지 않아도 된다.
333

$ echo $(( foo$b + foo$c ))     # foo$b 는 foo1 이 되고, foo$c 는 foo2 가 된다.
333

$ echo $(( $a$b + $a$c ))       # $a$b 는 foo1 이 되고, $a$c 는 foo2 가 된다.
333
abc_1=10
xyz_1=20
abc_2=30
xyz_2=40
abc_3=50
xyz_3=60

for i in {1..3}
do
    echo xyz_$i + abc_$i == $(( xyz_$i + abc_$i ))
done
-------------------------

xyz_1 + abc_1 == 30
xyz_2 + abc_2 == 70
xyz_3 + abc_3 == 110

: 명령 활용하기

: 명령을 활용하면 (( )) 명령과 같이 단독으로 연산을 할 수 있습니다.

# 기본적으로 명령행 상에서 단독으로 사용할 수는 없다.
$ $(( 1 + 2 ))
3: command not found

$ AA=$(( 1 + 2 ))

# 하지만 ':' 명령을 활용하면 명령행 상에서 단독으로 연산을 할 수 있습니다.
$ : $(( AA += 1, BB = AA + 1 ))

$ echo $AA $BB
4 5

현재 shell 의 변수값을 변경할 수 있습니다.

$ AA=100

$ : $(( AA = 200 ))

$ echo $AA
200

8 진수, 16 진수 연산

숫자가 0 으로 시작하면 8 진수로, 0x 로 시작하면 16 진수로 인식합니다.

$ echo $(( 010 )) $(( 8#10 ))          # 8 진수
8 8

$ echo $(( 0x10 )) $(( 16#10 ))        # 16 진수
16 16

$ echo $(( 2#1010 ))                   # 2 진수
10

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

$ echo $(( 010 + 0x10 + 10 + 2#10 ))   # 8 진수 + 16 진수 + 10 진수 + 2 진수
36

8 진수나 16 진수로 변경하려면 printf 명령을 사용하면 됩니다.

$ printf %x $(( 0135 ))           # 8 진수를 16 진수로 변경
5d

$ printf %o $(( 0x5d ))           # 16 진수를 8 진수로 변경
135

$ printf %x $(( 2#11010101 ))     # 2 진수를 16 진수로 변경
d5

Bitwise 연산

  • >>   (Right Shift) - Shift bits to right and adds '0's on left (Divide by 2 operation)
  • <<   (Left Shift) - Shifts bits to left and add '0' on right (Multiply by 2 operation)
  • &    (Bitwise AND) - Performs AND operation on every bit and produces result
  • |     (Bitwise OR) - Performs OR operation on every bit and produces result
  • ^    (Bitwise XOR) - Performs XOR operation on every bit and produces result
  • ~    (Bitwise NOT) - Inverts all the bits
  • <<=  (Left Shift Assign)
  • >>=  (Right Shift Assign)
  • &=   (Bitwise AND Assign)
  • |=    (Bitwise OR Assign)
  • ^=   (Bitwise XOR Assign)
# Right Shift
echo $(( 15 >> 1 ))         # '1111' >> 1 = '0111'  ( Ans : 7 )
echo $(( 15 >> 3 ))         # '1111' >> 3 = '0001'  ( Ans : 1 )

# Left Shift
echo $(( 15 << 1 ))         # '1111' << 1 = '11110'    ( Ans : 30 )
echo $(( 15 << 3 ))         # '1111' << 3 = '1111000'  ( Ans : 120 )

# Bitwise AND
echo $(( 15 & 1 ))          # '1111' & '0001' = '0001'  ( Ans : 1 )
echo $(( 15 & 3 ))          # '1111' & '0011' = '0011'  ( Ans : 3 )

# Bitwise OR
echo $(( 15 | 1 ))          # '1111' | '0001' = '1111'  ( Ans : 15 )
echo $(( 15 | 3 ))          # '1111' | '0011' = '1111'  ( Ans : 15 )

# Bitwise XOR
echo $(( 15 ^ 1 ))          # '1111' ^ '0001' = '1110'  ( Ans : 14 )
echo $(( 15 ^ 3 ))          # '1111' ^ '0011' = '1100'  ( Ans : 12 )

# Bitwise NOT
echo $(( ~1 ))              # ~ '0000 0001' = '1111 1110'  ( Ans : -2 )
echo $(( ~3 ))              # ~ '0000 0011' = '1111 1100'  ( Ans : -4 )
echo $(( ~15 ))             # ~ '0000 1111' = '1111 0000'  ( Ans : -16 )

연산자 우선순위는 C 언어와 같습니다.

~ > shift 연산 > <, <=, >, >= > ==, != > & > ^ > | > && > ||
# shift 연산이 우선순위가 높기 때문에 이것은 2 | ( 4 >> 1 ) 와 같다.
$ echo $(( 2 | 4 >> 1 ))
2

$ echo $(( ( 2 | 4 ) >> 1 ))
3

# &, ^, | 연산은 ==, != 등호 보다도 우선순위가 낮다.
# 따라서 다음 식은 2 | ( 4 == 3 ) 와 같다.
$ echo $(( 2 | 4 == 3 ))
2

$ echo $(( ( 2 | 4 ) == 3 ))
0

거듭제곱 연산

거듭제곱은 연산자로 ** 를 사용합니다 ( ^ 는 XOR 연산자로 사용되므로 ). 한가지 사용시 주의할 점은 연산 방식이 요즘 사용되는 프로그램들과 다릅니다. 이것은 과거의 유물로 수정되지 못하고 남아있는 기능인데 음수를 거듭제곱할 경우 보통 - 를 제외하고 먼저 거듭제곱을 한후에 결과에 - 가 붙는데 반해서 shell 에서는 음수 자체를 거듭제곱합니다. 이것은 ksh, zsh, bc 에서도 모두 동일합니다.

$ echo $(( -2 ** 2 ))                # (-2) 를 2 제곱 한것과 같다.
4                                    # (ksh, zsh, bc 모두 동일)

$ echo $(( -(2 ** 2) ))
-4

$ bc -l <<< '-2 ^ 2'                 # bc 도 오래된 명령이라 연산방식이 같다.
4

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

$ awk 'BEGIN { print -2 ^ 2 }'       # 그외 계산기나 요즘 사용되는 프로그램의 경우는
-4                                   # 2 에 2 제곱을 한후에 결과에 "-" 가 붙습니다.
                                     # ("^" 연산자가 "-" 보다 우선순위가 높으므로)
$ perl -E 'say -2 ** 2'
-4

$ python3 -c 'print (-2 ** 2)'
-4

$ qalc '-2 ^ 2'
−(2^2) = −4

소프트웨어는 이게 문제 입니다. 연산을 하는데 기본적인 기능을 처음에 잘못 정의하면 나중에 고칠수가 없습니다 ( 기존에 작성된 프로그램이 모두 사용할 수 없게 되므로 ). shell 방식이 문제가 되는것은 다음과 같이 - 연산자의 개수가 변경이 돼도 부호가 바뀌질 않습니다.

$ echo $(( 10 - 3 ** 2 ))         # 10 - ( 3 ** 2 ) = 10 - 9 = 1
1
$ echo $(( 10 - - 3 ** 2 ))       # 10 - ( - 3 ** 2 ) = 10 - 9 = 1
1
$ echo $(( 10 - - - 3 ** 2 ))     # - - 는 + 이므로
1                                 # 10 - ( - - 3 ** 2 ) = 10 - ( 3 ** 2 ) = 10 - 9 = 1

$ python3 -c 'print(10 - 3 ** 2)'          # 10 - ( 3 ** 2 ) = 10 - 9 = 1
1
$ python3 -c 'print(10 - - 3 ** 2)'        # - - 는 + 이므로
19                                         # 10 + ( 3 ** 2 ) = 10 + 9 = 19
$ python3 -c 'print(10 - - - 3 ** 2)'      # - - - 는 - 이므로
1                                          # 10 - ( 3 ** 2 ) = 10 - 9 = 1

Quiz

입력 값을 2 진수로 변환해서 출력하는 명령을 만드는 것입니다. 명령은 파일명을 인수로 받거나 파이프로 연결해 사용할 수 있어야 합니다.

같은 기능을 하는 명령으로 $ xxd -b file 가 있습니다.

$ binary data.bin                     $ echo 한글 | binary
. . .                                 . . .
$ binary data.bin 4                   $ cat data.bin | binary 4    # 4 는 컬럼 개수
. . .                                 . . .
---------------------------------------------------------------

binary () 
{ 
    # stdin 이 터미널인데 전달된 인수가 없거나 "-h" , "--help" 이면 usage 를 출력
    if [[ -t 0 && ( ${#@} == 0 || $1 == "-h" || $1 == "--help" ) ]]; then
        echo -e "\nUsage: $FUNCNAME file [cols]\n" >&2
        return 1
    fi
    local file cols command    # stdin 이 터미널이 아니면 cols 변수값만 설정
    test -t 0 && { file=\"$1\" cols=$2 ;} || cols=$1
    read -rd "" command <<@
    perl -lpe '\$_=unpack "B*"' $file | fold -w8
@
    if test $((cols)) -gt 0; then
        local dash
        printf -v dash "%${cols}s"
        command+="| paste -d ' ' ${dash// / -}"
    else
        command+="| paste -s -d ' '"
    fi
    eval "$command"
}
1. cols 변수는 옵션인데 만약에 if test "$cols" -gt 0; then ... 와같이 작성한다면 cols 변수값이
   설정되지 않았을 경우 test 명령문에서 "integer expression expected" 오류가 발생합니다.
   이때 $(( )) 표현식을 사용하면 설정되지 않은 변수에 대해 0 이 반환되므로 오류를 방지할수 있습니다.

2. perl 명령문에서 사용된 $file 변수는 원래 "$file" 와같이 quotes 을 사용해야 되는데요.
   (파일 이름에 공백이 있을수 있으므로). 하지만 quotes 을 사용하게 되면 stdin 이 터미널이
   아닐경우 $file 변수값이 존재하지 않게 되므로 "" 인수 오류가 발생합니다.
   따라서 이때는 quotes 처리를 $file 변수값을 설정하는 곳에서 file=\"$1\" 와같이 해줍니다.
   그러면 eval 에의해 명령문이 실행될때 최종 명령문에는 quotes 이 붙게 됩니다.

3. perl -lpe '$_=unpack "B*"' 명령문에서 $_ 변수는 shell 에서 사용되므로 escape 해야합니다.