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
  1. 사용되는 연산자 우선순위는 ( < + - < * / 입니다.

  2. 현재 연산자가 ostack 의 top 에 있는 연산자 보다 우선순위가 높아질 때까지 pop 해서 calc 하고 우선순위가 높아졌다면 ostack 에 push 합니다.

  3. - 3 - ( - 4 - + - 5 ) 에서 숫자 앞에 있는 - 3, - 4, + - 5 연산자는 두 개의 operand 를 이용해 연산하는 binary 연산자가 아니고 unary 연산자 이므로 ostack 에 push 하지 않고 number 에 부호를 설정해서 rstack 에 push 합니다. ( unary 연산자 위치에 * / 가 오는것은 오류 입니다. )

  4. 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
}