← Back to DevBytes

Zsh Scripting: Command Substitution Complete Guide

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:

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles