In the book LearningTheKornShell I stumbled across a nice feature of $((...)), which evaluates numeric expressions in combination with variables. If the variable contains an expression, it will be evaluated automatically. Lets look into an example:
$ e=3*4 $ print $(( e + 4 )) 16Hey, we have a simple numeric expression evaluator! Lets make a tool out of this feature.
In contrast of my general rule (dont write interactive shell scripts) I will implement an interactice calculator. The heart of the script will be:
while read do print $(( REPLY )) done
Interactive shell scripts should support input editing (with the user preferred emacs or vi bindings) and a per-user history file. The Korn Shell supports this out-of-the-box.
SCRIPT=${0##*/}
case "${VISUAL:-${EDITOR:-nomacs}}" in
*macs) set -o gmacs ;;
vi*) set -o vi ;;
esac
HISTSIZE=50
HISTFILE=~/.${SCRIPT}_history
while read -s REPLY?"$SCRIPT> "
do
print $(( REPLY ))
done
Based on the user environment variables VISUAL or EDITOR we try
to determine whether emacs or vi bindings should be used. If nothing
is set, we will use emacs, because naive users expects the cursor keys
to work.
Line editing is supported by the shell builtin read, if we use the prompt option ?.... after the variable name. If we also want to store the input into a history file, we have to use the option -s. If we do not set a separate HISTFILE, read will use the current shell history file. But we do not want to store the numerical expressions in our normal history file, we prefer a persistent script specific file.
This solution is quite powerfull, besides all the floating point operations of the Korn Shell, you can also use variables:
$ ic ic> exp(2) 7.38905609893 ic> 4*atan(1) 3.14159265359 ic> pi=4*atan(1) 3.14159265359 ic> sin(0.5*pi) 1We named our calculator ic, like interactive calc.
As you have already seen, this simple script is quite powerful. But every program, which reads some user input, should also accept commentary lines. So we will also implemetent such a feature:
while read -s REPLY?"$SCRIPT> " do # ignore lines starting with # [[ $REPLY == \#* ]] && continue ...
This is also very nice for the '#' command in the vi-mode.
Try the following with the current script:
$ ic ic> # an erronous input ic> 2 +* 3 /usr/local/bin/ic[27]: eval: line 1: 2 +* 3: arithmetic syntax error $Ooops, our script terminated! Thats not nice, the script should give an error message and continue with reading user input. Where is the problem?
Thats not so easy to answer. The problem happens inside the $((...)) and we cannot check beforehand, whether the expression in $REPLY is syntactically correct. Here an old (but seldom used) friend finds a use: eval. Normally you will not need it, but here it catches the error:
eval 'print $(( REPLY ))'
Lets try it:
ic> 2+*3 /usr/local/bin/ic[28]: eval: line 1: 2+*3: arithmetic syntax error ic>
To generate a nicer error message, we will further modify the script:
eval 'print $(( REPLY ))' 2>/dev/null || print "error"
Lets think of using binary operation:
ic> 2#1010<<4 160Wouldnt it be nice to print the result also as a binary or hex number?
Of course, we can do this. We will use the printf builtin instead of print. Printf accepts a format string and we will introduce a second special linestart character. A '%' will be used to switch the output format.
Lets come back to our example of printing a result as binary numbers:
ic> %.16.2d ic> 2*3 0000000000000110In this example we will print a 16 digit base 2 decimal.
So, how do we do that:
eval 'printf "${format}\n" "$((REPLY))"'
As last step we want to finish our little script by introducing a variable, which contains the last result. This convenience feature will make it easier to iterate or develop more complex expressions.
Here the final script:
#!/bin/ksh93
# name of program
SCRIPT=${0##*/}
# select key bindings based on users editor (defaults to emacs)
case "${VISUAL:-${EDITOR:-nomacs}}" in
*macs) set -o gmacs ;;
vi*) set -o vi ;;
esac
# save input in a separate history file
HISTSIZE=50
HISTFILE=~/.${SCRIPT}_history
# define some useful constants
float e=$(( exp(1) ))
float pi=$(( 4.0 * atan(1.0) ))
# last result
float last=0.0
# default output format
format=%g
# main loop
# read with prompt and save input in HISTFILE
while read -s REPLY?"$SCRIPT> "
do
# handle special commands, given in first column
case "$REPLY" in
\#*) continue;; # ignore lines starting with #
%*) format=$REPLY;& # switch outputformat and fall through
"") REPLY=$last;; # empty line reprints last result
esac
# using eval will result in ignoring of syntax errors
if eval 'last=$(( REPLY ))' 2>/dev/null
then
printf "${format}\n" "$last"
else
print "error"
fi
done
Thats it! Have fun.
Well, I like to have shell escapes, so we will implement it. Lines starting with '!' will be executed as shell commands. Its only one line of code, inserted before the comment handling line:
...
case "$REPLY" in
!*) eval "${REPLY:1}" ;& # shell escape and fall through
\#*) continue;; # ignore lines starting with #
...