100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Bash

Command Substitution in Depth

Understand how $(...) captures command output as a string, why it's preferred over backticks, and the quoting rules that prevent word-splitting bugs.

Control FlowIntermediate8 min readJul 10, 2026
Analogies

$(...) vs Backticks: Why Modern Bash Prefers the Former

Command substitution replaces $(command) (or the legacy ` command backtick form) with that command's standard output, with any trailing newlines stripped. The $(...) syntax, standardized by POSIX, nests cleanly — $(cmd1 $(cmd2)) — without the escaping headaches backticks require, since nested backticks need each inner backtick escaped with a backslash, quickly becoming unreadable. $(...) also visually distinguishes itself from literal text better than backticks, which are easy to mistake for single quotes in many fonts, making $(...)` the recommended style in virtually every modern shell style guide.

🏏

Cricket analogy: Nesting $(cmd1 $(cmd2)) cleanly is like a scorer nesting a player's strike rate calculation inside their overall match rating formula without needing awkward workarounds, unlike trying to nest two backtick-quoted stats which would require escaping.

Quoting Rules: Why $(command) Needs Double Quotes

Unquoted, $(command) undergoes word splitting on $IFS and pathname expansion just like an unquoted variable, so for f in $(ls *.txt) can break on filenames containing spaces and files=$(find . -name '*.log') followed by rm $files can accidentally expand globs in unexpected ways. Wrapping it in double quotes, "$(command)", preserves internal newlines and spaces exactly as the command produced them, which is essential when capturing multi-line output like git log or a file listing into a single variable meant to be printed or compared as one block of text. The one common exception is inside [[ ]] or when the substitution is intentionally meant to split into multiple words, such as building an array with read -ra arr <<< "$(cmd)".

🏏

Cricket analogy: Quoting "$(command)" to preserve a multi-line scorecard is like keeping a full over-by-over commentary transcript intact as one block, rather than letting it fragment into scattered individual words that lose the sequence.

Capturing Exit Status and Multi-Line Output

Command substitution runs in a subshell, so any variables set inside $(...) don't persist outside it, and the substitution itself evaluates to the captured stdout, not the command's exit status — you must check $? immediately after the assignment if the exit code matters, since assigning var=$(cmd) sets $? to cmd's exit status (not the assignment's). Because trailing newlines are always stripped from the captured output, but internal newlines are preserved when quoted, output=$(printf 'a\nb\n') followed by echo "$output" prints a then b on two lines, while echo $output unquoted collapses them onto one line separated by a space.

🏏

Cricket analogy: Checking $? right after result=$(run_drs_check.sh) is like the third umpire immediately confirming the review's verdict flag the moment it's returned, before any other decision overwrites it.

bash
#!/usr/bin/env bash
set -euo pipefail

# Preferred $(...) syntax, safely quoted
current_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "On branch: $current_branch"

# Multi-line capture preserved with quoting
changed_files="$(git diff --name-only HEAD~1)"
if [[ -n "$changed_files" ]]; then
    echo "Changed files:"
    echo "$changed_files"
fi

# Checking exit status of the captured command
if output="$(curl -sf https://api.example.com/health)"; then
    echo "Health check passed: $output"
else
    status=$?
    echo "Health check failed with exit code $status" >&2
    exit "$status"
fi

# Intentional word-splitting: building an array from command output
read -ra files_array <<< "$(ls *.log 2>/dev/null)"

$(...) and backticks both strip *trailing* newlines from the captured output, but they behave identically on that point — the real reason to prefer $(...) is nesting clarity and readability, not newline handling. If you need to preserve a genuinely trailing newline, append a sentinel character before capture and strip it afterward.

  • $(command) captures stdout as a string with trailing newlines stripped; prefer it over legacy backticks for clean nesting.
  • Always quote command substitution — "$(command)" — to prevent word splitting and glob expansion on the result.
  • Internal newlines are preserved only when the substitution is quoted; unquoted, they collapse into spaces.
  • Command substitution runs in a subshell, so variables assigned inside $(...) don't persist outside it.
  • var=$(cmd) sets $? to cmd's exit status — check it immediately if success/failure matters.
  • Nested $(cmd1 $(cmd2)) reads cleanly, unlike nested backticks which require escaping inner backticks.
  • Intentional unquoted substitution is occasionally useful, e.g. splitting output into array elements via read -ra.

Practice what you learned

Was this page helpful?

Topics covered

#Bash#AdvancedBashScriptingStudyNotes#DevOps#CommandSubstitutionInDepth#Command#Substitution#Depth#Backticks#StudyNotes#SkillVeris