#!/bin/zsh -i # # Zsh calculator. Understands most ordinary arithmetic expressions. # Line editing and history are available. A blank line or `q' quits. # # Runs as a script or a function. If used as a function, the history # is remembered for reuse in a later call (and also currently in the # shell's own history). There are various problems using this as a # script, so a function is recommended. # # The prompt shows a number for the current line. The corresponding # result can be referred to with $, e.g. # 1> 32 + 10 # 42 # 2> $1 ** 2 # 1764 # The set of remembered numbers is primed with anything given on the # command line. For example, # zcalc '2 * 16' # 1> 32 # printed by function # 2> $1 + 2 # typed by user # 34 # 3> # Here, 32 is stored as $1. This works in the obvious way for any # number of arguments. # # If the mathfunc library is available, probably understands most system # mathematical functions. The left parenthesis must be adjacent to the # end of the function name, to distinguish from shell parameters # (translation: to prevent the maintainers from having to write proper # lookahead parsing). For example, # 1> sqrt(2) # 1.4142135623730951 # is right, but `sqrt (2)' will give you an error. # # You can do things with parameters like # 1> pi = 4.0 * atan(1) # too. These go into global parameters, so be careful. You can declare # local variables, however: # 1> local pi # but note this can't appear on the same line as a calculation. Don't # use the variables listed in the `local' and `integer' lines below # (translation: I can't be bothered to provide a sandbox). # # You can declare or delete math functions (implemented via zmathfuncdef): # 1> function cube $1 * $1 * $1 # This has a single compulsory argument. Note the function takes care of # the punctuation. To delete the function, put nothing (at all) after # the function name: # 1> function cube # # Some constants are already available: (case sensitive as always): # PI pi, i.e. 3.1415926545897931 # E e, i.e. 2.7182818284590455 # # You can also change the output base. # 1> [#16] # 1> # Changes the default output to hexadecimal with numbers preceded by `16#'. # Note the line isn't remembered. # 2> [##16] # 2> # Change the default output base to hexadecimal with no prefix. # 3> [#] # Reset the default output base. # # This is based on the builtin feature that you can change the output base # of a given expression. For example, # 1> [##16] 32 + 20 / 2 # 2A # 2> # prints the result of the calculation in hexadecimal. # # You can't change the default input base, but the shell allows any small # integer as a base: # 1> 2#1111 # 15 # 2> [##13] 13#6 * 13#9 # 42 # and the standard C-like notation with a leading 0x for hexadecimal is # also understood. However, leading 0 for octal is not understood --- it's # too confusing in a calculator. Use 8#777 etc. # # Options: -# is the same as a line containing just `[#], # similarly -##; they set the default output base, with and without # a base discriminator in front, respectively. # # With the option -e, the arguments are evaluated as if entered # interactively. So, for example: # zcalc -e -\#16 -e 1055 # prints # 0x41f # Any number of expressions may be given and they are evaluated # sequentially just as if read automatically. emulate -L zsh setopt extendedglob # For testing in ZLE functions. local ZCALC_ACTIVE=1 # TODO: make local variables that shouldn't be visible in expressions # begin with _. local line ans base defbase forms match mbegin mend psvar optlist opt arg local compcontext="-zcalc-line-" integer num outdigits outform=1 expression_mode local -a expressions # We use our own history file with an automatic pop on exit. history -ap "${ZDOTDIR:-$HOME}/.zcalc_history" forms=( '%2$g' '%.*g' '%.*f' '%.*E' '') zmodload -i zsh/mathfunc 2>/dev/null autoload -Uz zmathfuncdef : ${ZCALCPROMPT="%1v> "} # Supply some constants. float PI E (( PI = 4 * atan(1), E = exp(1) )) # Process command line while [[ -n $1 && $1 = -(|[#-]*|f|e) ]]; do optlist=${1[2,-1]} shift [[ $optlist = (|-) ]] && break while [[ -n $optlist ]]; do opt=${optlist[1]} optlist=${optlist[2,-1]} case $opt in ('#') # Default base if [[ -n $optlist ]]; then arg=$optlist optlist= elif [[ -n $1 ]]; then arg=$1 shift else print -- "-# requires an argument" >&2 return 1 fi if [[ $arg != (|\#)[[:digit:]]## ]]; then print -- "-# requires a decimal number as an argument" >&2 return 1 fi defbase="[#${arg}]" ;; (f) # Force floating point operation setopt forcefloat ;; (e) # Arguments are expressions (( expression_mode = 1 )); ;; esac done done if (( expression_mode )); then expressions=("$@") argv=() fi for (( num = 1; num <= $#; num++ )); do # Make sure all arguments have been evaluated. # The `$' before the second argv forces string rather than numeric # substitution. (( argv[$num] = $argv[$num] )) print "$num> $argv[$num]" done psvar[1]=$num local prev_line cont_prompt while (( expression_mode )) || vared -cehp "${cont_prompt}${ZCALCPROMPT}" line; do if (( expression_mode )); then (( ${#expressions} )) || break line=$expressions[1] shift expressions fi if [[ $line = (|*[^\\])('\\')#'\' ]]; then prev_line+=$line[1,-2] cont_prompt="..." line= continue fi line="$prev_line$line" prev_line= cont_prompt= # Test whether there are as many open as close # parentheses in the line so far. if [[ ${#line//[^\(]} -gt ${#line//[^\)]} ]]; then prev_line+=$line cont_prompt="..." line= continue fi [[ -z $line ]] && break # special cases # Set default base if `[#16]' or `[##16]' etc. on its own. # Unset it if `[#]' or `[##]'. if [[ $line = (#b)[[:blank:]]#('[#'(\#|)((<->|)(|_|_<->))']')[[:blank:]]#(*) ]]; then if [[ -z $match[6] ]]; then if [[ -z $match[3] ]]; then defbase= else defbase=$match[1] fi print -s -- $line print -- $(( ${defbase} ans )) line= continue else base=$match[1] fi else base=$defbase fi print -s -- $line line="${${line##[[:blank:]]#}%%[[:blank:]]#}" case "$line" in # Escapes begin with a colon (:(\\|)\!*) # shell escape: handle completion's habit of quoting the ! eval ${line##:(\\|)\![[:blank:]]#} line= continue ;; ((:|)q) # Exit return 0 ;; ((:|)norm) # restore output format to default outform=1 ;; ((:|)sci[[:blank:]]#(#b)(<->)(#B)) outdigits=$match[1] outform=2 ;; ((:|)fix[[:blank:]]#(#b)(<->)(#B)) outdigits=$match[1] outform=3 ;; ((:|)eng[[:blank:]]#(#b)(<->)(#B)) outdigits=$match[1] outform=4 ;; (:raw) outform=5 ;; ((:|)local([[:blank:]]##*|)) eval $line line= continue ;; ((function|:f(unc(tion|)|))[[:blank:]]##(#b)([^[:blank:]]##)(|[[:blank:]]##([^[:blank:]]*))) zmathfuncdef $match[1] $match[3] line= continue ;; (:*) print "Unrecognised escape" line= continue ;; (*) # Latest value is stored as a string, because it might be floating # point or integer --- we don't know till after the evaluation, and # arrays always store scalars anyway. # # Since it's a string, we'd better make sure we know which # base it's in, so don't change that until we actually print it. eval "ans=\$(( $line ))" # on error $ans is not set; let user re-edit line [[ -n $ans ]] || continue argv[num++]=$ans psvar[1]=$num ;; esac if [[ -n $base ]]; then print -- $(( $base $ans )) elif [[ $ans = *.* ]] || (( outdigits )); then if [[ -z $forms[outform] ]]; then print -- $(( $ans )) else printf "$forms[outform]\n" $outdigits $ans fi else printf "%d\n" $ans fi line= done return 0