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

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: 12

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskSyntaxNotes
Capture output$(command)Preferred form
Legacy capturebacktick command backtickAvoid; does not nest well
Store in variablex="$(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 substitutiondiff <(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!

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

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).