Introduction to Command Substitution in Zsh
Command substitution is one of the most powerful features in Zsh scripting. It allows you to execute a command and use its output directly within another command or assign it to a variable. Whether you are automating system tasks, parsing logs, or building complex pipelines, mastering command substitution will dramatically improve the quality and flexibility of your scripts.
In this guide, we will explore what command substitution is, why it matters, the syntax options available in Zsh, practical use cases, and best practices to keep your scripts clean and reliable.
What Is Command Substitution?
Command substitution is a shell mechanism that runs a command, captures its standard output, and substitutes that output in place of the command. The trailing newline characters produced by the command are stripped automatically, which makes the output easy to embed in strings or assign to variables.
Zsh supports two syntaxes for command substitution:
$(command)— the modern POSIX-compliant form.`command`— the legacy backtick form, retained for compatibility.
The modern $(...) form is strongly preferred because it is easier to read, supports nesting without escaping, and works consistently across modern shells.
Why Command Substitution Matters
Without command substitution, capturing the output of a command would require temporary files or awkward pipelines. Command substitution lets you treat the output of any command as a first-class value in your script. This unlocks patterns such as:
- Storing dynamic values like the current date, hostname, or git branch in variables.
- Building file lists and iterating over them in loops.
- Constructing command arguments dynamically based on runtime conditions.
- Combining the output of multiple commands into a single string or array.
Because Zsh also has powerful array handling, command substitution pairs naturally with arrays to produce concise, expressive scripts.
Basic Syntax and Usage
The Modern Form: $(...)
The simplest way to use command substitution is to assign the output of a command to a variable:
#!/usr/bin/env zsh
current_date=$(date +%Y-%m-%d)
echo "Today is $current_date"
When the script runs, Zsh executes date +%Y-%m-%d, captures the output, and assigns it to current_date. The echo command then prints the substituted value.
The Legacy Form: Backticks
The backtick form achieves the same result but is harder to read and difficult to nest:
#!/usr/bin/env zsh
current_date=`date +%Y-%m-%d`
echo "Today is $current_date"
Avoid this form in new scripts. It exists primarily for compatibility with older shell code.
Using Substitution Inline
Command substitution is not limited to variable assignment. You can embed it directly inside other commands:
#!/usr/bin/env zsh
echo "There are $(ls | wc -l) files in this directory."
Here, ls | wc -l runs first, and its numeric output is inserted into the string before echo prints it.
Nesting Command Substitution
One of the major advantages of the $(...) syntax is that nesting is straightforward. You can place one command substitution inside another without any escaping:
#!/usr/bin/env zsh
# Find the file count inside the directory returned by 'pwd'
file_count=$(ls $(pwd) | wc -l)
echo "Files in $(pwd): $file_count"
With backticks, the same nesting would require backslash-escaping the inner backticks, which quickly becomes unreadable and error-prone.
Command Substitution and Arrays
Zsh, unlike many other shells, does not perform word splitting on unquoted variable expansions by default. However, command substitution still produces a single string. To convert the output into an array of lines, use the ${(f)...} parameter expansion flag:
#!/usr/bin/env zsh
# Split output of 'ls' into an array, one entry per line
files=(${(f)"$(ls)"})
for file in $files; do
echo "Found: $file"
done
The (f) flag splits the string on newlines. The double quotes around the command substitution preserve any internal whitespace in each line.
Splitting on Other Delimiters
If your output uses a different delimiter, such as commas or colons, use the ${(s/delimiter/)...} flag:
#!/usr/bin/env zsh
csv_line="apple,banana,cherry"
fruits=(${(s/,/)csv_line})
for fruit in $fruits; do
echo "Fruit: $fruit"
done
Handling Whitespace and Newlines
Command substitution strips trailing newlines from the captured output. This is usually desirable, but it can cause surprises when the output contains meaningful blank lines at the end. Consider this example:
#!/usr/bin/env zsh
output=$(printf "line1\nline2\n\n")
echo "Captured: [$output]"
The output will be [line1
line2] — the trailing newlines are removed. If you need to preserve them, you can append a sentinel character and strip it manually:
#!/usr/bin/env zsh
output=$(printf "line1\nline2\n\n"; echo "END")
output=${output%END}
echo "Captured: [$output]"
Leading and internal whitespace, however, is preserved exactly as produced by the command.
Practical Examples
Example 1: Capturing the Current Git Branch
#!/usr/bin/env zsh
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ -n $branch ]]; then
echo "You are on branch: $branch"
else
echo "Not inside a git repository."
fi
This pattern is commonly used in custom shell prompts and deployment scripts.
Example 2: Building a Dynamic File List
#!/usr/bin/env zsh
# Find all .log files modified in the last 24 hours
recent_logs=(${(f)"$(find /var/log -name '*.log' -mtime -1)"})
for log in $recent_logs; do
echo "Processing $log"
# Do something with each log file
done
Example 3: Using Substitution in Arithmetic
#!/usr/bin/env zsh
line_count=$(wc -l < /etc/passwd)
echo "Total users configured: $((line_count))"
Because wc -l may include leading whitespace, Zsh handles it gracefully inside arithmetic expansion, but you can also trim it explicitly with ${line_count// /} if needed.
Example 4: Combining Multiple Commands
#!/usr/bin/env zsh
summary="Host: $(hostname), Kernel: $(uname -r), Uptime: $(uptime -p)"
echo "$summary"
Multiple substitutions can appear in a single string, each evaluated independently.
Best Practices
Always Prefer $(...) Over Backticks
The $(...) form is clearer, supports nesting, and is the standard in modern shell scripting. Reserve backticks only for maintaining legacy code.
Quote Your Substitutions
Although Zsh does not word-split unquoted expansions by default, quoting is still a good habit, especially if your script may be ported to bash or sh. Quoting also protects against unexpected behavior when filenames contain spaces:
#!/usr/bin/env zsh
target="$(pwd)/important file.txt"
cp "$target" /tmp/backup/
Avoid Command Substitution for Side Effects
Command substitution captures stdout. If a command produces output you do not need, redirect it to /dev/null or to a file. Similarly, redirect stderr when appropriate:
#!/usr/bin/env zsh
# Suppress stderr but capture stdout
result=$(grep "pattern" file.txt 2>/dev/null)
Be Mindful of Performance
Each command substitution spawns a subshell. Inside tight loops, this can become expensive. Where possible, compute values once outside the loop:
#!/usr/bin/env zsh
# Inefficient: spawns 'date' on every iteration
for i in {1..1000}; do
echo "$(date +%s): iteration $i"
done
# Better: compute once
timestamp=$(date +%s)
for i in {1..1000}; do
echo "$timestamp: iteration $i"
done
Use Arrays for Multi-Line Output
When a command returns multiple lines, store the result in an array using ${(f)...} rather than iterating over a raw string. This avoids subtle bugs with embedded newlines and makes your intent explicit.
Check Exit Status When It Matters
Command substitution captures output, not exit status. However, you can still inspect $? immediately after the assignment:
#!/usr/bin/env zsh
content=$(cat /etc/shadow 2>/dev/null)
if (( $? != 0 )); then
echo "Failed to read file (permission denied?)"
exit 1
fi
For more robust error handling, consider using set -e or explicit checks immediately after each substitution.
Common Pitfalls
- Forgetting that trailing newlines are stripped. If your logic depends on exact output formatting, account for this behavior.
- Assuming word splitting. In Zsh, unquoted substitutions do not split on whitespace by default. Use
${(f)...}or${(s/.../)...}when you need splitting. - Ignoring stderr. Error messages from the substituted command will leak to the terminal unless redirected.
- Overusing subshells in loops. Each substitution forks a subshell; minimize this inside performance-critical code.
Conclusion
Command substitution is a foundational technique in Zsh scripting that turns the output of any command into a usable value. By understanding the $(...) syntax, mastering array splitting flags like ${(f)...}, and following best practices around quoting, error handling, and performance, you can write scripts that are both concise and robust. Whether you are building a quick automation helper or a complex deployment pipeline, command substitution will be one of the tools you reach for most often — so it is well worth using it deliberately and correctly from the start.