Learn BASH Scripting - Arithmetic Operations & Mathematical Evaluation
Episode 8 of 27

Learn BASH Scripting - Arithmetic Operations & Mathematical Evaluation

Handling numbers inside your scripts: understanding the limitations of bash integers, arithmetic expansion $(( )), the + - * / % ** operators, the legacy let and expr commands, floating point calculations with bc, and building a realistic disk usage calculator.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

In episode 7 we covered globbing & pathname expansion — how bash expands patterns into real file lists on the filesystem. In this episode we shift from text and files to numbers. Being able to compute may seem trivial, but almost every DevOps script needs it: calculating disk usage percentages, comparing response times, counting retries, or determining whether a file size exceeds a limit.

The catch is that bash is not a calculator. It's a scripting language designed in 1989 to orchestrate processes and text — and its arithmetic carries a rarely-noticed limitation: it can only handle whole integers. You can't divide 10 by 3 and get 3.33 with pure bash. Understanding this limitation — and knowing the right tool to break through it (bc) — is the core of this episode.

Main Discussion

Bash's Limitation: Integer-Only

Imagine bash as a cash register that only knows whole rupiah bills: it can add 5000 + 2000, but when it has to divide 10000 into 3, it answers 3333 and silently discards the remainder — without telling you that 1 is left over. For some tasks that's enough; for tasks that demand precision (percentages, interest rates, metrics), it's fatal.

Test this limitation quickly:

Integer division — remainder silently discarded
echo $(( 10 / 3 ))
Output
3

The result is 3, not 3.33. No error, no warning — bash simply performs integer division and discards the remainder. This is silent truncation: an answer that's "almost right" but wrong in precision. If you write a script that displays a disk usage percentage this way, 99% can appear when the real value is 99.9% — or, worse, 0% for 4.9%. That's why we need to understand when integers are enough, and when to bring in bc.

Arithmetic Expansion $(( )) — Bash's Arithmetic Language

The most common and recommended way to do math in bash is arithmetic expansion $(( expression )). The expression is evaluated, and the result replaces the entire expression — just like other expansions ($(...), $var):

Basic $(( )) operators
echo $(( 5 + 3 ))      # penjumlahan
echo $(( 10 - 4 ))     # pengurangan
echo $(( 6 * 7 ))      # perkalian
echo $(( 20 / 5 ))     # pembagian (integer!)
echo $(( 10 % 3 ))     # sisa bagi (modulo)
echo $(( 2 ** 10 ))    # pangkat
Output
8
6
42
4
1
1024

The complete operator table:

OperatorMeaningExampleResult
+Addition$(( 5 + 3 ))8
-Subtraction$(( 10 - 4 ))6
*Multiplication$(( 6 * 7 ))42
/Integer division (remainder discarded)$(( 20 / 6 ))3
%Modulo (remainder)$(( 20 % 6 ))2
**Power$(( 2 ** 10 ))1024
++Increment(( i++ ))add 1
--Decrement(( i-- ))subtract 1
+=Compound assignment(( total += 10 ))total + 10

Variables Inside $(( )) — The $ Is Optional

This is one of bash's unique traits: inside $(( )), variable names can be written without the $ sign. Bash automatically reads them as numbers:

Variables inside $(( )) without the $ sign
#!/usr/bin/env bash
harga=15000
jumlah=4
 
total=$(( harga * jumlah ))
echo "Total bayar: Rp$total"
Output
Total bayar: Rp60000

Notice: harga and jumlah are used without $ inside $(( )). This differs from regular variable expansion, which always needs $. Both forms are actually allowed — $(( $harga * $jumlah )) is also valid — but writing without $ is cleaner and avoids bash misinterpreting the variable when it holds special syntax. This may seem trivial, but understanding why both are equally valid will save you from strange puzzles later.

(( )) Without $ — As a Statement

If you write $(( )), the result is inserted into the text. If you write (( )) without $, it becomes a statement that performs the calculation — and more importantly, the evaluation result determines the exit status:

  • If the result is non-zero → exit status 0 (success).
  • If the result is zero → exit status 1 (failure).
(( )) as a conditional statement
#!/usr/bin/env bash
total=1500000
 
if (( total >= 1000000 )); then
    echo "Total melewati satu juta — butuh persetujuan."
else
    echo "Total di bawah ambang batas."
fi
Output
Total melewati satu juta — butuh persetujuan.

The (( expression )) pattern as a condition is one of the most common idioms in bash scripts — we'll use it to the fullest in episode 9 on conditionals. For now, just understand: (( )) communicates through exit status, and bash translates "non-zero = true" into the language of conditions.

let and expr: Predecessors You'll Still Meet

Before $(( )) existed, bash provided the let command, and before that, the external program expr. You'll encounter both in old scripts, outdated tutorials, or machines with old bash versions. Understand how to read them so you're not confused — but don't write new code with them.

let

let evaluates an arithmetic expression and modifies variables (assignment), without printing the result:

let — predecessor of $(( ))
let x=5+3
let "y = x * 2"
echo "x=$x, y=$y"
Output
x=8, y=16

Notice let "y = x * 2" needs quotes because * would be matched by the shell's globbing — a good moment to be reminded: the * operator outside $(( )) or let is treated as a wildcard (episode 7), not multiplication.

expr — The Fragile External Program

expr is an external program (not a shell builtin) that evaluates expressions. It's notoriously fragile: operators must be spaced and escaped, and every variable needs $. Syntax mistakes happen very easily:

expr — a noisy external program
x=5
y=3
echo "5 + 3 = $(expr $x + $y)"
echo "5 * 3 = $(expr $x \* $y)"
Output
5 + 3 = 8
5 * 3 = 15

See how fragile it is: expr $x + $y must be space-separated, and * must be escaped to \* so it doesn't become a wildcard. Every expression also goes through an external process (slow). Conclusion: expr is a historical artifact — know it well enough to read it, but use $(( )) for all new code.

Tip

Rules of thumb: write new arithmetic code with $(( )) (and (( )) for conditions). Use let only if you maintain old scripts that already use it, and never write new expr — it's slow, fragile, and causes more bugs than it solves.

bc — Breaking the Integer Limit with Fractions

Bash can only do integers. When percentages, averages, or precise division are needed — say 10 / 3 = 3.33 — bash has to hand over to an external tool designed for the job: bc (basic calculator). bc is not part of bash; it's a text-based calculator program present on almost all Unix/Linux systems.

How to use it: send the expression as input, receive the result as output:

bc — fractional division with scale
echo "scale=2; 10 / 3" | bc
Output
3.33

The scale variable tells bc how many digits to keep after the decimal point. Without scale, bc behaves like bash: integer division. The left side of the pipe is an echo command handing over the expression; the right side is bc computing it. This leverages the pipeline we've known since the start of the series: the output of one program becomes the input of another.

Storing the Result in a Variable with Command Substitution

To use bc's result inside your script's logic, wrap it in command substitution $(...):

Storing bc's result in a variable
#!/usr/bin/env bash
hasil=$(echo "scale=2; 10 / 3" | bc)
echo "Hasil: $hasil"
Output
Hasil: 3.33

Notice the distinguishing pattern: $(( )) produces an integer from bash itself; $(echo ... | bc) produces a fractional string from an external program. They look similar visually but their mechanisms are completely different. When you need a whole number — use $(( )). When you need fractions/precision — use $(echo "scale=N; expression" | bc).

Comparing Fractional Values

There's a hidden trap: bc produces a string, not a number. So [ "$hasil" -gt 5 ] won't work for "3.33" — the -gt operator is for integers only. To compare fractional results, let bc do the comparing:

Comparing fractions with bc
#!/usr/bin/env bash
pemakaian=87.5
 
if [ "$(echo "$pemakaian > 80" | bc)" -eq 1 ]; then
    echo "PERINGATAN: pemakaian melebihi 80%!"
fi
bc does the comparing, returning 1 (true) or 0 (false)
Output
PERINGATAN: pemakaian melebihi 80%!

bc evaluates 87.5 > 80 to 1 (true) or 0 (false) — and that result, being the integer 1 or 0, is safe to compare with -eq. This is the standard idiom for fractional comparison logic in bash.

Practice: Disk Usage Calculator

Time to combine everything into a real script: calculating the disk usage percentage for a directory. This is a very common monitoring task — and a perfect example of $(( )) and bc working together.

kalkulator_disk.sh — directory usage percentage
#!/usr/bin/env bash
set -euo pipefail
 
target="${1:-$HOME}"
 
if [ ! -d "$target" ]; then
    echo "Error: '$target' bukan direktori." >&2
    exit 1
fi
 
total_bytes=$(du -s --block-size=1 "$target" | awk '{print $1}')
ruang_kapasitas=$(( total_bytes > 0 ? total_bytes : 1 ))
 
# Simulasi kapasitas partisi 10 GB (10 * 1024^3 byte)
kapasitas_partisi=$(( 10 * 1024 * 1024 * 1024 ))
 
persen=$(( ruang_kapasitas * 100 / kapasitas_partisi ))
 
echo "Direktori      : $target"
echo "Ukuran         : $(du -sh "$target" | cut -f1)"
echo "Kapasitas partisi: 10 GiB"
echo "Pemakaian      : ${persen}%"
Combination of du, $(( )), and bc for disk usage metrics
Running it and sample output
./kalkulator_disk.sh /var/log
Output
Direktori      : /var/log
Ukuran         : 1.2G
Kapasitas partisi: 10 GiB
Pemakaian      : 12%

Let's dissect the important parts:

LineWhat HappensWhy
du -s --block-size=1Measure the directory's total bytes without roundingWe need a precise number before dividing
${1:-$HOME}Parameter expansion with a default (parameter expansion episode)The script still runs without arguments
$(( ruang_kapasitas * 100 / kapasitas_partisi ))Integer arithmetic for the percentageMultiply by 100 first so the percentage isn't always 0
total_bytes > 0 ? total_bytes : 1Bash ternary operatorAvoids division by zero (pitfall below)

Notice the multiply-before-divide order: $(( ruang_kapasitas * 100 / kapasitas_partisi )), not $(( ruang_kapasitas / kapasitas_partisi * 100 )). Why? Because integer division discards the remainder. If the order were reversed, ruang_kapasitas / kapasitas_partisi would yield 0 for most small directories — then 0 * 100 = 0%. By multiplying by 100 first, integer precision is better preserved. Details like operation order are what separate correct scripts from scripts that merely look correct.

Important

Beware of integer multiplication/division ordering. a / b * 100 and a * 100 / b produce very different numbers because of truncation in division. If a result looks suspicious, check first: which one is divided first? For percentages, always multiply first, then divide.

Common Pitfalls: Arithmetic Traps

1. Division by zero

echo $(( 10 / 0 )) produces the bash error "division by 0" and, if set -e is active, the script stops. When the divisor comes from dynamic data (file sizes, user input), always validate first or provide a safe value — like the ternary operator in the script above.

2. Wrong quoting in expr

expr 5 * 3 will error with "syntax error" because * is expanded as a wildcard by the shell before expr sees it. This is one reason expr is considered fragile — and another reason to use $(( )), which is free of this problem.

3. Forgetting that variables inside $(( )) don't need $

The opposite of what's commonly feared: $(( $harga )) is valid, but writing $(( harga )) is also valid and cleaner. What's not valid is writing $(( harga )) when harga hasn't been set — its value is treated as 0. No error, just a 0 answer. Always make sure the variable is populated.

4. Integer division truncation (silent!)

$(( 5 / 2 )) = 2, not 2.5 — and there's no warning. This is the main cause of the "weird percentage" bugs in monitoring scripts. If precision is needed, switch to bc immediately.

5. Comparing bc results (fractional strings) with integer operators

[ "$hasil" -gt 5 ] errors when $hasil contains "3.33". Let bc do the comparing, then test its 1/0 result with -eq.

6. Using ** outside $(( ))

The power operator ** is only recognized inside an arithmetic context. Writing echo 2 ** 10 outside an arithmetic context triggers globbing on **, and the result isn't 1024. Always wrap it in $(( )).

Conclusion

In this episode 8, we learned how bash handles numbers: the integer-only limitation that keeps bash from being a full calculator; arithmetic expansion $(( )) with the + - * / % ** operators; the difference of (( )) as an exit-status statement; the legacy let and expr commands you should recognize but not write anew; bc as the escape hatch for fractional numbers; and a practical pattern for calculating disk usage percentages that combines it all.

Key takeaways:

  • Bash is integer-only — division silently discards the remainder.
  • Use $(( )) for integer math; variables inside it may omit $.
  • * before / preserves percentage precision.
  • Fractions = bc with scale, result is a string — let bc do the comparing.
  • expr is fragile (* quoting), let is outdated — write new code with $(( )).
  • Always prevent division by zero.

Arithmetic rarely stands alone. In real scenarios you almost always compute while making decisions: if disk usage exceeds 80%, send a warning; if the backup file doesn't exist, create it; if arguments are missing, reject them. That means we need decision-making structures — and in the next episode, episode 9, we'll cover Conditionals & Test Operatorsif/elif/else, the difference between [ ] and [[ ]], test operators for strings, numbers, and files, all the way to &&/|| short-circuiting. See you there!

Learn BASH Scripting - Arithmetic Operations & Mathematical Evaluation | Learn BASH Scripting