Operators
우선순위가 높은 것부터 낮은 순서입니다.
Operators | Description |
---|---|
( . . . ) | Grouping. |
$ |
Field reference. |
++ -- |
Increment and decrement (prefix and postfix). |
^ ** |
Power. (right to left) |
+ - ! |
Unary plus, minus, logical NOT. |
* / % |
Multiply, divide and modulus (remainder). |
+ - |
Add and subtract. |
nothing | String concatenation. |
< <= == != > >= >> | |& |
Relational and redirection. |
~ !~ |
Matching, nonmatching. |
in |
Array membership. |
&& |
Logical AND. |
|| |
Logical OR. |
?: |
Conditional. (right to left) |
= += -= *= /= %= ^= **= |
Assignment. |
The
|&
,**
, and**=
operators are not specified by POSIX. For maximum portability, do not use them.
# '^=' 는 power 대입 연산자
$ awk 'BEGIN { aa = 3; aa ^= 2; print aa }'
9
++
--
연산자는 변수와의 사이에 공백이 올 수 있습니다.
$ awk 'BEGIN { n = 1; print ++ n; print n }'
2
2
$ awk 'BEGIN { n = 1; print n ++; print n }'
1
2
따라서 다음과 같은 경우 결과가 123
이 되지 않습니다.
왜냐하면 두 번째와 같이 해석되기 때문입니다.
$ awk 'BEGIN { n = 12; x = 2; r = n ++x; print r }'
122
$ awk 'BEGIN { n = 12; x = 2; r = (n ++)x; print r }'
122
$ awk 'BEGIN { n = 12; x = 2; r = n ++x; print n }'
13
r
값이 123
이 되게 하려면 다음과 같이 해야 합니다.
$ awk 'BEGIN { n = 12; x = 2; r = n (++x); print r }'
123
$ gawk 'BEGIN { n = 12; x = 2; r = (n) ++x; print r }'
123
$ gawk 'BEGIN { n = 12; x = 2; r = n "" ++x; print r }'
123
Quiz
7 + 3 * (5 - 2) + 4
와 같은 사칙연산 수식을 라인 단위로 입력받아
연산 결과를 출력하는 것입니다. 덧셈, 뺄셈보다는 곱셈, 나눗셈을 먼저 계산해야 하고
괄호가 있을 경우 제일 먼저 계산해야 합니다.
여기서는 stack 을 활용하는 후위 표기법을 사용했습니다.
자세한 설명은 쏭이님의 홈페이지 를 참고하세요.
후위 표기법 (postfix notation) 은 역폴란드 표기법 (RPN, Reverse Polish Notation) 이라고도 하는데 연산자를 연산 대상의 뒤에 쓰는 표기법입니다. 이 방식은 수식을 계산할 때 특별한 변환 없이, 수식을 앞에서부터 읽어 나가면서 stack 에 저장하면 된다는 장점이 있다.
$ ./parse.sh <<< '1 + 2 * 3'
7
$ cat file # 수식은 공백 없이 붙여 쓸 수 있습니다.
1 + 2 * 3 + 4
( 1 + 2 ) * 3 + 4
7+3*(5-2)+4
- - 3 - + - - 4 + 5
-3 - ( -4 + 5 )
3 - -( -10 + -2 )
3 - -(-( -10 + -2 ))
3 + -( -10 - -(3) + -( 3 - ( -10 + -2 ) + -2 ))
$ ./parse.sh file # 또는 cat file | ./parse.sh
11
13
20
4
-4
-9
15
23
사용되는 연산자 우선순위는
(
<+
-
<*
/
입니다.현재 연산자가 ostack 의 top 에 있는 연산자 보다 우선순위가 높아질 때까지 pop 해서 calc 하고 우선순위가 높아졌다면 ostack 에 push 합니다.
- 3 - ( - 4 - + - 5 )
에서 숫자 앞에 있는- 3
,- 4
,+ - 5
연산자는 두 개의 operand 를 이용해 연산하는 binary 연산자가 아니고 unary 연산자 이므로 ostack 에 push 하지 않고 number 에 부호를 설정해서 rstack 에 push 합니다. ( unary 연산자 위치에*
/
가 오는것은 오류 입니다. )
3 - - ( -10 + -2 )
와 같이 괄호 앞에 있는 unary 연산자는 괄호가 닫힐때 최종 결과값에 부호를 설정해야 합니다. 괄호는 nesting 될수 있으므로 ustack 을 이용해"("
에서 부호값을 저장하고")"
에서 rstack 에서 결과값을 pop 해서 부호를 설정합니다.
#!/usr/bin/env -S gawk -f
# stack 은 세 개를 사용합니다. ostack 에는 operator 와 괄호가 들어가고
# rstack 에는 operand 가 들어가고 calc 계산 결과가 저장됩니다.
# ustack 은 "(" 괄호 앞의 부호를 저장하는데 사용됩니다.
BEGIN { FS=""; token[0][0]; ostack[0]; rstack[0]; ustack[0] }
{
idx = 0
tokenize() # 라인 별로 tokenize 를 해서
print parse() # 연산 결과를 출력합니다.
delete token # 다음 입력 라인을 위한 초기화
}
function parse( i, len, op, pk, val, unary, unary_p) {
unary = unary_p = 1 # unary 연산자 부호 설정을 위한 변수. _p 는 괄호용
len = length(token)
for (i=0; i < len; i++) {
switch ( token[i]["type"] ) {
case "number" :
# unary 값으로 부호를 설정해서 rstack 에 push 합니다.
push(rstack, unary * token[i]["value"])
unary = 1 # push 후에는 값을 1 로 reset
break
case "operator" :
op = token[i]["value"]
# 현재 연산자 바로 이전 token 이 ")" 와 "number" 가 아니면 unary 연산자
if (token[i-1]["value"] != ")" && token[i-1]["type"] != "number")
{
# unary 연산자 자리에 * or / 가 오면 오류
if ( op ~ /*|\// ) error( "* or / on unary operator" )
if ( token[i+1]["value"] == "(") { # 바로 다음 token 이 "(" 이면
unary_p = unary * (op 1) # 괄호를 위한 unary_p 변수를
unary = 1 # 설정하고 unary 변수는 reset
} else unary *= (op 1) # 이외는 unary 변수를 설정
break
}
pk = peek(ostack) # ostack 의 top 항목을 조회해서
if ( pk == "" || pk == "(" ) # 항목이 empty 이거나 "(" 일 경우
{ push(ostack, op); break } # 연산자를 ostack 에 push 합니다.
if ( op == "*" || op == "/" ) { # 현재 연산자가 "*" or "/" 이면
while( peek(ostack) ~ /*|\// ) # 같은 우선순위 연산자를 꺼내어
calc( pop(ostack)) # calc 하고 완료되면 현재
push(ostack, op) # 연산자를 ostack 에 push 합니다
break
}
# 그외 현재 연산자가 "+" or "-" 일 경우 우선순위가 더 낮은 "(" 가
# ostack 의 top 이 될때까지 꺼내어 calc 하고 현재 연산자를 push 합니다.
while( peek(ostack) != "(" && slen(ostack) > 0 )
calc( pop(ostack))
push( ostack, op)
break
case "parenthesis" :
val = token[i]["value"]
if ( val == "(" ) { # "(" 를 만나면 ostack 에 push 하고
push(ostack, val) # 괄호를 위한 부호 설정을 위해
push(ustack, unary_p) # ustack 에 unary_p 를 push 하고
unary_p = 1 # 완료되면 1 로 reset 합니다.
} else { # ")"
while (val = pop(ostack)) { # ")" 를 만나면
if ( val == "(" ) break # "(" 가 나올때 까지 연산자를
else calc(val) # pop 해서 calc 합니다.
}
if ( val != "(" ) error( "parentheses missmatch. more ')'" )
# ustack 에 저장해 놓은 부호값을 최종 결과값에 적용합니다.
push(rstack, pop(ustack) * pop(rstack))
}
}
}
len = slen(ostack) # ostack 에 연산자가 남아 있을경우
for (i = 1; i <= len; i++) { # pop 해서 calc 합니다.
val = pop(ostack) # "(" 가 남아 있다는 것은 ")" 가 모자란것
if ( val == "(" ) error( "parentheses missmatch. less ')'" )
calc( val )
}
return pop(rstack) # ostack 에 있는 모든 연산자가 calc 되고 난후에는
# rstack 에는 최종 결과만 남아있게 됩니다.
}
function calc(op, num1, num2, res) {
num1 = pop(rstack) # rstack 에서 두개의 operand 를 pop 해서
num2 = pop(rstack) # 인수로 전달된 op 연산자와 연산을 하고
switch (op) {
case "+" : res = num2 + num1; break
case "-" : res = num2 - num1; break
case "*" : res = num2 * num1; break
case "/" :
if (num1 == 0 ) error( "division by zero" )
res = num2 / num1
}
push(rstack, res) # 결과를 다시 rstack 으로 push 합니다.
}
function error (str) {
print "ERROR: " str > "/dev/stderr"
exit 1
}
############ tokenize ############
function tokenize ( i, num) {
for (i = 1; i <= NF; i++) {
switch ($i) {
case "+" : case "-" :
case "*" : case "/" :
token[idx]["type"] = "operator"
token[idx++]["value"] = $i
break
case "(" : case ")" :
token[idx]["type"] = "parenthesis"
token[idx++]["value"] = $i
break
case /[0-9.]/ :
while ( $i ~ /[0-9.]/ ) num = num $(i++)
token[idx]["type"] = "number"
token[idx++]["value"] = num
num=""; i--
break
case /[ \t]/ :
break
default :
error( "tokenize : '" $i "'")
}
}
}
############ stack ############
function push(arr, val) {
arr[ length(arr) ] = val
}
function pop (arr, len, ret) {
len = length(arr)
if ( len > 1 ) {
ret = arr[ len - 1 ]
delete arr[ len - 1 ]
}
return ret
}
function peek (arr, len, ret) {
len = length(arr)
if ( len > 1 )
ret = arr[ len - 1 ]
return ret
}
function slen(arr) {
return length(arr) - 1
}
2 .
이번에는 사칙연산에 더해서 %
^
연산자도 추가하는 것입니다.
%
연산자는 우선순위가 *
/
와 같기 때문에 간단히 추가할 수 있지만,
^
연산자의 경우는 우선순위가 *
/
보다 높습니다.
또한 unary 연산자보다도 높기 때문에 다음과 같은 결과가 되어야 합니다.
# -2 가 4 제곱이 돼서 16 이 되는것이 아니고 2 ^ 4 가 먼저 계산되고 결과에 - 가 붙는다.
$ awk 'BEGIN { print -2 ^ 4 }'
-16
$ awk 'BEGIN { print -2 ^ -4 }' # 지수가 음수
-0.0625
따라서 ^
연산자 왼쪽에 있는 숫자에 붙는 부호는 괄호에서 부호를 처리할 때처럼
먼저 ustack 에 부호를 저장하고 나중에 연산 결과에 대해 부호를 적용해야 합니다.
. . .
function parse( i, len, op, pk, val, unary, unary_p) {
len = length(token)
unary = unary_p = 1
for (i=0; i < len; i++) {
switch ( token[i]["type"] ) {
case "number" :
if ( token[i+1]["value"] == "^" ) { # 숫자 바로 다음 연산자가 "^" 이면
push( ustack, unary) # 부호를 나중에 연산 결과에 설정해야
push( rstack, token[i]["value"]) # 하므로 ustack 에 push 합니다.
} else
push( rstack, unary * token[i]["value"])
unary = 1
break
case "operator" :
op = token[i]["value"]
if (token[i-1]["value"] != ")" && token[i-1]["type"] != "number")
{
if ( op ~ /*|\/|%|\^/ ) error( "* / % ^ on unary operator" )
if ( token[i+1]["value"] == "(") {
unary_p = unary * (op 1)
unary = 1
} else unary *= (op 1)
break
}
pk = peek(ostack)
# "^" 연산자는 우선순위가 제일 높으므로 무조건 ostack 에 push
if ( pk == "" || pk == "(" || op == "^" )
{ push(ostack, op); break }
# "%" 연산자는 우선순위를 "*" "/" 와 같게 취급
if ( op == "*" || op == "/" || op == "%" ) {
while( peek(ostack) ~ /*|\/|%|\^/ )
calc( pop(ostack))
push(ostack, op)
break
}
while( peek(ostack) != "(" && slen(ostack) > 0 )
calc( pop(ostack))
push( ostack, op)
break
case "parenthesis" :
val = token[i]["value"]
if ( val == "(" ) {
push(ostack, val)
push(ustack, unary_p)
unary_p = 1
} else { # ")"
while (val = pop(ostack)) {
if ( val == "(" ) break
else calc(val)
}
if ( val != "(" ) error("parentheses missmatch. more ')'")
# 닫는 괄호 바로 다음 연산자가 "^" 이면 부호를 설정하면 안된다.
if ( token[i+1]["value"] != "^" )
push(rstack, pop(ustack) * pop(rstack))
}
}
}
len = slen(ostack)
for (i = 1; i <= len; i++) {
val = pop(ostack)
if ( val == "(" ) error("parentheses missmatch. less ')'")
calc( val )
}
return pop(rstack)
}
function calc (op, num1, num2, res) {
num1 = pop(rstack)
num2 = pop(rstack)
switch (op) {
case "+" : res = num2 + num1; break
case "-" : res = num2 - num1; break
case "*" : res = num2 * num1; break
case "/" :
if (num1 == 0 ) error( "division by zero" )
res = num2 / num1
break
case "%" :
if (num1 == 0 ) error( "division by zero" )
res = num2 % num1
break
case "^" : res = num2 ^ num1
}
if ( op == "^" )
push(rstack, pop(ustack) * res) # "^" 연산 결과를 저장할 때 부호를 설정
else
push(rstack, res)
}
function tokenize ( i, num) {
for (i = 1; i <= NF; i++) {
switch ($i) {
case "+" : case "-" :
case "*" : case "/" :
case "%" : case "^" : # "%" "^" 연산자 추가
token[idx]["type"] = "operator"
token[idx++]["value"] = $i
break
case "(" : case ")" :
token[idx]["type"] = "parenthesis"
token[idx++]["value"] = $i
break
case /[0-9.]/ :
while ( $i ~ /[0-9.]/ ) num = num $(i++)
token[idx]["type"] = "number"
token[idx++]["value"] = num
num=""; i--
break
case /[ \t]/ :
break
default :
error( "tokenize : '" $i "'")
}
}
}
. . .
같은 우선순위 연산자가 연이어 있을 경우 왼쪽부터 계산할지, 오른쪽부터 계산할지
정하는 것을 associativity 라고 하는데 ^
연산자는 right associativity 입니다.
$ awk 'BEGIN { print 1 / 2 * 4 }'
2
$ awk 'BEGIN { print (1 / 2) * 4 }' # "^" 이외의 연산자들은 왼쪽부터 계산하는
2 # left associativity 이다.
$ awk 'BEGIN { print 1 / (2 * 4) }'
0.125
$ awk 'BEGIN { print 4 ^ 3 ^ 2 }'
262144
$ awk 'BEGIN { print 4 ^ (3 ^ 2) }' # "^" 연산자는 오른쪽부터 계산하는
262144 # right associativity 이다.
$ awk 'BEGIN { print (4 ^ 3) ^ 2 }'
4096
# 대입 연산도 연산 결과가 value 인 expression 으로 right associativity 에 해당합니다.
$ awk 'BEGIN { a = b = c = 100; print a,b,c }'
100 100 100
$ awk 'BEGIN { a = ( b = ( c = 100 )); print a,b,c }'
100 100 100
최종 결과
#
# 실행: ./parse.sh <<< '1 + 2 * 3 ^ (2 % 5) / 2'
#
sh$ cat parse.sh
#!/usr/bin/env -S gawk -M -f
BEGIN { FS=""; token[0][0]; ostack[0]; rstack[0]; ustack[0] }
{
idx = 0
tokenize()
print parse()
delete token
}
function parse( i, len, op, pk, val, unary, unary_p) {
len = length(token)
unary = unary_p = 1
for (i=0; i < len; i++) {
switch ( token[i]["type"] ) {
case "number" :
if ( token[i-1]["type"] == "number" ) error( "number after number" )
if ( token[i+1]["value"] == "(" ) error( "'(' after number" )
if ( token[i+1]["value"] == "^" ) {
push( ustack, unary)
push( rstack, token[i]["value"])
} else
push( rstack, unary * token[i]["value"])
unary = 1
break
case "operator" :
if ( token[i+1]["value"] == ")" ) error("')' after operator");
if ( i + 1 == len ) error("early termination.");
op = token[i]["value"]
if (token[i-1]["value"] != ")" && token[i-1]["type"] != "number")
{
if ( op ~ /*|\/|%|\^/ ) error( "'" op "' operator not allowed." )
if ( token[i+1]["value"] == "(") {
unary_p = unary * (op 1)
unary = 1
}
else unary *= (op 1)
break
}
pk = peek(ostack)
if ( pk == "" || pk == "(" || op == "^" )
{ push(ostack, op); break }
if ( op == "*" || op == "/" || op == "%" ) {
while( peek(ostack) ~ /*|\/|%|\^/ )
calc( pop(ostack))
push(ostack, op)
break
}
while( peek(ostack) != "(" && slen(ostack) > 0 )
calc( pop(ostack))
push( ostack, op)
break
case "parenthesis" :
val = token[i]["value"]
if ( val == "(" ) {
if ( token[i+1]["value"] == ")" ) error("')' after '('");
push(ostack, val)
push(ustack, unary_p)
unary_p = 1
} else {
if ( token[i+1]["value"] == "(" ) error("'(' after ')'");
while (val = pop(ostack)) {
if ( val == "(" ) break
else calc(val)
}
if ( val != "(" ) error("')' parentheses missmatch.")
if ( token[i+1]["value"] != "^" )
push(rstack, pop(ustack) * pop(rstack))
if ( token[i+1]["type"] == "number")
error("number after ')'");
}
}
}
len = slen(ostack)
for (i = 1; i <= len; i++) {
val = pop(ostack)
if ( val == "(" ) error("'(' parentheses missmatch.")
calc( val )
}
return pop(rstack)
}
function calc (op, num1, num2, res) {
num1 = pop(rstack)
num2 = pop(rstack)
switch (op) {
case "+" : res = num2 + num1; break
case "-" : res = num2 - num1; break
case "*" : res = num2 * num1; break
case "/" :
if (num1 == 0 ) error( "division by zero" )
res = num2 / num1
break
case "%" :
if (num1 == 0 ) error( "division by zero" )
res = num2 % num1
break
case "^" : res = num2 ^ num1
}
if ( op == "^" )
push(rstack, pop(ustack) * res)
else
push(rstack, res)
}
function error (str) {
print "Error: " str > "/dev/stderr"
exit 1
}
############ tokenize ############
function tokenize ( i, num) {
for (i = 1; i <= NF; i++) {
switch ($i) {
case "+" : case "-" :
case "*" : case "/" :
case "%" : case "^" :
token[idx]["type"] = "operator"
token[idx++]["value"] = $i
break
case "(" : case ")" :
token[idx]["type"] = "parenthesis"
token[idx++]["value"] = $i
break
case /[0-9.]/ :
while ( $i ~ /[0-9.]/ ) num = num $(i++)
token[idx]["type"] = "number"
token[idx++]["value"] = num
num=""; i--
break
case /[ \t]/ :
break
default :
error( "tokenize : '" $i "'")
}
}
}
############ stack ############
function push (arr, val) {
arr[ length(arr) ] = val
}
function pop (arr, len, ret) {
len = length(arr)
if ( len > 1 ) {
ret = arr[ len - 1 ]
delete arr[ len - 1 ]
}
return ret
}
function peek (arr, len, ret) {
len = length(arr)
if ( len > 1 )
ret = arr[ len - 1 ]
return ret
}
function slen (arr) {
return length(arr) - 1
}