Bash scripting guide · part 9 of 32
Arithmetic
Integer math with $(( )) and (( )), the exit status of (( )) that breaks set -e, leading-zero octal surprises, and what to use for decimals.
Bash does integer arithmetic natively. $(( expression )) evaluates an expression and expands
to the result; (( expression )) evaluates it as a command, for its side effects and its
exit status. The syntax inside both is borrowed from C: + - * / %, ** for powers,
comparisons, && ||, bit operators, assignments like += and ++, and the ?: ternary.
$(( )) for values
Integer operators
a=17 b=5
echo "sum $((a + b)), diff $((a - b)), product $((a * b))"
echo "quotient $((a / b)), remainder $((a % b)), power $((b ** 3))"
echo "negative division: $((-17 / 5)) remainder $((-17 % 5))"
echo "precedence: $((2 + 3 * 4)) vs $(( (2 + 3) * 4 ))"
sum 22, diff 12, product 85 quotient 3, remainder 2, power 125 negative division: -3 remainder -2 precedence: 14 vs 20
Division truncates toward zero, so 17 / 5 is 3 and -17 / 5 is -3, and the remainder
takes the sign of the dividend. Inside $(( )), variables do not need a $, and spaces are
free.
Variable names inside an arithmetic expression are evaluated as expressions themselves. An unset
or empty variable counts as 0, which is convenient for counters and dangerous for typos: $((
totl + 1 )) is silently 1. A variable holding text that is not a number or a valid name is an
error.
(( )) for conditions and updates
(( )) is a command. It returns status 0 (true) when the expression's value is non-zero,
and 1 (false) when it is zero. That makes numeric conditions read naturally, and it is how you
update a counter without an assignment statement.
Numeric tests and counters
count=0
for f in logs/*; do
(( count += 1 ))
done
echo "$count log files"
if (( count > 2 && count < 10 )); then echo "a reasonable number"; fi
load=7; (( load >= 5 )) && echo "high load: $load"
3 log files a reasonable number high load: 7
Inside (( )), > and < are numeric comparisons, not redirections, and && combines
them. This reads better than [ "$count" -gt 2 ] and is the idiomatic bash form; ['s
-gt and friends are covered in Test operators.
The (( count++ )) trap with set -e
Because a zero result means "false", an expression that evaluates to 0 makes (( )) return
status 1. Post-increment returns the old value, so (( count++ )) when count is 0 is a
failing command, and a script running with set -e exits on it.
A counter increment that kills the script
#!/usr/bin/env bash
set -e
count=0
(( count++ ))
echo "count is $count"
bash count.sh; echo "exit status: $?"
exit status: 1
Nothing was printed: (( count++ )) evaluated to 0, returned status 1, and set -e
stopped the script there. Use (( ++count )) (evaluates to the new value), count=$((count +
1)) (an assignment always succeeds) or (( count++ )) || true.
Increments that always succeed
#!/usr/bin/env bash
set -e
a=0 b=0
b=$(( b + 1 ))
(( ++a ))
echo "a=$a b=$b"
bash count.sh
a=1 b=1
Pre-increment yields 1, which is true, and an assignment from $(( )) has status 0
regardless of the value. Both forms are safe under set -e.
Leading zeros mean octal
A number written with a leading 0 is read as octal, 0x as hexadecimal, and base#digits in
any base from 2 to 64. This bites when you compute with values like 08 from a date or a
zero-padded file name: 8 is not an octal digit.
Zero-padded numbers fail with 08 and 09
month=08
echo "next month: $(( month + 1 ))"
/bin/bash: line 2: 08: value too great for base (error token is "08")
Bash parsed 08 as octal, found the invalid digit 8 and reported value too great for
base. 07 would have worked, so this bug waits until August.
Force base 10, and other bases
month=08
echo "next month: $(( 10#$month + 1 ))"
echo "hex ff = $((0xff)), binary 1010 = $((2#1010)), octal 755 = $((8#755))"
printf '%02d\n' $(( 10#$month + 1 ))
next month: 9 hex ff = 255, binary 1010 = 10, octal 755 = 493 09
10# forces decimal, so 08 is 8. The other forms convert from hexadecimal, binary and octal.
printf %02d adds the zero padding back for display. Note 10#$month needs the $:
10#month would be a syntax error.
Integers only, 64-bit
Bash arithmetic uses signed 64-bit integers. There are no fractions, and overflow wraps around
silently. For decimals, hand the calculation to a tool that does them: bc for
arbitrary precision, awk for floating point, or printf to format the result.
Decimals need bc or awk
echo "bash: $(( 7 / 2 ))"
echo "3.5 + 1" | bc
echo "scale=3; 7 / 2" | bc
awk 'BEGIN { printf "%.2f\n", 7 / 2 }'
echo $(( 1.5 + 1 ))
bash: 3 4.5 3.500 3.50 /bin/bash: line 5: 1.5 + 1 : syntax error: invalid arithmetic operator (error token is ".5 + 1 ")
Bash truncated 7 / 2 to 3. bc handles decimals, with scale setting the number of
digits after the point. awk uses floating point. Bash itself rejects 1.5 outright with a
syntax error.
64-bit limits and silent wraparound
max=9223372036854775807
echo "max: $max"
echo "max+1: $(( max + 1 ))"
echo "2**64: $(( 2 ** 64 ))"
max: 9223372036854775807 max+1: -9223372036854775808 2**64: 0
One more than the largest signed 64-bit integer wraps to the most negative value, and
2**64 wraps to 0. No error is reported, so values near these limits (byte counts on huge
files, nanosecond timestamps multiplied up) need bc.
Assignments, the ternary and $RANDOM
Arithmetic expressions can assign (=, +=, *=, <<=...), and several can be joined with a
comma; the value of the last one is the result. The ternary cond ? a : b picks between two
numbers. $RANDOM gives an integer from 0 to 32767, fine for jitter and sampling, never for
anything security-related.
Compute several values in one expression
total=405 parts=4
(( each = total / parts, rest = total % parts ))
echo "each $each, remainder $rest"
files=$(ls | wc -l); echo "$files entries, $(( files > 10 ? 10 : files )) shown"
delay=$(( RANDOM % 5 + 1 )); (( delay >= 1 && delay <= 5 )) && echo "delay in range"
each 101, remainder 1 11 entries, 10 shown delay in range
The comma joined two assignments in one (( )). The ternary capped the count at 10; it
works only with numbers, so for choosing between strings use an if. RANDOM % 5 + 1
gives 1 to 5.
Other spellings
let 'x = x + 1' and declare -i x (see Variables) are older ways to do the same
arithmetic. expr 1 + 2 is an external program from before shells had arithmetic and is only
needed for very old sh. POSIX sh supports $(( )) but not (( )), **, ++ or base#.
Related
bcAn arbitrary-precision calculator language, used from scripts for decimal math the shell cannot do.exprEvaluate an integer, comparison or string expression given as separate arguments and print the result.awkScan text as records and fields, and run a small program on each line that matches a pattern.printfPrint text and values through a format string, with precise control over spacing, numbers and escapes.declareCreate variables with attributes (array, associative array, integer, read-only, exported), or show how variables are defined.- VariablesAssigning and reading shell variables, the no-spaces rule, braces, unset vs empty, scope, readonly, and the special variables bash sets for you.
- ConditionalsHow if, elif, &&, || and ! decide using exit statuses, the three test syntaxes, guard clauses, and why
a && b || cis not an if-else. - Test operatorsEvery file, string and number test for [ ] and [[ ]], pattern and regex matching, and the traps like
[ $a > $b ]creating a file. - Strict mode and its caveatsWhat set -euo pipefail actually does, the cases where -e silently does nothing, the traps in -u and pipefail, and a balanced way to use it.
- Loopsfor over lists, files and ranges, C-style for, while and until, break and continue, retry loops, and redirecting a whole loop's input or output.