You may be able to convert binary to decimal, octal, or hex in your head but it seems that you can’t do simple arithmetic anymore and you can never find a calculator when you need one.
What to do?
Create a calculator using shell arithmetic and RPN notation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#!/usr/bin/env bash # cookbook filename: rpncalc # # simple RPN command line (integer) calculator # # takes the arguments and computes with them # of the form a b op # allow the use of x instead of * # # error check our argument counts: if [ \( $# -lt 3 \) -o \( $(($# % 2)) -eq 0 \) ] then echo "usage: calc number number op [ number op ] ..." echo "use x or '*' for multiplication" exit 1 fi ANS=$(($1 ${3//x/*} $2)) shift 3 while [ $# -gt 0 ] do ANS=$((ANS ${2//x/*} $1)) shift 2 done echo $ANS |
The idea of RPN (or postfix) style of notation puts the operands (the numbers) first, followed by the operator.
If we are using RPN, we don’t write 5 + 4 but rather 5 4 + as our expression.
If you want to multiply the result by 2, then you just put 2 * on the end, so the whole expression would be 5 4 + 2 *, which is great for computers to parse because you can go left to right and never need parentheses.
The result of any operation becomes the first operand for the next expression.
In our simple bash calculator we will allow the use of lowercase x as a substitute for the multiplication symbol since * has special meaning to the shell.
But if you escape that special meaning by writing ‘*’ or \* we want that to work, too.
How do we error check the arguments? We will consider it an error if there are less than three arguments (we need two operands and one operator, e.g., 6 3 /).
There can be more than three arguments, but in that case there will always be an odd number (since we start with three and add two more, a second operand and the next operator, and so on, always adding two more; the valid number of arguments would be 3 or 5 or 7 or 9 or …).
We check that with the expression:
1 |
$(($# % 2)) -eq 0 |
to see if the result is zero. The $(( )) says we’re doing some shell arithmetic inside.
We are using the % operator (called the remainder operator) to see if $# (which is the number of arguments) is divisible by 2 with no remainder (i.e., -eq 0).
Now that we know there are the right number of arguments, we can use them to compute the result. We write:
1 |
ANS=$(($1 ${3//x/*} $2)) |
which will compute the result and substitute the asterisk for the letter x at the same time.
When you invoke the script you give it an RPN expression on the command line, but the shell syntax for arithmetic is our normal (infix) notation.
So we can evaluate the expression inside of $(( )) but we have to switch the arguments around.
Ignoring the x-to-* substitution for the moment, you can see it is just:
1 |
ANS=$(($1 $3 $2)) |
which just moves the operator between the two operands. bash will substitute the parameters before doing the arithmetic evaluation, so if $1 is 5 and $2 is 4 and $3 is a + then after parameter substitution bash will have:
1 |
ANS=$((5 + 4)) |
and it will evaluate that and assign the result, 9, to ANS.
Done with those three arguments, we shift 3 to toss them and get the new arguments into play.
Since we’ve already checked that there are an odd number of arguments, if we have any more arguments to process, we will have at least two more (only 1 more and it would be an even number, since 3+1=4).
From that point on we loop, taking two arguments at a time.
The previous answer is the first operand, the next argument (now $1 as a result of the shift) is our second operand, and we put the operator inside $2 in between and evaluate it all much like before.
Once we are out of arguments, the answer is what we have in ANS.
One last word, about the substitution. ${2} would be how we refer to the second argument.
Though we often don’t bother with the {} and just write $2, we need themhere for the additional operations we will ask bash to perform on the argument.
We write ${2//x/*} to say that we want to replace or substitute (//) an x with (indicated by the next /) an * before returning the value of $2.
We could have written this in two steps by creating an extra variable:
1 2 |
OP=${2//x/*} ANS=$((ANS OP $1)) |
That extra variable can be helpful as you first begin to use these features of bash, but once you are familiar with these common expressions, you’ll find yourself putting them all together on one line (even though it’ll be harder to read).
Are you wondering why we didn’t write $ANS and $OP in the expression that does the evaluation?
We don’t have to use the $ on variable names inside of $(( )) expressions, except for the positional parameters (e.g., $1, $2).
The positional parameters need it to distinguish them from regular numbers (e.g., 1, 2).