맺음말

  • 쉘 스크립트는 프로그래밍 언어가 아닙니다. 시스템에 존재하는 명령들을 조합해서 실행하는데 도움을 주는 역할을 합니다. 반복문을 이용해 외부 명령을 실행하면 속도가 많이 느려질 수 있습니다.

  • shbash 에 비해 기능도 적지만 아직도 스크립트를 작성하는데 주로 사용됩니다. 이것은 sh 이 가진 기능 만으로도 충분히 쉘 스크립트 목적을 달성할 수 있기 때문입니다. 또한 특정 명령에서 쉘을 이용해 외부 명령을 실행할 때 bash 보다는 sh 을 사용하므로 sh 의 사용법도 함께 알아두어야겠습니다.

  • /bin, /usr/bin, /sbin, /usr/sbin 디렉토리에 있는 명령들을 알고 사용할 수 있어야겠습니다.


다음은 shell 스크립트를 활용한 산술연산 파서 입니다. 기본적으로 연산에 $(( )) 표현식을 사용하므로 소수 사용이나 지수를 음수로하는 거듭제곱 연산은 안됩니다. 그리고 거듭제곱 연산은 shell 방식을 따릅니다.

$ ./parse.sh '1 + (((((2 * 3) + 1) / 2) + 1) ** 2 * 2)'
33
$ echo $(( 1 + (((((2 * 3) + 1) / 2) + 1) ** 2 * 2) ))    # 맞는지 테스트
33
$ python3 -c 'print( 1 + (((((2 * 3) + 1) / 2) + 1) ** 2 * 2) )'
41.5

$ ./parse.sh '-5 ** 2'              $ ./parse.sh '1 + 2 * 3 ** (2 % 5) / 2'
25                                  10
$ python3 -c 'print(-5 ** 2)'       $ python3 -c 'print(1 + 2 * 3 ** (2 % 5) / 2)'
-25                                 10.0
$ cat parse.sh
#!/bin/bash

str=$1 toks=() stack=()
len=0 cnt=0 paren=0

############  tokenizer  ###########

for (( i = 0; i < ${#str}; i++ )); do
    tok=${str:i:1}
    case $tok in
        [0-9]) 
            while [[ ${str:i+1:1} == [0-9] ]]; do
                tok+=${str: ++i:1}
            done
            toks+=( "$tok" )
            ;;
        + | - | '*' | / | % | '(' | ')')
            if [[ $tok == '*' && ${str:i+1:1} == '*' ]]; then
                tok="**"; let i++
            fi
            toks+=( "$tok" )
            ;;
        [[:blank:]])
            ;;
        *)
            echo -e "\nError: tokenizer: \"$tok\"\n" >&2
            exit 1
    esac
done

len=${#toks[@]}

##############  stack  #############

push() 
{ 
    eval "$1[\${#$1[@]}]=$2"
}
pop()
{
    eval local len="\${#$1[@]}"
    if (( len > 0 )); then
        eval "$2=\${$1[len - 1]}"
        unset -v "$1[len - 1]"
        eval "$1=( \"\${$1[@]}\" )"
    else
        error 0
    fi
}

#####################################

error()
{
    echo -en "\nError: "
    for (( i = 0; i <= cnt + $1; i++ )); do
        echo -n "${toks[i]} "
    done
    echo -n "  <---"
    [[ paren -gt 0 && ${FUNCNAME[1]} == main ]] && echo -n ' ")"'
    echo
    exit 1
} >&2
add()
{
    local num1 num2 op next
    pop stack num1
    while [[ ${toks[cnt + 1]} == @(+|-) ]]; do
        op=${toks[++cnt]}
        let cnt++; pexp
        pop stack num2
        case ${toks[cnt + 1]} in
            '*' | / | %) next="mul" ;;
            '**') next="pow" ;;
        esac
        if [[ -n $next ]]; then
            push stack $num2
            $next
            pop stack num2
        fi
        num1=$(( num1 $op num2 ))       # 덧셈, 뺄셈 연산
    done
    push stack $num1
}
mul()
{
    local num1 num2 op
    pop stack num1
    while [[ ${toks[cnt + 1]} == @('*'|/|%) ]]; do
        op=${toks[++cnt]}
        let cnt++; pexp 
        pop stack num2
        if [[ ${toks[cnt + 1]} == '**' ]]; then
            push stack $num2
            pow
            pop stack num2
        fi
        num1=$(( num1 $op num2 ))       # 곱셈, 나눗셈, 나머지 연산
    done
    push stack $num1
}
pow()
{
    local num1 num2 op
    pop stack num1
    op=${toks[++cnt]}
    let cnt++; pexp 
    [[ ${toks[cnt + 1]} == '**' ]] && pow
    pop stack num2
    num1=$(( num1 $op num2 ))           # 거듭제곱 연산
    push stack $num1
}
pexp() 
{
    local minus num1
    for (( ; cnt < len; cnt++ )); do
        case ${toks[cnt]} in
            *[0-9]*)
                push stack "$minus${toks[cnt]}"
                return ;;
            + | -)
                if [[ -z $minus && ${toks[cnt]} == '-' ]] ||
                    [[ $minus == '-' && ${toks[cnt]} == '+' ]]; then
                    minus="-"
                else
                    minus=""
                fi ;;
            '(')
                let cnt++ paren++
                parse 
                pop stack num1
                push stack $(( $minus num1 ))
                return ;;
            *)
                error 0
        esac
    done
}
parse()
{
    pexp
    while (( cnt + 1 < len )); do
        case ${toks[cnt + 1]} in
            + | -) add ;;
            '*' | / | %) mul ;;
            '**') pow ;;
            ')') 
                let cnt++ paren--
                (( paren < 0 )) && error 0
                return ;;
            *) error 1
        esac
    done
}
parse
(( paren > 0 )) && error 0
pop stack res
echo $res

awk 로 작성한 역폴란드 표기법 (RPN, Reverse Polish Notation) 파서
https://mug896.github.io/awk-script/operators.html#quiz
C 언어를 이용한 recursive descent parser 와 LR parser
https://github.com/mug896/arithmetic-parser