$(...) vs. backticks and the pitfall they both share
$(...) and backticks appear to produce the same result at first glance: a command's output gets substituted somewhere else in the script. The difference only shows up with nesting, quoting, and understanding why a variable set inside the substitution vanishes without a trace afterward. This article covers both.
Table of Contents
- 1. What command substitution does and why two notations exist
- 2. $(...): syntax and basic behavior
- 3. Backticks: syntax and their escaping quirks
- 4. Nesting: why $(...) nests cleanly and backticks do not
- 5. Why $(...) is almost always the better choice
- 6. The shared side effect: command substitution always runs in a subshell
- 7. Practical pitfalls: word splitting and the swallowed newline
- 8. When to avoid command substitution
- 9. $(...) and backticks compared directly
- 10. Summary
- 11. FAQ
1. What command substitution does and why two notations exist
Command substitution replaces an expression in a script with the standard output of the command inside it. Bash runs that command in its own subshell, collects everything it writes to standard output, strips trailing newlines, and inserts the result where the substitution stood in the surrounding command. Two notations achieve the same result: the modern $(command) and the older backtick notation inherited from the Bourne shell.
Historically, backticks came first, $(...) was added with the Korn shell and later became part of POSIX. Both forms are equally available and functionally identical in any modern Bash today, as long as there is no additional need for nesting or complex quoting. That is exactly where the two notations part ways.
2. $(...): syntax and basic behavior
$(command) starts a subshell in which command runs completely as its own script, with its own environment as a copy of the calling process. Bash collects everything written to standard output as the result of the substitution, automatically stripping one or more trailing newline characters at the end, while newlines within the text itself are preserved.
Because $(...) opens and closes a unique pair of parentheses, Bash's parser can unambiguously recognize the content as a nested context in which quotes, escape characters, and even further $(...) constructs get reinterpreted independently of the surrounding context, similar to nested parentheses in a programming language.
current_branch=$(git rev-parse --abbrev-ref HEAD)
echo "Branch: $current_branch"
files_changed=$(git diff --name-only "$current_branch" main | wc -l)
echo "Files changed: $files_changed"
3. Backticks: syntax and their escaping quirks
The backtick notation `command` produces the same result as $(command) in the simplest case, but does not use a unique pair of brackets, only two identical backtick characters as start and end markers. Bash's parser therefore has to recognize the second backtick purely textually as the end, which introduces special rules for any contained backslash that differ from the behavior inside $(...).
A backslash before a dollar sign, another backtick, or a backslash itself gets interpreted once already at the backtick level before the actual command ever sees it, leading to behavior many Bash users find unintuitive and that simply does not happen with $(...), because there each level cleanly owns its own bracket.
# Identical in simple cases
echo `date +%F`
echo $(date +%F)
# Differ once a backslash is involved
echo `echo \\$HOME` # prints $HOME, not the path
echo $(echo \$HOME) # also prints $HOME, but more consistently readable
4. Nesting: why $(...) nests cleanly and backticks do not
Because $(...) has a unique opening and closing character, the parser correctly resolves arbitrarily deeply nested calls like $(command1 $(command2 $(command3))), each level gets its own pair of brackets and stays visually traceable for the reader. Editors with bracket highlighting can also visualize this structure automatically, which considerably eases debugging in complex scripts.
Backticks, by contrast, require escaping the inner backticks with a backslash at every nesting level so the parser can tell them apart from the outer level, for example `command1 \`command2\``. Past two nesting levels this expression becomes barely readable, and past three levels it is effectively unmaintainable in practice, which is why backticks are unsuitable for anything beyond the simplest cases.
5. Why $(...) is almost always the better choice
Beyond nesting, several other reasons favor $(...): in many fonts a backtick is hard to distinguish visually from a plain single quote, which invites copy-paste errors from formatted documents or chat messages, whereas $( and ) are visually unambiguous. On top of that, virtually every common linter such as ShellCheck treats $(...) as the recommended form and explicitly flags backticks as outdated.
ShellCheck consistently flags backticks with rule SC2006, recommending they be replaced with $(...), and most style guides for production Bash scripts, such as the Google Shell Style Guide, explicitly ban backticks. Anyone writing new Bash code today should use $(...) without exception and only recognize backticks as equivalent but outdated when reading older scripts.
6. The shared side effect: command substitution always runs in a subshell
Regardless of whether $(...) or backticks are used, Bash runs the contained command in its own subshell in both cases, a child process with a copy of the environment at the time of the call. Every variable assignment, every cd call, and every shell option change made inside that substitution only affects the subshell and is lost the moment the substitution finishes and the child process exits.
This side effect surprises many people who try to fit a counter increment or a global status variable update inside a command substitution, for example wrongly assuming result=$(counter=$((counter+1)); echo done) would increment the outer counter variable. In reality only the copy inside the subshell increments, the outer variable stays unchanged, a behavior identical to the subshell problem seen with pipes.
counter=0
result=$(counter=$((counter+1)); echo "inside: $counter")
echo "$result"
echo "outside: $counter"
# inside: 1
# outside: 0 <- the change was lost together with the subshell
7. Practical pitfalls: word splitting and the swallowed newline
An unquoted $(command) is subject to word splitting and pathname expansion, exactly like an unquoted variable, so output containing spaces or glob characters can unexpectedly break apart into multiple arguments or expand against existing files. The rule is therefore the same as with variables: wrap "$(command)" in double quotes, unless splitting into multiple words is explicitly desired.
A second surprise concerns multi-line output: command substitution only strips trailing newlines at the very end of the whole output, not newlines in the middle. Anyone capturing the output of ls or find and expecting it to automatically become a space-separated list overlooks that newlines still sit inside it, only getting broken into separate words once it is inserted unquoted and undergoes word splitting.
# Unquoted: word splitting breaks the output apart
files=$(find . -maxdepth 1 -name "*.log")
for f in $files; do echo "processing: $f"; done # breaks on spaces in names
# Quoted and with mapfile: robust against spaces in filenames
mapfile -t files < <(find . -maxdepth 1 -name "*.log")
for f in "${files[@]}"; do echo "processing: $f"; done
8. When to avoid command substitution
Every command substitution starts at least one extra process for the subshell itself and typically another for the contained command, which noticeably costs time inside a tight loop running thousands of iterations. A result=$(echo "$x" | tr 'a-z' 'A-Z') inside a loop with 10000 iterations spawns 20000 extra processes, while the same task using the built-in parameter expansion ${x^^} needs no external process at all.
As a rule of thumb: for individual calls, the process overhead is irrelevant and the readability of command substitution outweighs any performance concern. Inside loops with a high iteration count, though, it pays to check whether a native Bash parameter expansion, an array, or a single call outside the loop can produce the same output without spawning a new subshell on every iteration.
9. $(...) and backticks compared directly
Both notations produce identical results in simple cases but differ significantly in nesting, readability, and tooling support. The table below summarizes the decision criteria for new Bash code.
| Criterion | $(...) | Backticks | Recommendation |
|---|---|---|---|
| Nesting | Arbitrarily deep, clearly readable | Requires backslash escaping per level | $(...) for any nesting |
| Readability in an editor | Unambiguous brackets | Easily confused with quotes | Prefer $(...) |
| ShellCheck compliance | Recommended form | SC2006, flagged as outdated | $(...) for new code |
| Subshell behavior | Identical to backticks | Identical to $(...) | No difference, applies to both |
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Shell scripts that hold up in production?
We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.
Code Review
ShellCheck analysis and manual review for critical Bash pattern violations.
Refactoring
Retrofitting error handling, logging and safe file operations.
CI Integration
Wiring ShellCheck and BATS into pipelines and building regression tests.
10. Summary
Command Substitution: The Essentials at a Glance
Basic behavior
$(command) and `command` both produce the command's standard output, with trailing newlines stripped.
Nesting
$(...) nests arbitrarily deep, backticks require backslash escaping from the second level onward and quickly become unreadable.
Subshell trap
Variable changes inside a command substitution are lost because they run inside their own subshell.
Recommendation
Use $(...) exclusively for new code, only recognize backticks as equivalent but outdated when reading older scripts.