How to Create a Bash Calculator

How to Create a Bash Calculator

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 into bc, which would otherwise produce a confusing parse error.
  • scale=6 is a bc setting that controls how many digits appear after the decimal point in results; without it, bc defaults to scale=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" into bc works because bc reads 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 bc or Bash arithmetic contexts without checking it’s actually numeric first. While $(( )) and bc don’t execute arbitrary shell commands the way eval does, malformed input can still cause script crashes or unexpected error output that a poorly handled script might mishandle downstream.
  • Avoid eval for building calculator logic entirely. It’s tempting to use eval "result=$((${num1}${operator}${num2}))" for flexibility, but this reopens the door to shell injection if any of those values come from untrusted input. The case statement 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 spawning bc as an external process on every iteration.
  • Reserve bc calls 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) or sudo 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 set scale=; without it, bc defaults 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 % and bc‘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 using bc, resulting in unexpectedly truncated decimal output.
  • Using eval to build arithmetic expressions dynamically from user input, introducing shell injection risk.
  • Not escaping * in case patterns 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 bc manual: https://www.gnu.org/software/bc/manual/html_mono/bc.html
  • POSIX awk specification: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/awk.html

Total
2
Shares

Leave a Reply

Previous Post
How to Monitor Log Files in Bash

How to Monitor Log Files in Bash

Next Post
How to Generate Random Numbers in Bash

How to Generate Random Numbers in Bash

Related Posts