맺음말

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

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

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


다음은 shell 스크립트를 활용한 산술연산 파서 입니다.

$ ./parse.sh '1 + (((((2 * 3) + 1) / 2) + 1) ** 2 * 2)'
41.5
# 맞는지 테스트
$ python3 -c 'print( 1 + (((((2 * 3) + 1) / 2) + 1) ** 2 * 2) )'
41.5
#!/usr/bin/bash
tokens=() pos=0 value=

error() {
    echo "Error: $*" >&2
    exit 1
}
tokenize() {
    local input=$1 token
    while [[ -n $input ]]; do
        # Skip whitespace
        if [[ $input =~ ^[[:space:]]+ ]]; then
            input=${input#"${BASH_REMATCH[0]}"}
            continue
        fi
        # Number: integer or floating point
        if [[ $input =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+) ]]; then
            token=${BASH_REMATCH[1]}
            tokens+=( "$token" )
            input=${input#"$token"}
            continue
        fi
        if [[ $input == "**"* ]]; then
            tokens+=( "**" )
            input=${input:2}
            continue
        fi
        token=${input:0:1}
        case $token in
            '+' | '-' | '*' | '/' | '%' | '(' | ')')
                tokens+=( "$token" )
                input=${input:1}
                ;;
            *)
                error "Invalid character: $token"
                ;;
        esac
    done
    tokens+=( "EOF" )
}
peek() {
    echo "${tokens[pos]}"
}
consume() {
    pos=$((pos + 1))
}
calc() {
    local a=$1 op=$2 b=$3
    awk -v a="$a" -v b="$b" -v op="$op" '
    BEGIN {
        if (op == "+")      r = a + b
        else if (op == "-") r = a - b
        else if (op == "*") r = a * b
        else if (op == "/") {
            if (b == 0) {
                print "Error: division by zero" > "/dev/stderr"
                exit 1
            }
            r = a / b
        }
        else if (op == "%") {
            if (b == 0) {
                print "Error: modulo by zero" > "/dev/stderr"
                exit 1
            }
            r = a % b
        }
        else if (op == "**") r = a ^ b
        else {
            print "Unknown operator" > "/dev/stderr"
            exit 1
        }
        printf "%.17g\n", r
    }'
}
# ------------------------------------------
# Grammar:
# expression := term (('+' | '-') term)*
# term       := unary (('*' | '/') unary)*
# unary      := ('+' | '-') unary | power
# power      := primary ('**' unary)?
# primary    := NUMBER | '(' expression ')'
# ------------------------------------------
parse_expression() {
    parse_term
    local result=$value op
    while [[ $(peek) == @('+'|'-') ]]; do
        op=$(peek)
        consume
        parse_term
        result=$(calc "$result" "$op" "$value")
    done
    value=$result
}
parse_term() {
    parse_unary
    local result=$value op
    while [[ $(peek) == @('*'|'/'|'%') ]]; do
        op=$(peek)
        consume
        parse_unary
        result=$(calc "$result" "$op" "$value")
    done
    value=$result
}
parse_unary() {
    local op
    case $(peek) in
        '+')
            consume
            parse_unary
            ;;
        '-')
            consume
            parse_unary
            value=$(calc 0 - "$value")
            ;;
        *)
            parse_power
            ;;
    esac
}
parse_power() {
    parse_primary
    local result=$value
    if [[ $(peek) == '**' ]]; then
        consume
        parse_unary
        result=$(calc "$result" "**" "$value")
    fi
    value=$result
}
parse_primary() {
    local token
    token=$(peek)
    case $token in
        '(')
            consume
            parse_expression
            if [[ $(peek) != ')' ]]; then
                error "Missing closing parenthesis"
            fi
            consume
            ;;
        'EOF')
            error "Unexpected end of expression"
            ;;
        *)
            if [[ $token =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]]; then
                value=$token
                consume
            else
                error "Unexpected token: $token"
            fi
            ;;
    esac
}

if [[ $# -ne 1 ]]; then
    echo "Usage: $0 'expression'" >&2
    exit 1
fi
tokenize "$1"
parse_expression
if [[ $(peek) != EOF ]]; then
    error "Unexpected token: $(peek)"
fi
echo "$value"

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