Quick calculations in Bash
This is probably going to seem stupid, but every time I decide to do something in Bash that should only take me a minute, I end up losing an hour. I obsess over “better” ways to do everything. Not surprisingly, my motivation often wanes before I actually find the better, more elegant way, so these little projects end up lackluster. Fortunately, I end up learning all kinds of new, mostly unrelated things in the process, which is what happened this evening. It’s amazing to me that I use UNIX every day, and can still be blown away by a new trick every time I go digging on Stack Overflow. There are some very, very smart people out there.
Anyway, I had two goals: first, I wanted a basic calculator on the command line with the ability to optionally output a result with no newlines, so it would be easy to substitute within another command or pass to pbcopy
. Basic inline math. Then, as a side project to this side project, I wanted a tally-keeper that would allow me to just keep inputting numbers until I told it to stop, and then just add them all up. Nothing brilliant, and I had simple answers for both problems in about 10 minutes.
Built-in evaluators in Bash kind of stink, though, and they need special formatting and escaping to deal with floats and accommodate basic symbols. bc
seemed like the logical answer to that, and that’s what I’m using right now. I picked up a few good tips from the blog post that Allan Odgaard wrote when he was on a similar mission, both from Allan and from the commenters. I love the web.
So here’s what I’ve got for the calculator. It strips any character that’s not grade-school math out and normalizes spaces, which works well for my needs. You can actually just trash the whole second line and pass “$@” to bc if you want to use additional symbols. The second function in this block calls the first and strips newlines, good for integration elsewhere. You can also alias either of these to a question mark for fast access (inspired by Allan) with alias ?="calc"
in your .bash_profile
. That’s where these functions are likely to go as well. If you want to try them out, paste them into ~/.bash_profile, save it, run source ~/.bash_profile
in Terminal and then type calc 3+(5*2)
or whatever you like.
function calc() {
equat=$(echo ${@//[^0-9\.\+\/\*\(\)]/ }| sed 's/[ \t]*//g')
echo $equat|bc -lq
}
## just for fun, ccalc trims the newline
## and copies the result directly to the clipboard
function ccalc () {
calc $@|tr -d '\n'|pbcopy
}
Things get tricky if you need to control the number of decimal places you have in the result. You need to pass additional commands to bc
, which appears to require some heredoc magic. This works for me (the scale=4 sets it to four decimal places):
function calc2() {
bc -l << EOF
scale=4
$@
quit
EOF
}
Next up is tally()
, a function that does what it says. You can either pass it a list of numbers, or run it and let it go into interactive mode. It will let you type numbers separated by returns, spaces or a combination, as long as there’s a separation. When you press “=” it adds up all of the numbers you’ve entered. I wanted it for when people start giving me numbers and amounts on the phone. And yes, I can do it in LaunchBar (et al), but like I said, sometimes I just get a bug to do it the hard way. The only thing I hate about this implementation is that you can’t use the default mapping of the Backspace key, as everything is interpreted raw. You have to run read
in readline mode to avoid this, but that means you can’t use Enter anymore. That’s the kind of thing that makes me spend an hour trying to solve it. This version is my compromise; you can try it with the “-e” argument and see if you prefer backspace to enter. Same installation: paste it, source and run it.
function tally() {
if [[ $# == 0 ]]; then
echo "Enter numbers, press = for sum:"
read -d "="
input=$REPLY
echo "================"
else
input="$@"
fi
sum=0
for i in $input; do
sum=$sum+$i
done
echo $sum|bc -l
}
And that, ladies and gentleman, is your dose of stupid Bash tricks for the evening. Good night.