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

Functionbc syntaxDescription
Sines(x)Radians
Cosinec(x)Radians
Arctangenta(x)Radians
Natural logl(x)ln(x)
Exponentiale(x)e^x
Square rootsqrt(x)
Powerx ^ y
Scalescale=nDecimal places

Regex for Parsing

PatternMatches
[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=10 sets 10 decimal places
  • -l loads math library
  • 2>/dev/null suppresses 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:

  1. Print prompt
  2. Read input
  3. Evaluate
  4. Print result
  5. 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 <= 1 returns 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:

  1. Replace special syntax with calculated values
  2. Loop until no more matches (handles nested expressions)
  3. Pass cleaned expression to bc

Regex patterns explained:

  • ([0-9]+)! - Capture digits before !
  • sqrt\(([^)]+)\) - Match sqrt(, 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:

  • tan doesn’t exist in bc, so we calculate sin/cos
  • log10(x) = ln(x) / ln(10) - Change of base formula
  • Compare with bc: echo "$val < 0" | bc returns 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:

  1. Add variables - Store values: x = 5, then x * 2
  2. Add unit conversion - 100 km to miles
  3. Add complex numbers - (3+4i) * (1+2i)
  4. Add expression history file - Persist across sessions
  5. 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:

  1. File Organizer - File operations and automation
  2. System Reporter - System commands and cross-platform scripting
  3. User Manager - Security and user administration
  4. Todo CLI - Data persistence with JSON
  5. Calculator - Math operations and expression parsing

Each project taught you patterns you can combine for larger tools. Now go build something amazing.