Introduction to Command Substitution
In Fish shell, command substitution is the mechanism that allows you to replace a command with its output. It’s a core concept that turns static scripts into dynamic, data-driven workflows. Understanding it deeply unlocks the full power of Fish for both interactive use and scripting.
What Is Command Substitution?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Command substitution is the process of executing a command and using its standard output as part of another command or variable assignment. In Fish, the syntax is simple and consistent: wrap any command in parentheses ( ... ). Unlike POSIX shells like Bash that use backticks or $(), Fish uses (command) exclusively. This uniformity reduces confusion and makes scripts more readable.
Key Difference from Other Shells
In Bash, you might write $(date) or `date`. In Fish, you just write (date). No dollar sign, no backticks. This is a deliberate design choice to keep the language clean and predictable. The parentheses serve as a clear visual boundary for the subcommand.
Why It Matters
Command substitution turns the shell into a powerful glue language. You can dynamically generate filenames, set variables based on system state, and build complex pipelines where one command’s output feeds directly into another’s arguments. Without it, scripts would be static, brittle, and unable to react to their environment. It enables automation that adapts, scales, and handles real-world data.
Basic Usage
Simple Output Capture
Use parentheses to run a command and insert its output directly into another command’s argument list:
echo (date)
# Prints the current date and time
Here, date runs, its stdout is captured, and that string becomes an argument to echo.
Variable Assignment
Store command output in a variable using set. Fish automatically splits the output into a list, with each line becoming one element:
set files (ls *.txt)
# $files now contains each .txt filename as a separate element
If there are no matches, the list is empty. This list behavior is fundamental to Fish.
Inside Command Arguments
You can place substitution anywhere an argument is expected:
echo "Current user: (whoami)"
cp (which somefile) /destination/
In Conditionals and Loops
Command substitution works seamlessly within if, while, and for constructs:
if test (count (ls -1 *.jpg)) -gt 10
echo "More than 10 JPEGs found"
end
Handling Multiple Lines and Lists
Fish treats newline-separated output as a list by default. When you assign a command’s output to a variable, each line becomes an independent element. This is incredibly useful for iterating over files or lines, but it can surprise you if you expect a single string containing newlines.
Example: List Expansion
set contents (cat myfile.txt)
# If myfile.txt has three lines, $contents is a list of three items
printf '%s\n' $contents
# Prints each line individually
Preserving Newlines as a Single String
To keep the entire output as one string (including newlines), pipe through string collect:
set contents (cat myfile.txt | string collect)
# $contents is now a single string with embedded newlines
Alternatively, you can use command substitution directly inside double quotes when passing to another command, but Fish variable assignment does not require quotes. The string collect approach is explicit and safe.
Advanced Techniques
Nested Command Substitutions
Fish allows you to nest parentheses without any escaping. The innermost command runs first, its output becomes the arguments for the next outer command, and so on:
echo (basename (dirname (which fish)))
# Runs which fish → /usr/bin/fish
# dirname → /usr/bin
# basename → bin
# echo finally prints "bin"
Nesting can be as deep as needed, but keep readability in mind.
Capturing Standard Error
By default, command substitution only captures stdout. If you need to capture stderr as well, redirect stderr to stdout inside the substitution:
set output (command 2>&1)
# Both stdout and stderr are captured into $output
To capture only stderr and discard stdout, swap the streams:
set errors (command 2>&1 >/dev/null)
Working with Exit Status
Command substitution does not interfere with $status. After a substitution runs, $status reflects the exit code of the substituted command:
echo (ls nonexistent)
set stat $status
# $stat is non-zero, and the error message went to stderr (not captured)
If you need both output and exit status, consider running the command separately before capturing:
command -q some_cmd; set exit_code $status
set result (some_cmd)
Using Substitutions in Math
Combine with the math command for dynamic arithmetic:
math (count *.txt) + 1
# Adds 1 to the number of .txt files
Substitution as Loop Input
Generate loop iterators dynamically:
for file in (find . -name '*.conf')
echo "Config file: $file"
end
This runs find and splits its output into lines, iterating over each one.
Best Practices
- Be aware of list expansion: Always consider whether a command’s output will be treated as a list. If you need a single string (e.g., a file’s whole content with newlines), pipe to
string collect. - Handle empty output gracefully: Test for non-empty output with
test -n (command)or check the exit status before using the result to avoid empty variable surprises. - Use
(), never$()or backticks: Fish’s syntax is clean; using Bash-style substitutions will cause syntax errors. Stick to parentheses. - Trim unwanted whitespace: Commands like
git branch --show-currentmay output a trailing newline. Usestring trimto clean up:set branch (git branch --show-current | string trim). - Avoid excessive nesting: Deeply nested substitutions harm readability. Assign intermediate variables for clarity.
- Capture exit status separately when needed: For critical commands, run them first and store
$statusexplicitly; do not rely on the exit status of a substitution alone if the output is also crucial. - Quote when passing to external commands: While Fish variables don’t need quotes for assignment, using
"$var"inside a command argument ensures the entire list element is treated as one token, preventing unintended splitting.
Common Pitfalls
One of the most frequent pitfalls is expecting a single string from a multi-line command. For example, set text (cat file) splits file lines into list items. If you later use $text in a context expecting a single block (like writing to a file), you’ll get multiple arguments. Always use string collect when the original structure matters.
Another issue: command substitution inside an if condition. if (ls *.txt) won’t reliably detect file existence because ls sends errors to stderr, which isn’t captured and doesn’t affect the condition’s boolean result. Instead, use count or test -f.
Be mindful of command output that contains wildcard characters. Fish does not perform additional glob expansion on substituted output, but if you pass the result unquoted to another command, word splitting into list elements may interact unexpectedly. Quoting or using appropriate list handling avoids this.
Conclusion
Command substitution in Fish is a clean, powerful feature that eliminates the syntactic clutter of legacy shells. Its parentheses-only syntax, combined with the shell’s native list handling, enables you to build dynamic and expressive scripts with minimal friction. By understanding how output becomes lists, when to preserve strings, and how to manage exit statuses, you can write robust automation that responds intelligently to its environment. Master command substitution, and you unlock the full potential of Fish scripting.