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.

  1. 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.
  2. 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.
  3. 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

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 status 0 because 6*7≠0.
  • The 2nd: : always sends nothing to stdout with status 0.
  • The 3rd: echo outputs the expansion of 6*7: 42, and successfully, so status 0.
# 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 status 1 because 2-2=0.
  • The 2nd: : always sends nothing to stdout with status 0.
  • The 3rd: echo outputs the expansion of 2-2: 0, and successfully, so status 0.

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