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.

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.
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:
echo $(( 10 / 3 ))3The 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.
$(( )) — Bash's Arithmetic LanguageThe 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):
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 )) # pangkat8
6
42
4
1
1024The complete operator table:
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ | 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 |
$(( )) — The $ Is OptionalThis is one of bash's unique traits: inside $(( )), variable names can be written without the $ sign. Bash automatically reads them as numbers:
#!/usr/bin/env bash
harga=15000
jumlah=4
total=$(( harga * jumlah ))
echo "Total bayar: Rp$total"Total bayar: Rp60000Notice: 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 StatementIf 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:
0 (success).1 (failure).#!/usr/bin/env bash
total=1500000
if (( total >= 1000000 )); then
echo "Total melewati satu juta — butuh persetujuan."
else
echo "Total di bawah ambang batas."
fiTotal 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 MeetBefore $(( )) 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.
letlet evaluates an arithmetic expression and modifies variables (assignment), without printing the result:
let x=5+3
let "y = x * 2"
echo "x=$x, y=$y"x=8, y=16Notice 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 Programexpr 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:
x=5
y=3
echo "5 + 3 = $(expr $x + $y)"
echo "5 * 3 = $(expr $x \* $y)"5 + 3 = 8
5 * 3 = 15See 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 FractionsBash 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:
echo "scale=2; 10 / 3" | bc3.33The 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.
To use bc's result inside your script's logic, wrap it in command substitution $(...):
#!/usr/bin/env bash
hasil=$(echo "scale=2; 10 / 3" | bc)
echo "Hasil: $hasil"Hasil: 3.33Notice 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).
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:
#!/usr/bin/env bash
pemakaian=87.5
if [ "$(echo "$pemakaian > 80" | bc)" -eq 1 ]; then
echo "PERINGATAN: pemakaian melebihi 80%!"
fiPERINGATAN: 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.
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.
#!/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}%"./kalkulator_disk.sh /var/logDirektori : /var/log
Ukuran : 1.2G
Kapasitas partisi: 10 GiB
Pemakaian : 12%Let's dissect the important parts:
| Line | What Happens | Why |
|---|---|---|
du -s --block-size=1 | Measure the directory's total bytes without rounding | We 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 percentage | Multiply by 100 first so the percentage isn't always 0 |
total_bytes > 0 ? total_bytes : 1 | Bash ternary operator | Avoids 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.
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.
exprexpr 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.
$(( )) 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.
$(( 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.
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.
** 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 $(( )).
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:
$(( )) for integer math; variables inside it may omit $.* before / preserves percentage precision.bc with scale, result is a string — let bc do the comparing.expr is fragile (* quoting), let is outdated — write new code with $(( )).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 Operators — if/elif/else, the difference between [ ] and [[ ]], test operators for strings, numbers, and files, all the way to &&/|| short-circuiting. See you there!