opscape

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 ))"
output
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"
output
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

count.sh
#!/usr/bin/env bash
set -e
count=0
(( count++ ))
echo "count is $count"
bash count.sh; echo "exit status: $?"
explain
output
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

count.sh
#!/usr/bin/env bash
set -e
a=0 b=0
b=$(( b + 1 ))
(( ++a ))
echo "a=$a b=$b"
bash count.sh
explain
output
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 ))"
outputexit 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 ))
output
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 ))
outputexit 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 ))"
output
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"
output
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