mirror of
git://git.code.sf.net/p/zsh/code
synced 2025-09-17 15:01:40 +02:00
404 lines
10 KiB
Bash
404 lines
10 KiB
Bash
#!/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 $<line-no>, 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: -#<base> is the same as a line containing just `[#<base>],
|
|
# similarly -##<base>; 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
|
|
|
|
zcalc_show_value() {
|
|
if [[ -n $base ]]; then
|
|
print -- $(( $base $1 ))
|
|
elif [[ $1 = *.* ]] || (( outdigits )); then
|
|
if [[ -z $forms[outform] ]]; then
|
|
print -- $(( $1 ))
|
|
else
|
|
printf "$forms[outform]\n" $outdigits $1
|
|
fi
|
|
else
|
|
printf "%d\n" $1
|
|
fi
|
|
}
|
|
|
|
# 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 rpn_mode matched show_stack i
|
|
integer max_stack
|
|
local -a expressions stack match mbegin mend
|
|
|
|
# We use our own history file with an automatic pop on exit.
|
|
history -ap "${ZDOTDIR:-$HOME}/.zcalc_history"
|
|
|
|
forms=( '%2$g' '%.*g' '%.*f' '%.*E' '')
|
|
|
|
local mathfuncs
|
|
if zmodload -i zsh/mathfunc 2>/dev/null; then
|
|
zmodload -P mathfuncs -FL zsh/mathfunc
|
|
mathfuncs="("${(j.|.)${mathfuncs##f:}}")"
|
|
fi
|
|
autoload -Uz zmathfuncdef
|
|
|
|
if (( ! ${+ZCALCPROMPT} )); then
|
|
typeset -g ZCALCPROMPT="%1v> "
|
|
fi
|
|
|
|
# Supply some constants.
|
|
float PI E
|
|
(( PI = 4 * atan(1), E = exp(1) ))
|
|
|
|
if [[ -f "${ZDOTDIR:-$HOME}/.zcalcrc" ]]; then
|
|
. "${ZDOTDIR:-$HOME}/.zcalcrc" || return 1
|
|
fi
|
|
|
|
# Process command line
|
|
while [[ -n $1 && $1 = -(|[#-]*|f|e|r(<->|)) ]]; 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 ));
|
|
;;
|
|
(r) # RPN mode.
|
|
(( rpn_mode = 1 ))
|
|
ZCALC_ACTIVE=rpn
|
|
if [[ $optlist = (#b)(<->)* ]]; then
|
|
(( show_stack = ${match[1]} ))
|
|
optlist=${optlist[${#match[1]}+1,-2]}
|
|
fi
|
|
;;
|
|
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
|
|
;;
|
|
|
|
(\$[[:IDENT:]]##)
|
|
# Display only, no calculation
|
|
line=${line##\$}
|
|
print -r -- ${(P)line}
|
|
line=
|
|
continue
|
|
;;
|
|
|
|
(*)
|
|
line=${${line##[[:blank:]]##}%%[[:blank:]]##}
|
|
if (( rpn_mode )); then
|
|
matched=1
|
|
case $line in
|
|
(=)
|
|
if (( ${#stack} < 1 )); then
|
|
print -r -- "${line}: not enough values on stack" >&2
|
|
line=
|
|
continue
|
|
fi
|
|
ans=${stack[1]}
|
|
;;
|
|
|
|
(+|-|\^|\||\&|\*|\*\*|/)
|
|
# Operators with two arguments
|
|
if (( ${#stack} < 2 )); then
|
|
print -r -- "${line}: not enough values on stack" >&2
|
|
line=
|
|
continue
|
|
fi
|
|
eval "(( ans = \${stack[2]} $line \${stack[1]} ))"
|
|
shift 2 stack
|
|
;;
|
|
|
|
(ldexp|jn|yn|scalb)
|
|
# Functions with two arguments
|
|
if (( ${#stack} < 2 )); then
|
|
print -r -- "${line}: not enough values on stack" >&2
|
|
line=
|
|
continue
|
|
fi
|
|
eval "(( ans = ${line}(\${stack[2]},\${stack[1]}) ))"
|
|
shift 2 stack
|
|
;;
|
|
|
|
(${~mathfuncs})
|
|
# Functions with a single argument.
|
|
# This is actually a superset, but we should have matched
|
|
# any that shouldn't be in it in previous cases.
|
|
if (( ${#stack} < 1 )); then
|
|
print -r -- "${line}: not enough values on stack" >&2
|
|
line=
|
|
continue
|
|
fi
|
|
eval "(( ans = ${line}(\${stack[1]}) ))"
|
|
shift stack
|
|
;;
|
|
|
|
(*)
|
|
# Treat as expression evaluating to new value to go on stack.
|
|
matched=0
|
|
;;
|
|
esac
|
|
else
|
|
matched=0
|
|
fi
|
|
if (( ! matched )); then
|
|
# 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
|
|
fi
|
|
argv[num++]=$ans
|
|
psvar[1]=$num
|
|
stack=($ans $stack)
|
|
;;
|
|
esac
|
|
if (( show_stack )); then
|
|
(( max_stack = (show_stack > ${#stack}) ? ${#stack} : show_stack ))
|
|
for (( i = max_stack; i > 0; i-- )); do
|
|
printf "%3d: " $i
|
|
zcalc_show_value ${stack[i]}
|
|
done
|
|
else
|
|
zcalc_show_value $ans
|
|
fi
|
|
line=
|
|
done
|
|
|
|
return 0
|