1#!/bin/zsh -i 2# 3# Zsh calculator. Understands most ordinary arithmetic expressions. 4# Line editing and history are available. A blank line or `q' quits. 5# 6# Runs as a script or a function. If used as a function, the history 7# is remembered for reuse in a later call (and also currently in the 8# shell's own history). There are various problems using this as a 9# script, so a function is recommended. 10# 11# The prompt shows a number for the current line. The corresponding 12# result can be referred to with $<line-no>, e.g. 13# 1> 32 + 10 14# 42 15# 2> $1 ** 2 16# 1764 17# The set of remembered numbers is primed with anything given on the 18# command line. For example, 19# zcalc '2 * 16' 20# 1> 32 # printed by function 21# 2> $1 + 2 # typed by user 22# 34 23# 3> 24# Here, 32 is stored as $1. This works in the obvious way for any 25# number of arguments. 26# 27# If the mathfunc library is available, probably understands most system 28# mathematical functions. The left parenthesis must be adjacent to the 29# end of the function name, to distinguish from shell parameters 30# (translation: to prevent the maintainers from having to write proper 31# lookahead parsing). For example, 32# 1> sqrt(2) 33# 1.4142135623730951 34# is right, but `sqrt (2)' will give you an error. 35# 36# You can do things with parameters like 37# 1> pi = 4.0 * atan(1) 38# too. These go into global parameters, so be careful. You can declare 39# local variables, however: 40# 1> local pi 41# but note this can't appear on the same line as a calculation. Don't 42# use the variables listed in the `local' and `integer' lines below 43# (translation: I can't be bothered to provide a sandbox). 44# 45# You can declare or delete math functions (implemented via zmathfuncdef): 46# 1> function cube $1 * $1 * $1 47# This has a single compulsory argument. Note the function takes care of 48# the punctuation. To delete the function, put nothing (at all) after 49# the function name: 50# 1> function cube 51# 52# Some constants are already available: (case sensitive as always): 53# PI pi, i.e. 3.1415926545897931 54# E e, i.e. 2.7182818284590455 55# 56# You can also change the output base. 57# 1> [#16] 58# 1> 59# Changes the default output to hexadecimal with numbers preceded by `16#'. 60# Note the line isn't remembered. 61# 2> [##16] 62# 2> 63# Change the default output base to hexadecimal with no prefix. 64# 3> [#] 65# Reset the default output base. 66# 67# This is based on the builtin feature that you can change the output base 68# of a given expression. For example, 69# 1> [##16] 32 + 20 / 2 70# 2A 71# 2> 72# prints the result of the calculation in hexadecimal. 73# 74# You can't change the default input base, but the shell allows any small 75# integer as a base: 76# 1> 2#1111 77# 15 78# 2> [##13] 13#6 * 13#9 79# 42 80# and the standard C-like notation with a leading 0x for hexadecimal is 81# also understood. However, leading 0 for octal is not understood --- it's 82# too confusing in a calculator. Use 8#777 etc. 83# 84# Options: -#<base> is the same as a line containing just `[#<base>], 85# similarly -##<base>; they set the default output base, with and without 86# a base discriminator in front, respectively. 87# 88# 89# To do: 90# - separate zcalc history from shell history using arrays --- or allow 91# zsh to switch internally to and from array-based history. 92 93emulate -L zsh 94setopt extendedglob 95 96# TODO: make local variables that shouldn't be visible in expressions 97# begin with _. 98local line ans base defbase forms match mbegin mend psvar optlist opt arg 99local compcontext="-zcalc-line-" 100integer num outdigits outform=1 101 102# We use our own history file with an automatic pop on exit. 103history -ap "${ZDOTDIR:-$HOME}/.zcalc_history" 104 105forms=( '%2$g' '%.*g' '%.*f' '%.*E' '') 106 107zmodload -i zsh/mathfunc 2>/dev/null 108autoload -Uz zmathfuncdef 109 110: ${ZCALCPROMPT="%1v> "} 111 112# Supply some constants. 113float PI E 114(( PI = 4 * atan(1), E = exp(1) )) 115 116# Process command line 117while [[ -n $1 && $1 = -(|[#-]*) ]]; do 118 optlist=${1[2,-1]} 119 shift 120 [[ $optlist = (|-) ]] && break 121 while [[ -n $optlist ]]; do 122 opt=${optlist[1]} 123 optlist=${optlist[2,-1]} 124 case $opt in 125 ('#') # Default base 126 if [[ -n $optlist ]]; then 127 arg=$optlist 128 optlist= 129 elif [[ -n $1 ]]; then 130 arg=$1 131 shift 132 else 133 print "-# requires an argument" >&2 134 return 1 135 fi 136 if [[ $arg != (|\#)[[:digit:]]## ]]; then 137 print - "-# requires a decimal number as an argument" >&2 138 return 1 139 fi 140 defbase="[#${arg}]" 141 ;; 142 esac 143 done 144done 145 146for (( num = 1; num <= $#; num++ )); do 147 # Make sure all arguments have been evaluated. 148 # The `$' before the second argv forces string rather than numeric 149 # substitution. 150 (( argv[$num] = $argv[$num] )) 151 print "$num> $argv[$num]" 152done 153 154psvar[1]=$num 155while vared -cehp "${ZCALCPROMPT}" line; do 156 [[ -z $line ]] && break 157 # special cases 158 # Set default base if `[#16]' or `[##16]' etc. on its own. 159 # Unset it if `[#]' or `[##]'. 160 if [[ $line = (#b)[[:blank:]]#('[#'(\#|)(<->|)']')[[:blank:]]#(*) ]]; then 161 if [[ -z $match[4] ]]; then 162 if [[ -z $match[3] ]]; then 163 defbase= 164 else 165 defbase=$match[1] 166 fi 167 print -s -- $line 168 line= 169 continue 170 else 171 base=$match[1] 172 fi 173 else 174 base=$defbase 175 fi 176 177 print -s -- $line 178 179 line="${${line##[[:blank:]]#}%%[[:blank:]]#}" 180 case "$line" in 181 # Escapes begin with a colon 182 (:(\\|)\!*) 183 # shell escape: handle completion's habit of quoting the ! 184 eval ${line##:(\\|)\![[:blank:]]#} 185 line= 186 continue 187 ;; 188 189 ((:|)q) 190 # Exit 191 return 0 192 ;; 193 194 ((:|)norm) # restore output format to default 195 outform=1 196 ;; 197 198 ((:|)sci[[:blank:]]#(#b)(<->)(#B)) 199 outdigits=$match[1] 200 outform=2 201 ;; 202 203 ((:|)fix[[:blank:]]#(#b)(<->)(#B)) 204 outdigits=$match[1] 205 outform=3 206 ;; 207 208 ((:|)eng[[:blank:]]#(#b)(<->)(#B)) 209 outdigits=$match[1] 210 outform=4 211 ;; 212 213 (:raw) 214 outform=5 215 ;; 216 217 ((:|)local([[:blank:]]##*|)) 218 eval $line 219 line= 220 continue 221 ;; 222 223 ((:|)function[[:blank:]]##(#b)([^[:blank:]]##)(|[[:blank:]]##([^[:blank:]]*))) 224 zmathfuncdef $match[1] $match[3] 225 line= 226 continue 227 ;; 228 229 (:*) 230 print "Unrecognised escape" 231 line= 232 continue 233 ;; 234 235 (*) 236 # Latest value is stored as a string, because it might be floating 237 # point or integer --- we don't know till after the evaluation, and 238 # arrays always store scalars anyway. 239 # 240 # Since it's a string, we'd better make sure we know which 241 # base it's in, so don't change that until we actually print it. 242 eval "ans=\$(( $line ))" 243 # on error $ans is not set; let user re-edit line 244 [[ -n $ans ]] || continue 245 argv[num++]=$ans 246 psvar[1]=$num 247 ;; 248 esac 249 if [[ -n $base ]]; then 250 print -- $(( $base $ans )) 251 elif [[ $ans = *.* ]] || (( outdigits )); then 252 if [[ -z $forms[outform] ]]; then 253 print -- $(( $ans )) 254 else 255 printf "$forms[outform]\n" $outdigits $ans 256 fi 257 else 258 printf "%d\n" $ans 259 fi 260 line= 261done 262 263return 0 264