Bash gets a bad reputation for math, and honestly, it’s a little deserved — the shell’s built-in arithmetic is integer-only, and anything involving decimals requires reaching for an external tool. Once I understood exactly where Bash’s native arithmetic ends and where tools like bc and awk pick up the slack, building a genuinely useful calculator script became straightforward. This article walks through building one from a simple integer calculator up to a full interactive tool that handles decimals, and covers the internals of how Bash arithmetic actually works.
Bash’s Native Arithmetic
Bash supports integer arithmetic natively through the $(( )) syntax:
result=$((5 + 3))
echo "$result"
Output:
8
It supports the standard operators: +, -, *, /, % (modulo), and ** (exponentiation):
echo $((10 / 3)) # 3 (integer division, truncates)
echo $((10 % 3)) # 1 (remainder)
echo $((2 ** 8)) # 256
Notice that 10 / 3 returns 3, not 3.333... — Bash arithmetic is strictly integer-based and truncates any fractional result. This is the single biggest limitation you’ll hit building a calculator in pure Bash.
The let and expr Alternatives
Two older ways to do the same arithmetic:
let result=5+3
echo "$result"
result=$(expr 5 + 3)
echo "$result"
I rarely use either in new scripts — $(( )) is cleaner, safer, and is the modern standard. expr in particular requires spaces around every operator and is slower since it’s an external command rather than a shell built-in.
Step One: A Simple Integer Calculator
#!/usr/bin/env bash
set -euo pipefail
num1=$1
operator=$2
num2=$3
case "$operator" in
+) echo $((num1 + num2)) ;;
-) echo $((num1 - num2)) ;;
\*) echo $((num1 * num2)) ;;
/)
if [ "$num2" -eq 0 ]; then
echo "Error: division by zero" >&2
exit 1
fi
echo $((num1 / num2))
;;
%) echo $((num1 % num2)) ;;
*) echo "Unknown operator: $operator" >&2; exit 1 ;;
esac
Run it:
./calc.sh 10 + 5
./calc.sh 10 / 3
Output:
15
3
Note the \* in the case pattern — since * is a glob wildcard in case patterns, it needs to be escaped to be matched literally as the multiplication operator, otherwise it would match everything and always trigger that branch.
Step Two: Adding Decimal Support with bc
To handle real decimal math, I bring in bc, the classic Unix arbitrary-precision calculator:
echo "10 / 3" | bc -l
Output:
3.33333333333333333333
The -l flag loads the standard math library, which also enables functions like sqrt(), s() (sine), c() (cosine), and sets a reasonable default decimal precision (scale).
Step-by-Step: A Full Decimal-Capable Calculator Script
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 <num1> <operator> <num2>"
echo "Operators: + - * / % ^"
exit 1
}
if [ "$#" -ne 3 ]; then
usage
fi
num1=$1
operator=$2
num2=$3
if ! [[ "$num1" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] || ! [[ "$num2" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
echo "Error: both arguments must be numbers" >&2
exit 1
fi
case "$operator" in
+|-|\*|/|\^)
result=$(echo "scale=6; $num1 $operator $num2" | bc -l)
;;
%)
result=$(echo "$num1 % $num2" | bc -l)
;;
*)
echo "Error: unknown operator '$operator'" >&2
usage
;;
esac
echo "Result: $result"
Run it:
./calc.sh 10 / 3
./calc.sh 2.5 \* 4
./calc.sh 2 ^ 10
Output:
Result: 3.333333
Result: 10.0
Result: 1024
Note that in bc, ^ means exponentiation (not XOR like in some other languages), which is different from Bash’s own ** operator.
How This Works Internally
- The regex
^-?[0-9]+(\.[0-9]+)?$validates that the input is a valid number: an optional leading minus sign, one or more digits, and an optional decimal portion. This prevents feeding garbage input intobc, which would otherwise produce a confusing parse error. scale=6is abcsetting that controls how many digits appear after the decimal point in results; without it,bcdefaults toscale=0, silently truncating all decimal output back to whole numbers, which surprises a lot of people the first time they use it.- Piping a string like
"10 / 3"intobcworks becausebcreads its program (a small arithmetic expression, in this case) from standard input.
Building an Interactive Calculator Loop
For a REPL-style calculator I can keep typing into:
#!/usr/bin/env bash
set -euo pipefail
echo "Bash Calculator (type 'exit' to quit)"
while true; do
read -rp "> " expression
if [ "$expression" = "exit" ]; then
break
fi
if [ -z "$expression" ]; then
continue
fi
result=$(echo "scale=6; $expression" | bc -l 2>/dev/null) || {
echo "Invalid expression"
continue
}
echo "$result"
done
Example session:
Bash Calculator (type 'exit' to quit)
> 5 + 3
8
> 10 / 3
3.333333
> sqrt(2)
1.414213
> exit
Because it’s just feeding raw input straight into bc, this version even supports full expressions with parentheses and functions, not just single operations — bc handles the actual parsing and precedence.
Real-World Use Case: A Percentage Calculator Function
#!/usr/bin/env bash
percentage_of() {
local part=$1
local whole=$2
echo "scale=2; ($part / $whole) * 100" | bc -l
}
echo "$(percentage_of 45 200)%"
Output:
22.50%
I use small functions like this inside larger reporting scripts — for example, calculating disk usage percentage or test pass rates without needing a separate calculator tool.
Automation Example: Calculating Average from a List of Numbers
#!/usr/bin/env bash
set -euo pipefail
numbers=(23 45 12 67 34 89 21)
sum=0
for n in "${numbers[@]}"; do
sum=$((sum + n))
done
count=${#numbers[@]}
average=$(echo "scale=2; $sum / $count" | bc -l)
echo "Sum: $sum"
echo "Count: $count"
echo "Average: $average"
Output:
Sum: 291
Count: 7
Average: 41.57
This pattern — accumulate with fast native Bash integer arithmetic, then hand off to bc only for the final division — is a good balance of performance and correctness.
Alternative: Using awk for Math
awk is another solid option for decimal math, and it’s often already being used elsewhere in a script for text processing, which can save you from bringing in bc as well:
awk "BEGIN { print 10 / 3 }"
Output:
3.33333
I tend to reach for bc when I need arbitrary precision or math functions, and awk when I’m already parsing text and just need a quick calculation inline without a separate pipe.
Security Considerations
- Never pass unvalidated user input directly into
bcor Bash arithmetic contexts without checking it’s actually numeric first. While$(( ))andbcdon’t execute arbitrary shell commands the wayevaldoes, malformed input can still cause script crashes or unexpected error output that a poorly handled script might mishandle downstream. - Avoid
evalfor building calculator logic entirely. It’s tempting to useeval "result=$((${num1}${operator}${num2}))"for flexibility, but this reopens the door to shell injection if any of those values come from untrusted input. Thecasestatement approach shown above avoids this risk entirely. - Watch for integer overflow in native Bash arithmetic. Bash uses signed 64-bit integers internally; extremely large calculations can silently wrap around rather than erroring, which is another good reason to route anything beyond simple counting logic through
bc.
Optimization Tips
- Use native Bash arithmetic (
$(( ))) for simple integer operations inside loops — it’s a shell built-in and avoids the overhead of spawningbcas an external process on every iteration. - Reserve
bccalls for the final calculation or for operations that genuinely need decimal precision, rather than piping every single intermediate step through it. - For heavy numerical workloads, Bash (even with
bc) isn’t the right tool — reach for Python or another language better suited to numerical computation.
Troubleshooting
- “bc: command not found”: install it via
sudo apt-get install bc(Debian/Ubuntu) orsudo dnf install bc(RHEL/Fedora); it doesn’t ship by default on every minimal system. - Division results are always whole numbers even with
bc: you forgot to setscale=; without it,bcdefaults to zero decimal places. - “division by zero” error crashes the script: always check for a zero divisor before attempting native Bash division, since
$(( x / 0 ))causes a fatal shell error, not a graceful failure. - Unexpected results with negative numbers and modulo: Bash’s
%andbc‘s%can behave differently from other languages regarding the sign of the result for negative operands — test edge cases explicitly if this matters for your use case.
Common Mistakes to Avoid
- Expecting
$(( 10 / 3 ))to return a decimal — native Bash arithmetic always truncates to an integer. - Forgetting to set
scale=when usingbc, resulting in unexpectedly truncated decimal output. - Using
evalto build arithmetic expressions dynamically from user input, introducing shell injection risk. - Not escaping
*incasepatterns when matching the multiplication operator, since it’s interpreted as a glob wildcard otherwise.
FAQs
Why does 10 / 3 give 3 instead of 3.33 in Bash? Because Bash’s built-in arithmetic ($(( ))) only supports integers; for decimal results, pipe the expression into bc -l or use awk.
Is bc installed by default on Linux? Not always — many minimal server images and containers don’t include it, so it’s worth adding an install check or documenting it as a dependency.
Can I do trigonometry or logarithms in a Bash calculator? Yes, bc -l provides s() (sine), c() (cosine), a() (arctangent), l() (natural log), and e() (exponential) as part of its math library.
What’s the fastest option for simple integer-only math in a loop? Native Bash arithmetic ($(( ))) is fastest since it requires no external process; only reach for bc or awk once decimals or advanced functions are needed.
Summary
Building a calculator in Bash really comes down to knowing the boundary between what the shell can do natively (fast, simple integer math) and where you need to hand things off to bc or awk for decimals and advanced functions. With careful input validation and a clear case-based structure instead of eval, it’s entirely possible to build a calculator that’s both safe and genuinely useful for everyday scripting and even interactive use.
References
- GNU Bash Reference Manual, Shell Arithmetic: https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic
- GNU
bcmanual: https://www.gnu.org/software/bc/manual/html_mono/bc.html - POSIX
awkspecification: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/awk.html