A quick intro to arithmetic evaluation and expansion in Bash
This is part of Underappreciated Bash idioms, which see.
Why write [ "$x" -gt 2 ] when you could write ((x > 2))?
The latter is immediately more readable. As a bonus, it's also about twice as fast.
If you aren't trying to stick to POSIX, and your script is already sprinkled with Bashisms, and you're only dealing with integers — then you might as well embrace Bash's arithmetic evaluation and expansion. Its syntax offers terse conditionals and safely does away with $ and quoting.
Examples
Here we define three variables and compare them:
((x=72, y=-15, z=x+2*y, y < x && 2*z > x)) && echo yes #⇒ yes
Suppose you want to convert 10:15:42 to seconds. You can do it directly:
## creating variables ((h=10, m=15, s=42, m+=h*60, s+=m*60)); echo "$s" #⇒ 36942 echo "$((h=10, m=15, s=42, m+=h*60, s+=m*60, s))" #⇒ 36942 ## without creating variables ((_=42, _+=15*60, _+=10*60**2)); echo "$_" #⇒ 36942 echo "$((_=42, _+=15*60, _+=10*60**2, _))" #⇒ 36942
Or you can define a function and call it:
## creating variables foo() { local h m s ((h="$1", m="$2", s="$3", m+=h*60, s+=m*60)) echo "$s" ;} foo 10 15 42 #⇒ 36942 ## without creating variables foo() (((_="$3", _+="$2"*60, _+="$1"*60**2)) echo "$_") foo 10 15 42 #⇒ 36942
Getting used to it
It doesn't take long to get used to it if you keep a few things in mind.
- Statements must be separated by commas, even if you continue in the next line. This is different from "outside
((…))", where you can do away with trailing semicolons. - You can get rid of all the
$except:
- in positional parameters (
$1,$2, etc.), because they're numeric; and - when you need to modify the variable:
${foo:2:5},${#bar},${!quux}, etc.
- in positional parameters (
- You must be able to choose between:
- arithmetic evaluation, expressed as
((…)), and - arithmetic expansion, expressed as
"$((…))"— which you should get into the good habit of quoting, even though it's arguably not strictly necessary.
- arithmetic evaluation, expressed as
Arithmetic evaluation
You use ((…)) for tests and assignments.
Arithmetic evaluations don't output anything.
They can modify variables, and they can send you a return status of 0 or 1.
Tests
Tests look like this:
foo() if (("$#">3 || "$1" != 42)) then echo "Bad input!" >&2; exit 1 else echo "Ok, let's move on." fi
or:
foo() { (("$#">3 || "$1" != 42)) && echo >&2 "Bad input!" && exit 1 echo "Ok, let's move on." ;}
Speaking of if…then…else, arithmetic evaluations have their own syntax for that.
Ternary operator
((x > y ? z=5 : x=1)) # Is x>y? Then assign z to 5. Otherwise assign x to 1. ((x < y ? w=4 : w=3)) # Is x<y? Then assign w to 4. Otherwise assign w to 3.
We can here assign to the result with no extra parentheses. This last one can be rewritten as:
((w=x < y ? 4 : 3))
Let's test that with an example:
((x=1, y=2, w=x < y ? 4 : 3, w == 4)) && echo yes!
yes!
Yes, w is 4.
What if you need some elif? Then you can nest conditionals.
# if x<y, then w=4; elif 5x>y, then w=3; else w=1; fi ((w=x < y ? 4 : 5*x > y ? 3 : 1))
which you can neatly rearrange as:
((w= x < y ? 4 : 5*x > y ? 3 : 1))
Need to add another declaration? Just add a , after the 1.
Assignments
Assignments also have return status, but you usually don't care about them, because they're always 0 (except when you assign to 0, in which case they're 1...):
((x=4)) ; echo "$?" #⇒ 0 ((x=4,y=5)); echo "$?" #⇒ 0 ((x=0,y=5)); echo "$?" #⇒ 0 ((x=4,y=0)); echo "$?" #⇒ 1 # last assignment is to 0, so...
What you usually care about is assigning:
foo() { local h m s ((h="$1", m="$2", s="$3", m+=h*60, s+=m*60)) echo "We have a total of $s seconds here." ;} foo 10 15 42
We have a total of 36942 seconds here.
Here you changed all those variables and ended up with a modified value of $s, which you then used in the output string.
Arithmetic expansion
You choose "$((…))" whenever you want to directly use the numeric value that it produces.
For example, the arithmetic evaluation of the previous function could have returned the value of $s directly, and could therefore be rewritten like this:
foo() { local h m s printf "We have a total of %d seconds here.\n" \ "$((h="$1", m="$2", s="$3", m+=h*60, s+=m*60, s))" ;} foo 10 15 42
or like this:
foo() { local h m s printf "We have a total of %d seconds here.\n" \ "$((h="$1", m="$2", s="$3", h*60**2 + m*60 + s))" ;} foo 10 15 42
or directly from the positional arguments, like this:
foo() { printf "We have a total of %d seconds here.\n" \ "$(("$1"*60**2 + "$2"*60 + "$3"))" ;} foo 10 15 42
Side by side
To wrap up, what happens when you run these?
# expression stdout $? arithmetic… ((6*7)) # 0 evaluation : "$((6*7))" # 0 expansion echo "$((6*7))" # 42 0 expansion
- The 1st: arithmetic evaluation, so nothing to
stdout, and status0because6*7≠0. - The 2nd:
:always sends nothing tostdoutwith status0. - The 3rd:
echooutputs the expansion of6*7:42, and successfully, so status0.
# expression stdout $? arithmetic… ((2-2)) # 1 evaluation : "$((2-2))" # 0 expansion echo "$((2-2))" # 0 0 expansion
- The 1st: arithmetic evaluation, so nothing to
stdout, and status1because2-2=0. - The 2nd:
:always sends nothing tostdoutwith status0. - The 3rd:
echooutputs the expansion of2-2:0, and successfully, so status0.
Add x= before any of these, say ((x=6*7)), and you get the same things.
The only difference is that x will be assigned. You can assign and send to stdout at the same time:
echo "x should become $((x=6*7))," echo "and x is now indeed $x."
x should become 42, and x is now indeed 42.
📆 2025-11-08