Command Substitution and Expansion
Reviewed & published by Brayan K
By the end of this lesson you'll be able to capture a command's output and drop it straight into another command, do integer math with arithmetic expansion, supply default values and measure string length with parameter expansion, feed two command outputs to a tool with process substitution, and avoid the classic bug of unquoted output splitting apart.
Part of the free Command Line course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Capture command output with $(command), the preferred form
- Understand the legacy backtick form and why $(...) nests better
- Store output in variables and pass it as command arguments
- Do integer math with arithmetic expansion $((...))
- Use ${VAR:-default} for fallbacks and ${#VAR} for length
- Feed two outputs to a tool with process substitution <(...) and always quote "$(...)"
1. Command Substitution: $(command)
Command substitution runs a command and replaces the $(command) expression with that command's standard output (trailing newlines are stripped). The modern form is $(...). There is also an older form that wraps the command in backtick characters, but $(...) is preferred because it reads more clearly and nests without extra escaping.
# Command substitution runs a command and replaces the $(...)
# expression with that command's standard output.
# Here 'echo' prints the literal string; we count its characters
# with wc -c. Then we substitute that result into a sentence.
echo "The word 'banana' has $(echo -n banana | wc -c) letters."
# The output of the inner command becomes part of the outer command's
# arguments. Trailing newlines from the output are stripped.
files_line="$(printf 'a\nb\nc\n' | wc -l)"
echo "There were $files_line lines."
# The older form wraps the command in backtick characters instead of
# $(...). It works, but $(...) is preferred: it reads better and nests.2. Storing Output in Variables and Arguments
The captured output is just text, so it works anywhere text does. Assign it with var="$(command)" to reuse the value, or drop a $(command) directly into another command's arguments. Quoting the substitution keeps multi-word output intact, which we will return to in the quoting section.
# Capture output into a variable, then reuse it.
# Make the file we are about to read, so this snippet gives the same answer
# on any machine. Reading /etc/shells instead would count whatever shells
# YOUR system happens to have installed.
printf '/bin/sh\n/bin/bash\n/usr/bin/zsh\n/bin/rbash\n' > shells.txt
# grep -c counts matching lines; we store that NUMBER in 'count'.
# Both /bin/bash and /bin/rbash contain "bash", so the answer is 2.
count="$(grep -c bash shells.txt)"
echo "bash appears on $count line(s) in shells.txt."
# The captured value is just text, so it works anywhere text does:
# as an argument to another command...
echo "Report: found $count match(es)."
# ...and you can substitute directly inside another command's args.
# Make a label from a fixed string and its length.
label="release"
echo "Tag: $label-$(echo -n "$label" | wc -c)"3. Nesting Substitutions
Because $(...) nests cleanly, you can place one substitution inside another and the shell evaluates the innermost one first. This is the headline advantage over the legacy backtick form, which would force you to backslash-escape the inner command.
# Because $(...) nests cleanly, you can put one substitution
# inside another. The shell evaluates the innermost one first.
# Inner: echo -n "hello world" -> "hello world"
# Middle: wc -w on that -> 2 (two words)
# Outer: build a sentence with that number.
echo "That phrase has $(echo "the phrase has $(echo -n 'hello world' | wc -w) words")."
# The same nesting with the older backtick form would require
# backslash-escaping the inner command, which is why $(...) wins.4. Arithmetic Expansion: $((...))
Use $((...)) for integer math. It follows normal operator precedence, and inside the double parentheses you can reference variables without a dollar sign. Remember it is integer-only: $((7 / 2)) is 3, and % gives the remainder.
# Arithmetic expansion $((...)) does INTEGER math. No dollar signs
# are needed on variable names inside the double parentheses.
echo "4 + 5 * 2 = $((4 + 5 * 2))" # precedence: 5*2 first, then +4
a=7
b=3
echo "$a divided by $b is $((a / b)) remainder $((a % b))" # integer only
# Combine arithmetic with command substitution: count words, then
# double the count using $((...)).
words="$(echo -n 'one two three' | wc -w)"
echo "double the word count: $((words * 2))"5. Parameter Expansion: defaults and length
${VAR:-default} expands to default when VAR is unset or empty, without changing VAR — perfect for fallbacks. ${#VAR} expands to the length (character count) of the value, which is handy for validating input.
# ${VAR:-default}: use 'default' when VAR is unset OR empty,
# without changing VAR itself.
unset name
echo "Hello, ${name:-guest}!" # name is unset -> guest
name="Ada"
echo "Hello, ${name:-guest}!" # name is set -> Ada
# ${#VAR}: the LENGTH (character count) of the value.
word="banana"
echo "'$word' has ${#word} characters."
# Handy together: fall back to a default, then measure it.
unset title
chosen="${title:-untitled}"
echo "Using '$chosen' (${#chosen} chars)."6. Process Substitution and Quoting
Process substitution <(command) hands a tool a filename that streams a command's output — ideal for diff <(cmd1) <(cmd2) with no temp files. And always wrap command substitution as "$(command)": unquoted output is subject to word splitting and globbing, which silently mangles anything with spaces or newlines.
# Process substitution <(command) hands a command a FILENAME that,
# when read, streams another command's output. Great for tools like
# diff that want file paths, with no temp files to clean up.
# Compare two fixed lists line by line. The '<' shows the line only
# in the first list; '>' shows a line only in the second.
diff <(printf 'apple\nbanana\ncherry\n') <(printf 'apple\ncherry\ndate\n')
echo "--- always quote command substitution ---"
# Output with spaces/newlines must be QUOTED to stay intact.
# Unquoted, the shell would split it on whitespace into separate args.
multi="$(printf 'two spaces')"
echo "[$multi]" # quoted: inner spaces preserved🎯 Your Turn: capture output into a variable
Fill in the one blank marked ___ using the # 👉 hint so the word count is captured with command substitution, then run it and check the Output panel matches.
# 🎯 YOUR TURN — capture a command's output into a variable.
# 1) Store the number of words in the phrase into 'n'.
# Use command substitution with $(...) and quote it.
n="___(echo -n 'red green blue' | wc -w)" # 👉 replace ___ with $
# 2) Print the captured value.
echo "The phrase has $n words."
# ✅ Expected output:
# The phrase has 3 words.🎯 Your Turn: default value then arithmetic
Supply a default for an unset variable, then do integer math on the result. Fill in the blank, then run it.
# 🎯 YOUR TURN — use a default, then do arithmetic on a count.
# 1) qty is unset; fall back to 4 with the :- default form.
unset qty
chosen="${qty:___4}" # 👉 replace ___ with :-
# 2) Triple the chosen value using arithmetic expansion.
echo "tripled: $((chosen * 3))"
# ✅ Expected output:
# tripled: 12Pro Tips
- 💡 Always quote it: write "$(command)", not bare $(command), so whitespace and newlines in the output survive word splitting.
- 💡 Prefer $(...) over backticks: the dollar-paren form nests directly and visually pairs like brackets, while the backtick form needs awkward escaping to nest.
- 💡 No dollar signs inside $((...)): arithmetic expansion reads variable names directly, so $((a + b)) is fine — no $a needed.
- 💡 Use defaults to harden scripts: ${1:-default} gives a missing argument a sensible value instead of letting it be empty.
Common Errors (and the fix)
- Output with spaces split into several arguments — you used bare $(command). Quote it: "$(command)" to keep it as one value.
- Spaces around = in an assignment — x = $(cmd) fails. Assignment needs no spaces: x=$(cmd).
- Expected a decimal but got a truncated number — $((...)) is integer-only, so $((7 / 2)) is 3. Use a tool like bc for floating point.
- Nesting with backticks got confusing — the legacy backtick form does not nest cleanly. Switch to $(outer $(inner)).
- ${name:-x} still gave the old value — :- only kicks in when the variable is unset or empty; a set, non-empty variable keeps its value.
📋 Quick Reference
| Task | Syntax | Notes |
|---|---|---|
| Capture output | $(command) | Preferred form |
| Legacy capture | backtick command backtick | Avoid; does not nest well |
| Store in variable | x="$(command)" | No spaces around = |
| Nest substitutions | $(outer $(inner)) | Innermost runs first |
| Integer math | $((a + b)) | No $ on names inside |
| Default value | ${VAR:-default} | If unset or empty |
| String length | ${#VAR} | Character count |
| Process substitution | diff <(c1) <(c2) | Output as a filename |
| Quote to be safe | "$(command)" | Prevents word splitting |
Mini-Challenge: a tiny text report
No blanks this time — just a brief and an outline. Combine command substitution, arithmetic expansion, and parameter expansion into a few commands that report on a fixed phrase, and check your output against the hints in the comments.
# 🎯 MINI-CHALLENGE: a tiny text report using expansion
# Write a few commands (or a small script) that:
#
# 1. Stores a fixed phrase in a variable, e.g. phrase="the quick brown fox".
# 2. Uses command substitution to capture the WORD count into a variable
# (hint: echo -n "$phrase" | wc -w), and the CHARACTER count via ${#phrase}.
# 3. Prints both counts, and uses arithmetic expansion to show the word
# count doubled with $((...)).
# 4. Uses ${missing:-none} to print a safe default for an unset variable.
#
# Remember to QUOTE your command substitutions: "$(...)"
# your commands here🎉 Lesson Complete!
- ✅ $(command) captures a command's output; it is preferred over the legacy backtick form and nests cleanly
- ✅ Store output with x="$(command)" and use it as variables or arguments
- ✅ $((...)) does integer arithmetic; variable names need no dollar sign inside
- ✅ ${VAR:-default} supplies a fallback and ${#VAR} gives the length
- ✅ <(command) is process substitution; always quote "$(command)" to prevent word splitting
Practice quiz
What does command substitution do?
- Captures the output of a command and substitutes it into the command line
- Deletes a command from history
- Renames a command
- Replaces a command with another command on the PATH
Answer: Captures the output of a command and substitutes it into the command line. Command substitution runs a command and replaces the $(command) expression with that command's standard output.
Which is the modern, preferred syntax for command substitution?
- <command>
- %command%
- $(command)
- {command}
Answer: $(command). $(command) is the modern POSIX form. It nests cleanly and is preferred over the older backtick form.
Why is $(...) preferred over the older backtick form?
- It works only in zsh
- It nests cleanly and is easier to read, while the backtick form does not nest well
- It runs faster
- Backticks were removed from bash
Answer: It nests cleanly and is easier to read, while the backtick form does not nest well. $(...) can be nested directly and avoids the confusing backslash-escaping the backtick form needs when nested.
What is the result of the arithmetic expansion $((4 + 5 * 2))?
- 26
- 11
- 18
- 14
Answer: 14. $((...)) does integer arithmetic with normal precedence, so 5 * 2 is 10, plus 4 is 14.
What does ${name:-guest} expand to when name is unset or empty?
- guest
- An empty string
- An error
- The literal text name
Answer: guest. ${VAR:-default} uses the default value when VAR is unset or empty; otherwise it uses VAR's value.
If word=hello, what does ${#word} produce?
- 1
- 5
- hello
- h
Answer: 5. ${#VAR} expands to the length of the variable's value, and hello has 5 characters.
Why should you wrap command substitution in double quotes, as in "$(cmd)"?
- It makes the command run faster
- It is required for the command to run at all
- It converts the output to uppercase
- It prevents word splitting and globbing, preserving whitespace and newlines in the output
Answer: It prevents word splitting and globbing, preserving whitespace and newlines in the output. Quoting with "$(cmd)" stops the shell from splitting the output on whitespace and from expanding glob characters, keeping the result intact.
What does process substitution <(command) provide to the command using it?
- Nothing; it is a comment
- The exit status of command
- A filename that, when read, yields the output of command
- The process ID of command
Answer: A filename that, when read, yields the output of command. <(command) gives a filename (often /dev/fd/63) that streams the command's output, so tools expecting a file path can read it.
Which command compares the output of two commands without creating temporary files?
- diff $(cmd1) $(cmd2)
- diff <(cmd1) <(cmd2)
- diff ((cmd1)) ((cmd2))
- diff cmd1 cmd2
Answer: diff <(cmd1) <(cmd2). diff <(cmd1) <(cmd2) uses process substitution to feed each command's output to diff as a file.
What does count=$(grep -c root /etc/passwd) store in count?
- The number printed by the grep command
- The text grep -c root /etc/passwd
- The exit status of grep
- Nothing, because grep cannot be substituted
Answer: The number printed by the grep command. Command substitution in an assignment stores the command's standard output, so count receives the number grep -c printed.
Continue this course
- Previous: Globbing and Wildcards
- Next: Redirection and File Descriptors — stdin/stdout/stderr, >, >>, 2>&1, /dev/null and here-documents
- Quick reference: Command Line cheat sheet
Frequently asked questions
What is command substitution and how do I write it?
Command substitution runs a command and replaces the expression with that command's standard output. The modern form is dollar-paren: $(command). For example, count=$(wc -l < file.txt) stores the line count in count. There is also an older form that wraps the command in backtick characters, but $(command) is preferred because it reads more clearly and nests without extra escaping.
What is the difference between $(...) and the older backtick form?
They do the same job: capture a command's output. The difference is nesting and readability. With $(...) you can write $(outer $(inner)) directly, and the dollar-paren visually pairs like brackets. The backtick form does not nest cleanly; you must backslash-escape the inner backticks, which gets confusing fast. Modern scripts use $(...) almost everywhere.
How is arithmetic expansion different from command substitution?
Command substitution $(...) captures the text output of a command. Arithmetic expansion $((...)) evaluates an integer math expression and substitutes the numeric result, for example $((3 + 4)) becomes 7. Inside $((...)) you can reference variables without a dollar sign, like $((a * b)). Note bash arithmetic is integer only, so $((7 / 2)) is 3, not 3.5.
When should I use ${VAR:-default} versus ${#VAR}?
These are parameter expansions. ${VAR:-default} gives you a fallback value: it expands to default when VAR is unset or empty, without changing VAR. Use it to make scripts robust when a variable might be missing. ${#VAR} expands to the length (number of characters) of the value, which is handy for validation, like checking that an input is not empty.
Why do people say to always quote command substitution?
Unquoted, the output of $(...) is subject to word splitting and filename globbing, so spaces, tabs, and newlines break it into multiple arguments and any * or ? could expand to filenames. Wrapping it as "$(command)" preserves the output exactly as one value. Process substitution <(...) is different: it provides a filename, and you typically pass it where a file path is expected, such as diff <(cmd1) <(cmd2).