Build an Interactive Calculator: Master Bash Math Operations
You can do $((2+2)) in Bash. Congratulations, you’re a calculator. But can you do sqrt(16)? What about 5! or sin(3.14159)?
Bash’s built-in arithmetic is limited to integers. For anything real, you need bc. And if you want it interactive, you need to build it yourself.
Today, we’re building an Interactive Calculator - a REPL (Read-Eval-Print Loop) that handles basic math, trigonometry, logarithms, and even factorial. With history and a ans variable for chaining calculations.
The Problem: Bash Can’t Do Real Math
Try this in Bash:
echo $((5 / 2)) # 2 (not 2.5!)
echo $((2 ** 0.5)) # Error!
Bash only does integers. For anything useful, you need bc:
echo "5 / 2" | bc -l # 2.50000000000000000000
echo "sqrt(2)" | bc -l # 1.41421356237309504880
echo "s(3.14159265)" | bc -l # 0.00000000358979323846 (sin in radians)
But piping to bc for every calculation is tedious. Let’s build something better.
The Toolkit: Math in Bash and bc
Bash Built-in Arithmetic
$((expression)) # Integer math only
((a++)) # Increment
((a > b)) # Comparison (returns 0 or 1)
The bc Calculator
bc # Basic calculator
bc -l # Load math library (trig, log, scale=20)
bc Math Library Functions
| Function | bc syntax | Description |
|---|---|---|
| Sine | s(x) | Radians |
| Cosine | c(x) | Radians |
| Arctangent | a(x) | Radians |
| Natural log | l(x) | ln(x) |
| Exponential | e(x) | e^x |
| Square root | sqrt(x) | |
| Power | x ^ y | |
| Scale | scale=n | Decimal places |
Regex for Parsing
| Pattern | Matches |
|---|---|
[0-9]+! | Factorial (5!) |
sqrt\([^)]+\) | sqrt(expression) |
([0-9.]+)%.*of.*([0-9.]+) | Percentage (25% of 200) |
The Walkthrough: Building Step by Step
Step 1: Basic bc Wrapper
calc() {
echo "scale=10; $*" | bc -l 2>/dev/null
}
# Test
calc "5 / 2" # 2.5000000000
calc "sqrt(16)" # 4.0000000000
calc "2 ^ 8" # 256
What’s happening:
scale=10sets 10 decimal places-lloads math library2>/dev/nullsuppresses errors
Step 2: Interactive Loop (REPL)
main() {
echo "Bash Calculator - Type 'help' or 'quit'"
echo ""
while true; do
echo -n "calc> "
read -r input || exit 0 # Handle Ctrl+D
[[ -z "$input" ]] && continue
case "$input" in
quit|exit|q) echo "Goodbye!"; exit 0 ;;
help|h|\?) print_help ;;
history) show_history ;;
clear) clear ;;
*) process_expression "$input" ;;
esac
done
}
REPL pattern:
- Print prompt
- Read input
- Evaluate
- Print result
- Loop
Step 3: Factorial Function
bc doesn’t have factorial. We’ll implement it:
factorial() {
local n=$1
if [[ $n -lt 0 ]]; then
echo "Error: factorial of negative number"
return 1
fi
if [[ $n -le 1 ]]; then
echo 1
else
# Recursive calculation
local prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
# Test
factorial 5 # 120
factorial 10 # 3628800
Recursion in Bash:
- Function calls itself with smaller input
- Base case:
n <= 1returns 1 - Each call multiplies
n * factorial(n-1)
Step 4: Expression Parser
Here’s where regex shines:
process_expression() {
local expr="$*"
local original_expr="$expr"
# Replace 'ans' with last result
expr="${expr//ans/$LAST_RESULT}"
# Handle factorial: 5!
while [[ "$expr" =~ ([0-9]+)! ]]; do
local num="${BASH_REMATCH[1]}"
local fact_result=$(factorial "$num")
expr="${expr//${num}!/$fact_result}"
done
# Handle sqrt(x)
while [[ "$expr" =~ sqrt\(([^)]+)\) ]]; do
local inner="${BASH_REMATCH[1]}"
local result=$(calc "sqrt($inner)")
expr="${expr/sqrt($inner)/$result}"
done
# Handle sin(x)
while [[ "$expr" =~ sin\(([^)]+)\) ]]; do
local inner="${BASH_REMATCH[1]}"
local result=$(calc "s($inner)")
expr="${expr/sin($inner)/$result}"
done
# Handle cos(x)
while [[ "$expr" =~ cos\(([^)]+)\) ]]; do
local inner="${BASH_REMATCH[1]}"
local result=$(calc "c($inner)")
expr="${expr/cos($inner)/$result}"
done
# Handle percentage: 25% of 200
if [[ "$expr" =~ ([0-9.]+)%[[:space:]]*of[[:space:]]*([0-9.]+) ]]; then
local pct="${BASH_REMATCH[1]}"
local num="${BASH_REMATCH[2]}"
local result=$(calc "$pct * $num / 100")
echo " = $result"
LAST_RESULT="$result"
return
fi
# Replace ^ with ** for bc
expr="${expr//\^/**}"
# Calculate
local result=$(calc "$expr")
if [[ -z "$result" ]]; then
echo " Error: Invalid expression"
return 1
fi
# Clean up trailing zeros
result=$(echo "$result" | sed 's/\.0*$//; s/\(\.[0-9]*[1-9]\)0*$/\1/')
LAST_RESULT="$result"
add_to_history "$original_expr" "$result"
echo " = $result"
}
Parsing strategy:
- Replace special syntax with calculated values
- Loop until no more matches (handles nested expressions)
- Pass cleaned expression to bc
Regex patterns explained:
([0-9]+)!- Capture digits before!sqrt\(([^)]+)\)- Matchsqrt(, capture anything except), match)${expr/pattern/replacement}- First occurrence${expr//pattern/replacement}- All occurrences
Step 5: History Feature
HISTORY=()
LAST_RESULT=0
add_to_history() {
local entry="$1 = $2"
HISTORY+=("$entry")
# Keep only last 20 entries
if [[ ${#HISTORY[@]} -gt 20 ]]; then
HISTORY=("${HISTORY[@]:1}")
fi
}
show_history() {
echo ""
echo "═══ Calculation History ═══"
if [[ ${#HISTORY[@]} -eq 0 ]]; then
echo " No calculations yet"
else
for i in "${!HISTORY[@]}"; do
echo " $((i + 1)). ${HISTORY[$i]}"
done
fi
echo ""
}
Array operations:
HISTORY+=("item")- Append to array${#HISTORY[@]}- Array length${HISTORY[@]:1}- Slice from index 1 (remove first)${!HISTORY[@]}- Array indices
Step 6: Additional Math Functions
# Trigonometry (bc uses radians)
sin_fn() { calc "s($1)"; }
cos_fn() { calc "c($1)"; }
tan_fn() { calc "s($1)/c($1)"; } # tan = sin/cos
# Logarithms
log_fn() { calc "l($1)"; } # Natural log
log10_fn() { calc "l($1)/l(10)"; } # Log base 10
# Absolute value
abs_fn() {
local val=$(calc "$1")
if [[ $(echo "$val < 0" | bc) -eq 1 ]]; then
calc "-1 * $val"
else
echo "$val"
fi
}
# Round to decimal places
round_fn() {
local val=$1
local places=${2:-0}
printf "%.${places}f\n" "$val"
}
bc tricks:
tandoesn’t exist in bc, so we calculatesin/coslog10(x) = ln(x) / ln(10)- Change of base formula- Compare with bc:
echo "$val < 0" | bcreturns 1 (true) or 0 (false)
Step 7: Pretty Interface
CYAN='\033[0;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
print_header() {
clear
echo -e "${CYAN}"
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ BASH CALCULATOR v1.0 ║"
echo "╠═══════════════════════════════════════════════════════════╣"
echo "║ Type 'help' for commands | 'quit' to exit ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
print_help() {
echo ""
echo -e "${CYAN}═══ Calculator Help ═══${NC}"
echo ""
echo "Basic: 5 + 3, 10 - 4, 6 * 7, 20 / 4, 17 % 5"
echo "Power: 2 ^ 8"
echo "Roots: sqrt(16)"
echo "Factorial: 5!"
echo "Trig: sin(3.14), cos(0), tan(0.785)"
echo "Logs: log(10), log10(100)"
echo "Percent: 25% of 200"
echo ""
echo "Special: ans (last result), history, clear, quit"
echo ""
}
Usage Examples
./calc.sh
calc> 5 + 3
= 8
calc> 2 ^ 8
= 256
calc> sqrt(16)
= 4
calc> 5!
= 120
calc> sin(3.14159)
= 0
calc> 25% of 200
= 50
calc> ans + 10
= 60
calc> history
1. 5 + 3 = 8
2. 2 ^ 8 = 256
3. sqrt(16) = 4
...
The Complete Script
#!/bin/bash
set -euo pipefail
# Colors and state
CYAN='\033[0;36m'
GREEN='\033[0;32m'
NC='\033[0m'
HISTORY=()
LAST_RESULT=0
# Math functions
calc() { echo "scale=10; $*" | bc -l 2>/dev/null; }
factorial() { ... }
sin_fn() { ... }
# ... other functions ...
# Core functions
process_expression() { ... }
add_to_history() { ... }
show_history() { ... }
# UI
print_header() { ... }
print_help() { ... }
# Main loop
main() {
if ! command -v bc &>/dev/null; then
echo "Error: bc is required"
exit 1
fi
print_header
while true; do
echo -ne "${CYAN}calc>${NC} "
read -r input || exit 0
[[ -z "$input" ]] && continue
case "$input" in
quit|exit|q) echo "Goodbye!"; exit 0 ;;
help|h|\?) print_help ;;
history) show_history ;;
clear|cls) print_header ;;
*) process_expression "$input" ;;
esac
done
}
main
What You Just Learned
You built a full-featured calculator. Along the way, you mastered:
- bc for real math - Floating point, trig, logs
- Regex parsing - Extracting patterns from expressions
- REPL pattern - Interactive command loops
- Recursion - Factorial implementation
- String manipulation - Pattern replacement
- Array management - History with size limits
Your Challenge
The calculator works. Now extend it:
- Add variables - Store values:
x = 5, thenx * 2 - Add unit conversion -
100 km to miles - Add complex numbers -
(3+4i) * (1+2i) - Add expression history file - Persist across sessions
- Add graphing - ASCII plot of functions
The best way to learn is to build something you’ll actually use.
This concludes the Basic Bash Projects series! You’ve built:
- File Organizer - File operations and automation
- System Reporter - System commands and cross-platform scripting
- User Manager - Security and user administration
- Todo CLI - Data persistence with JSON
- Calculator - Math operations and expression parsing
Each project taught you patterns you can combine for larger tools. Now go build something amazing.