Navigation: Homepage | xmlgawk | Buchkritik | Sitemap

An Interactive Calculator

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 ))
 16
Hey, we have a simple numeric expression evaluator! Lets make a tool out of this feature.

Sketch of solution

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

Iteration 1: line editing & history

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)
 1
We named our calculator ic, like interactive calc.

Iteration 2: comment lines

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.

Iteration 3: Error Handling

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"

Iteration 4: output formatting

Lets think of using binary operation:

 ic> 2#1010<<4
 160
Wouldnt 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
 0000000000000110
In this example we will print a 16 digit base 2 decimal.

So, how do we do that:

 eval 'printf "${format}\n" "$((REPLY))"'

Iteration 5: finish

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.

Bonus: Shell Escape

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 #
 ...

last modified $Date: 2006/11/04 17:20:58 $