skip to content

Arithmetic Evaluation

Bash does integer math only, in two syntaxes: $(( )) yields a value while (( )) yields an exit status — where a result of 0 counts as failure, the classic trap that kills `((i--))` under set -e. Anything fractional means shelling out to bc or awk.

part ofBashoverview, primer and where to startread it →
on this pageshow

questions

5

In bash, what does `echo $(( 10 / 3 ))` print, and how do you compute a percentage or an average with a decimal point inside a bash script?

level: juniorimportance: must knowfreq 58%

answer

  1. no decimal point exists here
  2. truncates, and toward zero
  3. multiply before you divide
  4. delegate to awk or bc
  5. printf formats but cannot divide

basics

~20 s

It prints 3. Bash arithmetic is integer-only, so division truncates toward zero and there is no fractional result. For decimals you call out to another tool — awk or bc — or scale the integers up before dividing and format the result with printf.

solid answer

~50 s

Bash has no floating-point arithmetic at all; every value in `$(( ))` is an integer, so `10 / 3` truncates to 3 and `1 / 2` is 0. Truncation is toward zero, so `$(( -7 / 2 ))` is -3 rather than -4, and `%` follows to match. If you actually need decimals, you delegate: `awk 'BEGIN { printf "%.2f\n", 10/3 }'` is the most portable choice since awk is everywhere, and `echo "scale=2; 10/3" | bc` works when bc is installed. A third option keeps everything in bash: scale up first, as in `$(( part * 100 / total ))` for an integer percentage, and add half the divisor before dividing if you want rounding instead of truncation. Note that bash's `printf` can *format* a float you were given, but it cannot compute one.

code

bash · 6 lines
bash
#!/usr/bin/env bash
passed=7 total=9
echo "integer:  $(( 100 * passed / total ))%"          # 77
echo "rounded:  $(( (100 * passed + total / 2) / total ))%"  # 78
awk -v p="$passed" -v t="$total" 'BEGIN { printf "awk:      %.2f%%\n", 100 * p / t }'
printf '%s\n' "scale=2; 100 * $passed / $total" | bc

go deeper

for a junior

Say clearly that bash does integer math only, so $(( 10 / 3 )) is 3, and name awk or bc as the way to get a decimal. Know that a literal like 1.5 is a syntax error inside $(( )).

for a middle

Explain truncation toward zero, the sign of %, and the multiply-before-divide rule for percentages. Be ready to show the scaled-integer trick that keeps a script dependency-free.

for a senior

Reason about the environment: bc is often missing from slim images, awk is not, and silent 64-bit overflow can corrupt byte-count math. Show how you would pass values into awk safely with -v.

for a principal

Frame the boundary decision — at what point numeric requirements, rounding rules or precision mean the task should leave the shell entirely rather than accumulate fixed-point workarounds in a script others must maintain.

## Bash arithmetic is integers, full stop Every value the shell's arithmetic evaluator handles is a signed integer, in practice 64-bit. There is no float type, no automatic promotion, and no error when precision is lost — the fractional part is simply dropped. ```bash echo $(( 10 / 3 )) # 3 echo $(( 1 / 2 )) # 0 echo $(( 7 / 8 )) # 0 ``` Writing a literal with a decimal point is not a workaround; it is a syntax error in the arithmetic evaluator: ```bash echo $(( 1.5 + 1 )) # bash: 1.5 + 1: syntax error: invalid arithmetic operator ``` This is a bash-specific limitation rather than a shell-wide one — ksh93 and zsh do support floating-point arithmetic — but a script written for bash cannot rely on that. ## Truncation, not rounding, and it goes toward zero Division truncates rather than rounding: `$(( 9 / 10 ))` is 0, not 1. For negative operands bash follows C99 and truncates **toward zero**, which is worth stating precisely because floor division would give a different answer: ```bash echo $(( -7 / 2 )) # -3 (toward zero, not -4) echo $(( -7 % 2 )) # -1 (sign follows the dividend) ``` The remainder operator is defined so that `(a / b) * b + (a % b) == a`, which is why `%` can return a negative number — a real surprise if you were using it to index into an array or bucket a hash. Overflow is equally silent. Exceed the 64-bit range and the value wraps around with no diagnostic, so a script multiplying byte counts can produce a confidently wrong negative number. ## Three ways to get a decimal **awk** — the most portable, since awk is mandated by POSIX and present on essentially every system including minimal containers: ```bash awk -v p="$passed" -v t="$total" 'BEGIN { printf "%.1f%%\n", 100 * p / t }' ``` Passing values with `-v` rather than interpolating them into the program text also keeps script variables out of awk's source, which matters when the values are not fully trusted. **bc** — a calculator built for this, but note that it is *not* installed by default on many slim images, and that `scale` must be set explicitly or it also truncates: ```bash echo "scale=2; 100 * $passed / $total" | bc printf '%s\n' "scale=4; 22/7" | bc ``` Without `scale=2`, `echo "10/3" | bc` prints 3 — the same truncation you were trying to escape. `bc -l` loads the math library and defaults scale to 20. **Scaled integers inside bash** — often the best answer when you only need one or two decimal places, because it adds no dependency: ```bash pct=$(( 100 * passed / total )) # whole-number percent, truncated rounded=$(( (100 * passed + total / 2) / total )) # add half the divisor to round tenths=$(( 1000 * passed / total )) # then insert the point when printing printf '%d.%01d%%\n' $(( tenths / 10 )) $(( tenths % 10 )) ``` Multiply **before** you divide — `$(( 100 * passed / total ))` is right, `$(( 100 * (passed / total) ))` is always 0 or 100 because the inner division truncates first. That ordering mistake is the single most common bug in shell percentage math. ## printf formats, it does not compute Bash's `printf` builtin understands `%f` and friends, so `printf '%.2f\n' 3.14159` prints 3.14. It is a formatter: it cannot divide. `printf '%.2f\n' "$(( 10 / 3 ))"` prints 3.00, because the truncation already happened in the arithmetic before printf ever saw the value. `printf` is also how you zero-pad an integer (`printf -v month '%02d' "$m"`) and how you store the result in a variable with `-v` instead of forking a command substitution. ## When to stop If a script is doing enough numeric work that you are threading values through awk repeatedly, that is a signal the job has outgrown the shell. Bash is glue for processes; a task whose core is arithmetic — statistics, currency, anything where rounding rules are part of the requirement — is cheaper and safer in awk itself, Python, or a real program. Reaching for that judgment early is a better answer than an elaborate fixed-point implementation in shell.

  • What does `$(( -7 / 2 ))` evaluate to in bash, and why is that worth knowing?
    It is -3, not -4: bash follows C and truncates toward zero rather than flooring. The remainder matches, so `$(( -7 % 2 ))` is -1. It matters whenever you use `%` to bucket or index, because a negative remainder can produce an out-of-range index from what looks like safe modulo arithmetic.
  • Why is `$(( 100 * (passed / total) ))` almost always wrong?
    The inner division runs first in integer arithmetic, so unless passed equals or exceeds total it truncates to 0 and the whole expression is 0. Multiplying before dividing — `$(( 100 * passed / total ))` — keeps the precision you need. It is the classic ordering bug in shell percentage math.
  • Between awk and bc for decimal math in a script, which would you default to and why?
    awk, because POSIX mandates it and it is present on effectively every system including slim container images, while bc is frequently absent. awk also takes values through `-v` instead of string-interpolated program text, and it formats in the same step with printf. bc is fine when you know it is installed and want `scale` semantics.
  • Can bash's printf be used to divide two numbers?
    No. `printf` only formats values it is given, so `printf '%.2f\n' "$(( 10 / 3 ))"` prints 3.00 — the truncation already happened in the arithmetic. printf is still useful for zero-padding integers and, with `-v`, for assigning a formatted string without a subshell.

saying these in an interview costs you the question

  • Expecting $(( 10 / 3 )) to print 3.33
  • Thinking printf can perform the division
  • Assuming bc is installed everywhere
  • Dividing before multiplying in a percentage
  • Believing division rounds instead of truncating

context

open as a page

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

level: middleimportance: must knowfreq 68%

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.

open as a page

In bash, what does declaring a variable with `declare -i` change about later assignments to it, and how does that compare with a plain assignment using `$(( ))`?

level: middleimportance: should knowfreq 30%

basics

~20 s

declare -i marks a variable as integer, so every later assignment to it is evaluated as an arithmetic expression rather than stored as text. Non-numeric input silently becomes 0, and += means addition instead of string concatenation. A plain assignment with $(( )) makes that evaluation visible at each use site.

open as a page

A bash script running under `set -euo pipefail` counts matches with `((count++))` inside a loop, and it exits silently on the very first iteration with count starting at 0. Why does that line abort the script?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The post-increment count++ evaluates to the value before the increment, which is 0. A zero result makes the (( )) command exit with status 1, and a non-zero status on a bare command aborts the script. The variable was incremented; only the status looked like failure.

open as a page

A bash backup script sets `month=$(date +%m)` and then computes `$(( month + 1 ))`. It works most of the year but in August fails with `value too great for base (error token is "08")`. What happened, and how do you fix it?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

A number with a leading zero is octal in bash arithmetic, so 08 is invalid — octal has no digit 8. Force base 10 with the base#number syntax, writing $(( 10#$month + 1 )), or strip the leading zero before doing the math.

open as a page