Why this matters
Scripts count things constantly: retries left, files processed, disk usage percentages, error lines in a log. You met $(( )) in module 7 for computing a value. This lesson adds its sibling (( )), which turns arithmetic into a condition, so you can write if (( errors > 10 )) instead of the clumsier if [ "$errors" -gt 10 ]. You will also learn the two classic traps of shell arithmetic: zero means "false", and leading zeros mean octal.
Two forms, two jobs
$(( expression )) arithmetic EXPANSION: replaced by the result, as text
(( expression )) arithmetic COMMAND: produces no text, sets an exit status
$ echo $(( 7 * 6 ))
42
$ (( 7 * 6 )); echo $?
0
$ (( 7 - 7 )); echo $?
1
The rule for the command form: status 0 if the value is non-zero, 1 if it is zero. This is the reverse of exit statuses, on purpose: it makes (( )) read like a condition in C or Python, where 0 is false and anything else is true. Comparisons yield 1 for true and 0 for false, so (( a > b )) succeeds exactly when a is greater.
Like [[, (( )) is a keyword: bash parses the inside itself. That is why < and > mean "less than" and "greater than" here rather than redirection, and why you can drop the $ before variable names.
The operators
Inside (( )) and $(( )) you have C's integer operators:
| Operators | Meaning |
|---|---|
+ - * / % | add, subtract, multiply, divide (truncates toward zero), remainder |
** | power |
< <= > >= == != | comparisons (1 or 0) |
&& || ! | logical and, or, not |
= += -= *= /= %= | assignment |
++ -- | increment, decrement (before or after the name) |
cond ? a : b | choose a or b |
( ) | grouping |
$ x=17
$ echo $(( x / 5 )) $(( x % 5 )) $(( x ** 2 ))
3 2 289
$ (( x > 10 && x < 20 )) && echo "teen"
teen
$ (( x += 3 )); echo $x
20
$ echo $(( x > 15 ? 1 : 0 ))
1
Note x / 5 is 3, not 3.4: bash arithmetic is integer-only, using 64-bit signed numbers (up to about 9.2 quintillion; beyond that it silently wraps around). For decimals, use bc or awk: echo 'scale=2; 17/5' | bc prints 3.40.
Names inside (( )) are variables even without $. An unset or empty variable counts as 0, and a variable holding another name is evaluated recursively. Writing $x also works; the difference only shows with odd values, and the plain name is the usual style.
if with (( ))
#!/bin/bash
# disk.sh: warn when usage of / exceeds a limit
limit=${1:-80}
used=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')
if (( used >= limit )); then
echo "WARNING: / is ${used}% full (limit ${limit}%)"
exit 1
fi
echo "ok: / is ${used}% full"
$ bash disk.sh
ok: / is 41% full
$ bash disk.sh 30
WARNING: / is 41% full (limit 30%)
df --output=pcent / prints a header and a line like 41%; tail -n 1 keeps the number line and tr -dc '0-9' deletes every character that is not a digit (-d delete, -c complement). Your percentage will differ.
Validating input before arithmetic
Arithmetic on garbage does surprising things. A non-number like abc is taken as a variable name (usually unset, so 0), and 3x is a syntax error:
$ n=abc; (( n > 5 )) && echo big || echo small
small
$ n=3x; (( n > 5 ))
bash: ((: 3x: value too great for base (error token is "3x")
Worse, because bash evaluates names inside arithmetic, feeding user input straight into (( )) can even run commands in crafted cases. So check that a value is a number first. A glob pattern inside [[ ]] is enough for non-negative integers:
if [[ -z $n || $n == *[!0-9]* ]]; then
echo "error: not a whole number: $n" >&2
exit 2
fi
*[!0-9]* means "somewhere there is a character that is not a digit". Together with the empty check, that accepts exactly the strings made of digits. (Allowing a minus sign, or using a regular expression with =~, comes in module 15.)
The octal trap
A leading zero makes a number octal (base 8), and 0x makes it hexadecimal:
$ echo $(( 010 )) $(( 0x10 ))
8 16
$ m=08; (( m > 6 ))
bash: ((: 08: value too great for base (error token is "08")
$ (( 10#$m > 6 )) && echo "after June"
after June
10# forces base 10. You need it whenever numbers come from dates (date +%m gives 08), times, or zero-padded file names like img_009.png. Note that 10# needs the $, because it works on the digits: 10#$m.
The zero-status trap
Since (( )) fails when the value is 0, a statement used just for its side effect can "fail":
$ i=0
$ (( i++ )); echo "status $? i=$i"
status 1 i=1
i++ evaluates to the old value, 0, so the status is 1. In a normal script this does not matter. Under set -e (module 12), which aborts on any failing command, it ends your script at the first increment. Write (( i += 1 )) (value 1, status 0), (( ++i )), or i=$(( i + 1 )) instead.
Common mistakes
$on the left of an assignment.(( $x = 5 ))with x=3 becomes(( 3 = 5 )):attempted assignment to non-variable. Write(( x = 5 )).- Expecting decimals.
$(( 10 / 4 ))is 2. Usebcorawkfor fractions. - Spaces around
=in plain assignments. Inside(( ))spaces are fine; outside,x = 5is still "run command x". - Leading zeros.
08and09are errors;010is 8. Use10#$var. - Using
(( ))on unchecked input. Validate with[[ $n == *[!0-9]* ]]first. - Mixing up the two forms.
if $(( x > 3 ))expands toif 1, which tries to run a command named1:1: command not found.
Exercises
leap combines validation with a compound condition; the checker tries century years and bad input and checks the exit status. budget reads numbers from standard input and must exit 1 only when over budget.
Recap
$(( ))produces a number as text;(( ))is a command whose status is 0 when the value is non-zero.- Inside both: C integer operators, comparisons,
&&/||, assignments, no$needed on names. - Integer-only, 64-bit; division truncates. Use
bcorawkfor decimals. - Leading
0means octal: use10#$varfor zero-padded numbers. (( i++ ))with i at 0 has status 1; prefer(( i += 1 )).- Validate input before arithmetic.
Going further: read help let, which lists every arithmetic operator in precedence order. Try echo $(( 2**63 )) to see the 64-bit wrap-around, and echo $(( 16#ff )) for other bases.