← Back to DevBytes

Fish Scripting: Command Substitution Complete Guide

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles