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