skip to content

In bash, what is the difference between `$(( ))` and `(( ))`, and what does each one produce?

level: middleimportance: must knowfreq 68%

answer

  1. value versus decision
  2. one substitutes, the other returns
  3. C truth meets shell success
  4. zero result, exit status one

basics

~20 s

$(( )) is arithmetic expansion: it substitutes the computed integer into the command line. (( )) is an arithmetic command: it evaluates the same expression, prints nothing, and returns exit status 0 when the result is non-zero and 1 when it is zero.

solid answer

~50 s

Both use the same integer arithmetic evaluator; they differ in what they hand back. `$(( expr ))` is an expansion — bash replaces it with the numeric result, so `x=$((a + b))` or `echo $((10 * 3))` is how you get a value. `(( expr ))` is a compound *command*: it evaluates the expression, throws the value away, and reports an exit status. The mapping is inverted relative to what people expect, because it follows C's notion of truth rather than the shell's: a non-zero result means "true" and gives status 0, a zero result means "false" and gives status 1. That is why `if (( count > 0 )); then` reads naturally, and also why `((count++))` on a counter sitting at 0 returns 1 and can abort a script running under `set -e`. Inside either form, variable names are evaluated without a leading `$`, and an unset or empty name counts as 0.

code

bash · 5 lines
bash
i=5
echo "$(( i * 2 ))"    # 10 : the expansion substitutes a value
(( i > 3 )) && echo "i is greater than 3"
(( 1 )); echo "exit status of (( 1 )) is $?"   # 0
(( 0 )); echo "exit status of (( 0 )) is $?"   # 1

go deeper

for a junior

Know both spellings and what each is for: $(( )) when you want the number, (( )) when you want a yes/no test in an if or while. Say plainly that bash arithmetic is integers only.

for a middle

Explain that the two share one evaluator and differ only in what they return, and derive the inverted status from C truth meeting shell success. Be ready to state what (( 0 )) exits with and why.

for a senior

Show where the inversion bites in production: a counter increment aborting a set -e script, or an assignment that happens to evaluate to zero. Offer concrete, reviewable fixes rather than just naming the trap.

for a principal

Own the guidance your team codifies: when arithmetic conditions should replace [ ] numeric tests for readability, where you standardise on x=$(( ... )) over (( x = ... )) to keep exit statuses boring, and when integer-only math means the job belongs outside bash.

## One evaluator, two wrappers Bash has a single arithmetic evaluator that understands a C-like expression language: `+ - * / % **`, the comparison operators `< <= > >= == !=`, the logical operators `&& || !`, the bitwise operators `& | ^ ~ << >>`, assignment forms such as `= += -= *=`, the increment/decrement operators `++` and `--`, the comma operator, and the ternary `cond ? a : b`. Everything is **integer** arithmetic — typically 64-bit signed — with no floating point at all. That evaluator is reachable through two different pieces of syntax, and the entire question is about what each one gives back to the surrounding script. ## `$(( ))` — an expansion that yields a value `$(( expr ))` is one of bash's word expansions, in the same family as `$var` and `$(cmd)`. Bash evaluates the expression and textually replaces the whole construct with the decimal result before the command runs: ```bash a=7 b=3 x=$(( a * b )) # x is 21 echo "remainder: $(( a % b ))" # prints "remainder: 1" sleep $(( 2 ** 3 )) # sleep receives the literal argument 8 ``` Because the result is a plain word, the exit status of the surrounding command is whatever that command returns; the expansion itself contributes no status (it can only fail hard on a syntax or division-by-zero error, which is a shell error). ## `(( ))` — a command that yields an exit status `(( expr ))` is a compound command. It evaluates the same expression, **discards** the value — nothing is printed — and exits with a status derived from that value: - expression evaluates to a **non-zero** number → exit status **0** (success/true) - expression evaluates to **zero** → exit status **1** (failure/false) ```bash (( 1 )); echo $? # 0 (( 0 )); echo $? # 1 ``` ## Why the mapping looks backwards Inside the parentheses you are in C's world, where non-zero is true and 0 is false. Outside, you are in the shell's world, where exit status 0 is success and non-zero is failure. `(( ))` is the bridge between the two, so it must invert. Once you see it as "C-true becomes shell-success", the conditional usage reads correctly: ```bash if (( retries < max_retries )); then ...; fi while (( n > 1 )); do n=$(( n / 2 )); done (( verbose )) && echo "debug on" ``` The same inversion is the source of the tree's most notorious bug. `((count++))` is post-increment: its *value* is the variable's value **before** the increment. When `count` is 0, the value is 0, so the command exits 1 — even though the increment happened. Under `set -e` that terminates the script on the first iteration of a counting loop. Pre-increment `((++count))` yields 1 there and is safe at zero, `((count++)) || true` neutralises the status, and `count=$(( count + 1 ))` sidesteps it entirely because the status then belongs to the assignment. ## Rules that apply inside both forms - **No `$` needed on variable names.** `(( i > 3 ))` and `$(( i * 2 ))` read `i` directly. Writing `$i` is legal and usually works, but the bare name is the idiom — and it is required when you want to *assign*, as in `(( i = i + 1 ))`. - **A null or unset name evaluates to 0.** `(( undefined + 1 ))` is 1, not an error about an unbound name. - **No word splitting or globbing happens inside.** Spaces are free, so `(( a > b ))` needs no quoting gymnastics — a genuine contrast with `[ ]`, where an unquoted empty variable breaks the syntax. - **Leading zeros mean octal, `0x` means hex**, and `base#digits` selects an arbitrary base from 2 to 64. - **Values are recursively evaluated.** A variable holding the string `a + b` is itself evaluated as an expression. ## Choosing between them Use `$(( ))` whenever you need the number: assignments, arguments, string interpolation. Use `(( ))` whenever you need a decision: `if`, `while`, `&&`/`||` chains, and the C-style `for (( i = 0; i < n; i++ ))`. Do not use `[ "$a" -gt "$b" ]` style tests when `(( a > b ))` says the same thing more readably — though `[ ]`-family tests remain what you reach for on strings and files. And never use `(( ))` where you meant a value: `echo (( 2 + 2 ))` is a syntax error, and `x=$(( 2 + 2 ))` is the correct spelling.

  • Why does `(( ))` not need a `$` on the variables inside it, and what changes if you write one anyway?
    Inside an arithmetic context bash evaluates bare names as variables, so `(( i > 3 ))` works directly. Writing `$i` expands it to its text first and usually gives the same result, but it breaks assignment forms — `(( $i = 5 ))` tries to assign to the value, not the name — and it exposes you to whatever junk the variable holds. The bare name is the idiom.
  • What happens to a `(( ))` command's exit status when the expression is an assignment such as `(( x = 0 ))`?
    The status still comes from the resulting value, so `(( x = 0 ))` assigns 0 and exits 1, while `(( x = 5 ))` exits 0. This is why a strict-mode script can die on a perfectly successful assignment. If the status matters, use `x=$(( ... ))` instead, whose status is the assignment's.
  • Is there any output difference between `echo $(( 2 + 2 ))` and `(( 2 + 2 ))`?
    Yes. `echo $(( 2 + 2 ))` prints 4, because the expansion is replaced by the result and passed to echo. `(( 2 + 2 ))` prints nothing at all — it evaluates the expression, discards 4, and exits 0 because the value is non-zero. If you want to see the number, you need the expansion form.

saying these in an interview costs you the question

  • Thinking (( )) prints its result like echo does
  • Claiming (( 0 )) succeeds because zero means success
  • Writing echo (( 2 + 2 )) instead of the expansion form
  • Insisting every variable inside (( )) needs a $
  • Believing $(( )) sets the exit status of the line

context