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# With the option -e, the arguments are evaluated as if entered 89# interactively. So, for example: 90# zcalc -e -\#16 -e 1055 91# prints 92# 0x41f 93# Any number of expressions may be given and they are evaluated 94# sequentially just as if read automatically. 95 96emulate -L zsh 97setopt extendedglob 98 99# TODO: make local variables that shouldn't be visible in expressions 100# begin with _. 101local line ans base defbase forms match mbegin mend psvar optlist opt arg 102local compcontext="-zcalc-line-" 103integer num outdigits outform=1 expression_mode 104local -a expressions 105 106# We use our own history file with an automatic pop on exit. 107history -ap "${ZDOTDIR:-$HOME}/.zcalc_history" 108 109forms=( '%2$g' '%.*g' '%.*f' '%.*E' '') 110 111zmodload -i zsh/mathfunc 2>/dev/null 112autoload -Uz zmathfuncdef 113 114: ${ZCALCPROMPT="%1v> "} 115 116# Supply some constants. 117float PI E 118(( PI = 4 * atan(1), E = exp(1) )) 119 120# Process command line 121while [[ -n $1 && $1 = -(|[#-]*|f|e) ]]; do 122 optlist=${1[2,-1]} 123 shift 124 [[ $optlist = (|-) ]] && break 125 while [[ -n $optlist ]]; do 126 opt=${optlist[1]} 127 optlist=${optlist[2,-1]} 128 case $opt in 129 ('#') # Default base 130 if [[ -n $optlist ]]; then 131 arg=$optlist 132 optlist= 133 elif [[ -n $1 ]]; then 134 arg=$1 135 shift 136 else 137 print -- "-# requires an argument" >&2 138 return 1 139 fi 140 if [[ $arg != (|\#)[[:digit:]]## ]]; then 141 print -- "-# requires a decimal number as an argument" >&2 142 return 1 143 fi 144 defbase="[#${arg}]" 145 ;; 146 (f) # Force floating point operation 147 setopt forcefloat 148 ;; 149 (e) # Arguments are expressions 150 (( expression_mode = 1 )); 151 ;; 152 esac 153 done 154done 155 156if (( expression_mode )); then 157 expressions=("$@") 158 argv=() 159fi 160 161for (( num = 1; num <= $#; num++ )); do 162 # Make sure all arguments have been evaluated. 163 # The `$' before the second argv forces string rather than numeric 164 # substitution. 165 (( argv[$num] = $argv[$num] )) 166 print "$num> $argv[$num]" 167done 168 169psvar[1]=$num 170local prev_line cont_prompt 171while (( expression_mode )) || 172 vared -cehp "${cont_prompt}${ZCALCPROMPT}" line; do 173 if (( expression_mode )); then 174 (( ${#expressions} )) || break 175 line=$expressions[1] 176 shift expressions 177 fi 178 if [[ $line = (|*[^\\])('\\')#'\' ]]; then 179 prev_line+=$line[1,-2] 180 cont_prompt="..." 181 line= 182 continue 183 fi 184 line="$prev_line$line" 185 prev_line= 186 cont_prompt= 187 # Test whether there are as many open as close 188 # parentheses in the line so far. 189 if [[ ${#line//[^\(]} -gt ${#line//[^\)]} ]]; then 190 prev_line+=$line 191 cont_prompt="..." 192 line= 193 continue 194 fi 195 [[ -z $line ]] && break 196 # special cases 197 # Set default base if `[#16]' or `[##16]' etc. on its own. 198 # Unset it if `[#]' or `[##]'. 199 if [[ $line = (#b)[[:blank:]]#('[#'(\#|)(<->|)']')[[:blank:]]#(*) ]]; then 200 if [[ -z $match[4] ]]; then 201 if [[ -z $match[3] ]]; then 202 defbase= 203 else 204 defbase=$match[1] 205 fi 206 print -s -- $line 207 print -- $(( ${defbase} ans )) 208 line= 209 continue 210 else 211 base=$match[1] 212 fi 213 else 214 base=$defbase 215 fi 216 217 print -s -- $line 218 219 line="${${line##[[:blank:]]#}%%[[:blank:]]#}" 220 case "$line" in 221 # Escapes begin with a colon 222 (:(\\|)\!*) 223 # shell escape: handle completion's habit of quoting the ! 224 eval ${line##:(\\|)\![[:blank:]]#} 225 line= 226 continue 227 ;; 228 229 ((:|)q) 230 # Exit 231 return 0 232 ;; 233 234 ((:|)norm) # restore output format to default 235 outform=1 236 ;; 237 238 ((:|)sci[[:blank:]]#(#b)(<->)(#B)) 239 outdigits=$match[1] 240 outform=2 241 ;; 242 243 ((:|)fix[[:blank:]]#(#b)(<->)(#B)) 244 outdigits=$match[1] 245 outform=3 246 ;; 247 248 ((:|)eng[[:blank:]]#(#b)(<->)(#B)) 249 outdigits=$match[1] 250 outform=4 251 ;; 252 253 (:raw) 254 outform=5 255 ;; 256 257 ((:|)local([[:blank:]]##*|)) 258 eval $line 259 line= 260 continue 261 ;; 262 263 ((function|:f(unc(tion|)|))[[:blank:]]##(#b)([^[:blank:]]##)(|[[:blank:]]##([^[:blank:]]*))) 264 zmathfuncdef $match[1] $match[3] 265 line= 266 continue 267 ;; 268 269 (:*) 270 print "Unrecognised escape" 271 line= 272 continue 273 ;; 274 275 (*) 276 # Latest value is stored as a string, because it might be floating 277 # point or integer --- we don't know till after the evaluation, and 278 # arrays always store scalars anyway. 279 # 280 # Since it's a string, we'd better make sure we know which 281 # base it's in, so don't change that until we actually print it. 282 eval "ans=\$(( $line ))" 283 # on error $ans is not set; let user re-edit line 284 [[ -n $ans ]] || continue 285 argv[num++]=$ans 286 psvar[1]=$num 287 ;; 288 esac 289 if [[ -n $base ]]; then 290 print -- $(( $base $ans )) 291 elif [[ $ans = *.* ]] || (( outdigits )); then 292 if [[ -z $forms[outform] ]]; then 293 print -- $(( $ans )) 294 else 295 printf "$forms[outform]\n" $outdigits $ans 296 fi 297 else 298 printf "%d\n" $ans 299 fi 300 line= 301done 302 303return 0 304