---
title: 'Arithmetic and Command Substitution'
source: 'https://academia.sh/en/courses/shell-programming/arithmetic-and-command-substitution'
course: 'Shell Programming'
language: en
updated: '2026-08-17T18:10:01+00:00'
license: 'CC BY-SA 4.0'
---

# Arithmetic and Command Substitution

Integer arithmetic and its traps, the arithmetic command's inverted exit code, offloading decimal calculation, and turning command output into a value.

In the previous two lessons, the notations `$(( ... ))` and `$( ... )` were
used without justification. Both are fundamental shell tools, and both
have corners that silently produce a wrong result.

The principle established in the Variables lesson still holds: every value
in the shell is a character string. These two notations temporarily carry
those strings into another domain and bring them back — one into the
domain of numbers, the other into the domain of command output.

## Arithmetic Expansion

The `$(( expression ))` notation evaluates the expression inside as an
integer and substitutes the result as text.

```sh
echo "$(( 7 / 2 )) $(( -7 / 2 )) $(( 7 % 2 )) $(( 2 ** 10 ))"
```

```
3 -3 1 1024
```

Division **truncates toward zero**: it gives 3 for $7/2$, $-3$ for
$-7/2$. This is different from languages that round down; the distinction
matters when working with negative numbers.

Variables can be written without a dollar sign inside the expression; the
inside of the parentheses is already the number domain:

```sh
a=6; b=4
echo "$(( a + b )) $(( $a * $b ))"
```

```
10 24
```

Whitespace inside the parentheses is free, and word splitting does not
apply. The expression `$(( 1+2 * 3 ))` gives 7; precedence rules are the
same as the operator precedence in the Programming Fundamentals course.

## The Base Trap

The shell treats a number starting with zero as **octal**. Values coming
from zero-padded fields — hour, minute, sequence number — therefore
produce an error:

```sh
echo "$(( 08 ))"
```

```
bash: 08: value too great for base (error token is "08")
```

`08` is not valid in base eight. The base needs to be declared explicitly:

```sh
echo "$(( 10#08 ))"
```

```
8
```

The `10#` prefix reads the value in base ten. It should be used on every
value coming from an external source that might be zero-padded.

## The Arithmetic Command and the Inverted Exit Code

`(( expression ))` is not an expansion, it is a command: it does not
substitute a value, it only calculates and produces an exit code. The
code is **the inverse of the value**: if the result is nonzero, 0
(success); if the result is zero, 1 (failure).

```sh
(( 1 + 1 )); echo "result 2 -> code=$?"
(( 0 ));     echo "result 0 -> code=$?"
```

```
result 2 -> code=0
result 0 -> code=1
```

This inversion is not arbitrary, it is the reconciliation of two
conventions: in C-like languages, a nonzero value is "true," while in the
shell, code 0 is "success." The result is that arithmetic conditions can
be written naturally:

```sh
(( 5 > 3 )) && echo "comparison is true"
```

```
comparison is true
```

The trap shows up under strict mode. The expression `(( counter++ ))`
returns code 1 while `counter` is zero; in a script with `set -e` active,
this stops the script. The safe form for incrementing a counter is
`counter=$(( counter + 1 ))`. This trap will be taken up again in the
Error Handling lesson.

Operators such as `++`, `--`, `+=` can be used inside `(( ))`:

```sh
counter=0; (( counter++ )); (( counter += 5 )); echo "counter=$counter"
```

```
counter=6
```

In older scripts, `let` and `expr` are seen for the same job. `let` is a
bash extension and is equivalent to `(( ))`. `expr` is a separate program;
it creates a process on every call, and its operators need to be escaped
so the shell does not interpret them. Neither is needed in new scripts.

## The Shell Has No Decimal Calculation

Shell arithmetic is integer-only. When a decimal result is needed, the
work is handed off externally.

```sh
awk 'BEGIN { printf "%.2f\n", 4 / 30 * 100 }'
```

```
13.33
```

A calculator tool that computes with arbitrary precision can also be
used, but it should be kept in mind that the `scale` setting is applied
**at every division**:

```sh
echo 'scale=2; 4 / 30 * 100' | bc
echo 'scale=2; 4 * 100 / 30' | bc
```

```
13.00
13.33
```

In the first form, the division happened first and the result was
truncated to 0.13; the multiplication proceeded with this truncated
value. Arranging the order of operations so that multiplication comes
before division is a general rule in fixed-precision calculation.

The third way is scaled integer arithmetic. Placing the decimal point by
hand gives one digit of precision without calling an external tool:

```sh
part=4; total=30
printf '%d.%d\n' $(( part * 1000 / total / 10 )) $(( part * 1000 / total % 10 ))
```

```
13.3
```

This method truncates, it does not round. It is sufficient for
report-style output; not for monetary or scientific calculation.

## Command Substitution

The `$( command )` notation runs the command, captures its standard
output, and substitutes that text.

```sh
count=$(wc -l < access.log)
echo "[$count]"
```

```
[      30]
```

The leading spaces came through — this is the alignment difference
mentioned in the first lesson. Arithmetic expansion absorbs the spaces:

```sh
count=$(( $(wc -l < access.log) ))
echo "[$count]"
```

```
[30]
```

This nested form is the standard way to turn a command's output into a
number.

## The Transformations Substitution Applies

Three behaviors need to be known.

**Trailing newlines are stripped.** All trailing newlines in the
command's output are discarded; the ones in between are preserved.

```sh
d=$(printf 'a\n\n\n')
printf '<%s>\n' "$d"
```

```
<a>
```

This behavior is wanted most of the time, but it leads to loss in data
where the newline is meaningful.

**Unquoted writing is split.** The rule that holds for variable expansion
holds here too:

```sh
show_count() { printf 'count=%d\n' "$#"; }
show_count $(head -2 access.log)
show_count "$(head -2 access.log)"
```

```
count=20
count=1
```

In the unquoted form, the two lines were split into twenty words.
**Command substitution is always quoted.**

**The exit code is not lost by assignment, but it can be hidden.** An
assignment that only does substitution leaves the command's code in `$?`:

```sh
d=$(grep '999' access.log); echo "code=$?"
```

```
code=1
```

But if `local`, `export`, or `declare` precedes the assignment, the code
belongs to that command instead, and the real result does not show; this
trap was shown in the Functions and Scope lesson.

## Syntax Forms

The older counterpart of the `$( ... )` form is the backtick. It is not
preferred for two reasons: nested use requires a backslash for every
level, and a backslash's meaning inside backticks is different.
`$( ... )` requires no escaping at all in nested use:

```sh
echo "most requested: $(cut -d' ' -f7 "$(echo access.log)" | sort | uniq -c | sort -rn | head -1)"
```

```
most requested:    6 /api/data
```

To read an entire file, bash offers the `$(< file)` shortcut; because it
creates no process, it is cheaper than the `$(cat file)` form:

```sh
echo "[$(< single.txt)]"
```

```
[hello]
```

## Cost

Command substitution creates a subshell and, usually, an external
process. This is unnoticeable on a thirty-line log; in a script that
makes one call per line inside a loop, process creation alone can make up
the entire workload.

Rule: **if command substitution is inside a loop body, a way to do the
same job with a single external tool call is sought.** This entire next
topic is the application of this principle.

## Applying It to the Script

`report.sh` gains a summary block and a percentage column:

```sh
log_file=access.log

percent() {                     # percent PART TOTAL -> "13.3"
  local part="$1" total="$2"
  (( total == 0 )) && { echo "0.0"; return; }
  printf '%d.%d' $(( part * 1000 / total / 10 )) $(( part * 1000 / total % 10 ))
}

requests=$(( $(wc -l < "$log_file") ))
bytes=$(awk '{ t += $10 } END { print t + 0 }' "$log_file")

section "Log: $log_file"
printf '  request count : %d\n' "$requests"
printf '  total bytes   : %d\n' "$bytes"
printf '  per request   : %d bytes\n' $(( requests > 0 ? bytes / requests : 0 ))
```

```sh
./report.sh -n 3
```

```

== Log: access.log ==
  request count : 30
  total bytes   : 117780
  per request   : 3926 bytes

== Most requested 3 paths ==
   6 /api/data
   4 /missing.html
   4 /index.html

== Status code classes ==
  2xx successful         18  %60.0
  3xx redirect            3  %10.0
  4xx client error        6  %20.0
  5xx server error        3  %10.0
```

The ternary operator in `$(( requests > 0 ? bytes / requests : 0 ))`
prevents division by zero. In shell arithmetic, division by zero is an
error that stops the script; the divisor must always be tested.

The percentages add up to 100.0, and the numbers add up to the request
count. This internal consistency is the cheapest test of the report's
correctness.

## Summary

- `$(( ))` does integer arithmetic, truncates toward zero, and lets
  variables inside be written without a dollar sign; numbers starting
  with zero are read as octal, requiring the `10#` prefix.
- `(( ))` is a command, and its exit code is the inverse of the value; a
  zero result reports failure.
- Decimal calculation does not exist in the shell; it is handed off
  externally, or done with scaled integers.
- Command substitution strips trailing newlines and gets split in
  unquoted form; it is always quoted.
- Every substitution costs a subshell; calls inside a loop body should be
  replaced with a single bulk external tool call.

## Next Step

This topic completed the script's skeleton: the shebang, the argument
interface, input validation, functions, loops, and calculation. The
script works, but it does most of its job with blunt tools: it splits
paths with `cut`, counts status codes with `grep -c`, and reads the log
from the start for every count.

The next topic hands these jobs off to their real tools. It starts with
regular expressions — the common language for splitting the log's request
line into its fields, filtering out invalid lines, and reformatting the
timestamp.
