skip to content

Command Substitution

$(...) runs a command in a subshell and substitutes its stdout, stripping trailing newlines and — if you drop the quotes — splitting the result into words. The senior follow-up is exit status: in `x=$(f)` the assignment's own status hides the failure unless you split the declaration from the assignment.

part ofBashoverview, primer and where to startread it →
on this pageshow

questions

4

In a bash script, what is the difference between writing a command substitution as `$(cmd)` and as the older backtick form `` `cmd` ``, and why does nesting one inside another behave differently?

level: juniorimportance: must knowfreq 68%

answer

  1. two spellings, one behaviour
  2. the difference is in parsing
  3. one form needs escaping to nest
  4. backslashes multiply per level
  5. ShellCheck SC2006 flags the old one

basics

~20 s

Both run a command and substitute its standard output, but $(...) nests and quotes cleanly because everything up to the matching ) is parsed as a fresh command, while backticks need a backslash escape for every nested level and mangle backslashes and inner quotes.

solid answer

~40 s

Functionally they do the same job: run the command in a subshell, capture its standard output, strip the trailing newlines, and substitute the result. The difference is parsing. With `$( )`, bash finds the matching close paren and parses the inside as a brand-new command, so nesting and quoting just work: `dir=$(dirname "$(readlink -f "$0")")`. Backticks are parsed by scanning for the next backtick, so an inner substitution has to be escaped — `` `echo \`date\`` `` — and each nesting level adds another layer of backslashes. Backticks also treat `\` specially, so a literal backslash or `$` inside them behaves differently than you expect. `$( )` is standard POSIX, not a bashism, so there is essentially no reason left to write backticks in new code; ShellCheck flags them as SC2006.

code

bash · 7 lines
bash
# $( ) nests and quotes without escaping
script_dir=$(dirname "$(readlink -f "$0")")
echo "$script_dir"

# the same nesting with backticks needs escapes
script_dir=`dirname "\`readlink -f "$0"\`"`
echo "$script_dir"

go deeper

for a junior

Know both spellings on sight, and say plainly that you write $(...) in new code because it nests and reads better. Being able to name the backtick form when you meet it in an old script is enough at this level.

for a middle

Be ready to explain the parsing difference: $( ) is scanned to its matching paren and parsed fresh, backticks are scanned to the next backtick with an extra backslash-processing layer. Show a nested example both ways.

for a senior

Demonstrate that you treat the choice as a code-standard matter: ShellCheck SC2006 flags backticks, and you fix them during review along with the quoting of the result. Explain the shared properties that actually cause bugs — subshell, newline stripping, stdout only.

for a principal

Own the standardisation call: a repo-wide lint rule plus formatter removes an entire class of escaping bugs at zero runtime cost, and the portability objection to $( ) has not been real since POSIX.1-2001. Decide when mechanical rewrites of legacy scripts are worth the review churn.

## What command substitution does Command substitution runs a command and replaces the whole construct with that command's standard output. Both spellings — `$(command)` and `` `command` `` — do exactly that: bash runs the command in a subshell, collects everything it writes to stdout (stderr is *not* captured and goes straight to the terminal unless you redirect it), removes all trailing newlines, and substitutes what is left in place of the construct. ```bash now=$(date +%F) # now=2026-08-20 old=`date +%F` # identical result ``` The result is then subject to the usual expansion rules — if you leave it unquoted it is word-split and glob-expanded, which is why the habit is `"$(cmd)"` with quotes. That splitting behaviour is a topic of its own; the point here is that it applies identically to both spellings. ## The real difference is the parser The two forms differ in *how bash finds the end of the command and how it treats characters inside*. For `$( )`, bash scans forward keeping track of nesting and quoting until it finds the matching `)`. Everything between the parentheses is then parsed as a completely fresh command, exactly as if it appeared on its own line. Nothing inside needs escaping on account of being inside a substitution. For backticks, bash scans forward for the *next* backtick character. There is no nesting to track — a backtick is a backtick. Inside the backticks, backslash keeps its literal meaning except when it precedes `$`, `` ` `` or `\`, where it is consumed as an escape. That extra escape layer is where all the pain comes from. ## Nesting Because `$( )` tracks its own nesting, this is unremarkable: ```bash script_dir=$(dirname "$(readlink -f "$0")") ``` The same thing in backticks needs the inner pair escaped so the parser does not treat the second backtick as the closing one: ```bash script_dir=`dirname "\`readlink -f "$0"\`"` ``` And it gets worse per level: three levels deep, backslashes multiply, because each layer of parsing eats one. In practice nobody writes correct three-level backticks; they write `$( )`. ## Quoting Inside `$( )`, double quotes are just double quotes — the inner `"$0"` above is a normal quoted expansion, independent of the outer quotes. Inside backticks that appear within double quotes, the quoting interacts, and the classic result is that an inner quoted string terminates the outer one or a backslash disappears. A concrete example of the backslash rule: ```bash echo "$(echo '\$')" # prints \$ echo "`echo '\$'`" # the backslash is consumed differently ``` The rule to remember is that backticks add one round of backslash processing before the command is parsed; `$( )` adds none. ## Readability and tooling Backticks are visually easy to confuse with single quotes in a monospace font, and hard to see in a dense pipeline. ShellCheck emits SC2006 ("Use `$(...)` notation instead of legacy backticks") on every occurrence, so a linted codebase will not contain them. `$( )` has been in POSIX since POSIX.1-2001, so choosing it is not a bashism and does not cost portability — a `#!/bin/sh` script on dash, ash or busybox handles it fine. The only place you still see backticks is very old scripts and pre-1990s Bourne shells. ## What is the same, and worth not forgetting Everything else about the two forms is identical, and those shared properties are usually what the interview is really driving at: - The command runs in a **subshell**, so variables it assigns do not survive. - **All** trailing newlines are stripped, not just one. - Only **stdout** is captured; stderr passes through. Use `2>&1` inside the substitution if you want both. - The substitution's exit status is the exit status of the command it ran, which matters as soon as you put it on the right-hand side of a declaration. ## The short answer to give "They do the same thing, but `$( )` parses the inside as a fresh command so nesting and quoting are natural, while backticks re-scan for the next backtick and impose an extra backslash layer, so every nested level needs escaping. `$( )` is POSIX and lint-clean; write that."

  • Is `$(...)` a bashism? If I have to target `#!/bin/sh`, do I have to fall back to backticks?
    No. `$( )` has been in POSIX since POSIX.1-2001 and is supported by dash, ash, busybox sh, ksh and zsh, so a `/bin/sh` script can use it safely. Backticks are only needed for genuinely pre-POSIX Bourne shells, which you will not meet in practice. Arrays and `[[ ]]` are bashisms; command substitution with `$( )` is not.
  • Does command substitution capture stderr as well as stdout?
    No — only stdout is captured. Anything the command writes to stderr goes to the script's stderr and is not part of the substituted value, which is why an error message shows on the terminal while the variable ends up empty. If you want both, redirect inside the substitution: `out=$(cmd 2>&1)`. To discard errors instead, `out=$(cmd 2>/dev/null)`.
  • Why do people write `"$(cmd)"` with quotes rather than bare `$(cmd)`?
    Because the substituted text is an ordinary expansion result: unquoted, it is word-split on IFS and then glob-expanded, so output containing spaces or a `*` turns into several arguments or filenames. Quoting keeps the whole output as one word. The only time you deliberately leave it unquoted is when you actually want the output split into separate arguments.

$( ) is like a matched pair of brackets the parser can count; backticks are like using the same character to open and close a quote, so the only way to put one inside another is to escape it.

saying these in an interview costs you the question

  • Claims $( ) and backticks behave differently at runtime
  • Says $( ) is a bashism unavailable in /bin/sh
  • Thinks backticks nest without any escaping
  • Believes command substitution also captures stderr
  • Cannot explain why backticks need backslashes to nest

context

open as a page

A bash script does `body=$(cat report.txt)` on a file that ends with a blank line, and a later `printf '%s' "$body" | wc -c` reports fewer bytes than the file. What does command substitution do to the captured output, and how would you capture it byte-for-byte?

level: middleimportance: should knowfreq 50%

basics

~20 s

Command substitution strips every trailing newline from the captured output, not just the last one, so a file ending in blank lines loses them. To keep them, append a sentinel inside the substitution and remove it afterwards: body=$(cat report.txt; printf x); body=${body%x}.

open as a page

This bash script runs under `set -euo pipefail`, yet it prints `deploying` with an empty version and carries on after `get_version` fails: get_version() { cat VERSION; } main() { local ver=$(get_version); echo "deploying $ver"; } Why does the failure not stop the script, and what is the fix?

level: seniorimportance: should knowfreq 46%

basics

~20 s

local is itself a command, and its exit status — not the substitution's — becomes the status of the line, so the failure is masked and set -e never fires. Declare and assign on separate lines: local ver; ver=$(get_version). ShellCheck flags the original as SC2155.

open as a page

In bash, what does the `$(<file)` form of command substitution do differently from `$(cat file)`, and when would you not use it?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

$(<file) makes bash read the file's contents itself instead of forking and running an external cat, so it is faster and substitutes the same text. It is a bash and ksh extension rather than POSIX, so a strict /bin/sh script keeps $(cat file).

open as a page