Why this matters
Scripts constantly need to use one command's result inside another command: "name the archive after today's date", "warn if the disk has more than 1000 files", "how many errors did the log show today?". Command substitution captures a command's output as text, and arithmetic expansion lets you compute with it. Together with variables, they turn the shell from a command launcher into a small programming language.
Command substitution: $(command)
$(command) runs the command and is replaced by whatever it printed on standard output:
$ echo "Today is $(date +%A)."
Today is Sunday.
$ files=$(ls /etc | wc -l)
$ echo "There are $files entries in /etc"
There are 93 entries in /etc
$ touch "report-$(date +%F).txt"
$ ls
report-2026-11-01.txt
date +FORMAT prints the date in a chosen format (%F is YYYY-MM-DD, %H:%M the time; see date --help). Note the command inside $(...) can be anything, pipelines included.
What happens underneath
To run the inner command, bash creates a subshell: it forks a copy of itself, connects the copy's standard output to a pipe, and reads everything that comes through the pipe until the child exits.
bash ─── fork ───▶ subshell (copy of bash) ── runs `ls /etc | wc -l`
▲ stdout ──┐
│ │ pipe
└──── reads "93\n" ──────────┘ then removes trailing newlines → "93"
Consequences:
- Only standard output is captured. Error messages (standard error) still go to the terminal. Add
2>&1inside if you want them too:$(cmd 2>&1). - All trailing newlines are removed. Output
93\nbecomes93. Newlines in the middle stay. - The subshell is a separate process, so variable assignments or
cdinside it do not affect your shell:$(cd /tmp; pwd)prints/tmp, but you are still where you were afterwards. - The exit status of
x=$(cmd)is the status ofcmd, sox=$(cmd) || echo failedworks.
Quoting command substitution
The result of an unquoted $(...) is word-split and globbed, exactly like an unquoted variable (lesson 4). Quote it unless you want separate words:
$ echo $(printf 'line one\nline two')
line one line two
$ echo "$(printf 'line one\nline two')"
line one
line two
Quotes nest naturally inside $( ): the inner command is parsed fresh, so "$(head -n 1 "$file")" is correct and common.
The old form: backticks
You will see `command` in older scripts. It means the same as $(command), but nesting needs escaping and backslashes behave strangely inside it. Always write $(...) in new code:
n=`wc -l < file` # old
n=$(wc -l < file) # same, preferred
dir=$(basename "$(pwd)") # nesting is easy with $( )
Arithmetic expansion: $(( expression ))
bash has built-in integer arithmetic:
$ echo $(( 2 + 3 * 4 ))
14
$ echo $(( (2 + 3) * 4 ))
20
$ echo $(( 17 / 5 )) $(( 17 % 5 )) $(( 2 ** 10 ))
3 2 1024
$ echo $(( -7 / 2 ))
-3
The operators are the familiar ones from C: + - * / % (remainder), ** (power), comparison operators that yield 1 or 0 (<, >, <=, >=, ==, !=), &&, ||, and parentheses. Division throws away the fractional part, rounding toward zero.
Inside $(( )) variables can be used with or without $; the name alone is looked up and its value read as a number. An unset or empty variable counts as 0:
$ a=7; b=3
$ echo $(( a * b )) $(( $a - $b ))
21 4
$ echo $(( nosuchvar + 1 ))
1
Assignment operators work too: $(( count = count + 1 )), $(( count += 5 )). (Module 9 introduces (( )) without the $, for doing arithmetic as a statement and for numeric tests.)
Numbers with a leading zero are read as octal, and 0x as hexadecimal, a frequent trap with dates and padded numbers:
$ echo $(( 010 ))
8
$ echo $(( 08 ))
bash: 08: value too great for base (error token is "08")
$ echo $(( 10#08 ))
8
10# forces base 10. Remember this when you do arithmetic on date +%m (which gives 08 in August).
Integers are 64-bit, so they overflow silently past about 9.2 quintillion; that rarely matters in practice.
Decimals: bc
For fractions, hand the calculation to bc, the calculator program, via a pipe:
$ echo 'scale=2; 22 / 7' | bc
3.14
$ ratio=$(echo "scale=1; 12 * 100 / 42" | bc)
$ echo "$ratio%"
28.5%
scale sets the number of decimal digits. (awk, from the text processing module, is another option: awk 'BEGIN { printf "%.1f\n", 12*100/42 }'.)
Putting them together
A small report, built only from what you know so far (on a typical Debian system, where the package manager's log exists):
log=/var/log/dpkg.log
total=$(wc -l < "$log")
installs=$(grep -c ' install ' "$log")
echo "$installs of $total log lines are installs ($(( installs * 100 / total ))%)"
182 of 2731 log lines are installs (6%)
Common mistakes
- Spaces in the assignment.
n = $(wc -l < f)runs a command namedn. No spaces around=. - Getting the file name in the output.
wc -l fileprints12 file;wc -l < fileprints12. With redirection, wc does not know the name. - Expecting decimals.
$(( 10 / 4 ))is 2. Use bc for fractions. - Leading zeros.
$(( 08 + 1 ))is an error;09too. Use10#08. - Using
$( )and$(( ))interchangeably.$(2 + 3)tries to run a command named2:bash: 2: command not found. - Expecting a
cdin$( )to stick. It ran in a subshell.
Exercises
The code exercise is also tested against different files, so compute everything. The task uses the real date in the sandbox.
Recap
$(command)is replaced by the command's standard output, minus trailing newlines. It runs in a forked subshell, so changes inside do not leak out.- Quote command substitutions:
"$(cmd)". Prefer$( )to backticks. $(( expression ))does 64-bit integer arithmetic:+ - * / % **, comparisons, parentheses; variables need no$inside.- Leading zeros mean octal;
10#forces decimal. For decimals usebcor awk.
Going further: date +%s prints seconds since 1970 (the Unix epoch). Use two of them and $(( )) to time a command: start=$(date +%s); sleep 2; echo $(( $(date +%s) - start )). Then read date --help for more format codes.