Understanding I/O Redirection in Fish Shell
Input/Output redirection is the mechanism by which you control where a command reads its input from and where it sends its output. In the Fish shell, redirection syntax is intentionally clean and modern, shedding some of the cryptic shorthand found in POSIX shells like Bash. Yet it remains fully powerful, giving you precise control over standard input (stdin, file descriptor 0), standard output (stdout, file descriptor 1), and standard error (stderr, file descriptor 2).
Why I/O Redirection Matters
Without redirection, every command prints to your terminal and reads from your keyboard. That works for interactive use but falls apart the moment you need to:
- Log output to a file for later inspection
- Chain commands so the output of one becomes the input of another
- Suppress noise by discarding error messages
- Feed data into a command programmatically from a file or another process
- Capture both output and errors separately or together
Fish gives you a streamlined syntax for all of these scenarios. Once you internalize the patterns, your scripts become more robust and your one-liners more expressive.
Standard Output Redirection
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Redirecting stdout to a File with >
The most common operation is sending a command's standard output to a file. In Fish, you use the > operator followed by the target path. If the file does not exist, Fish creates it. If it does exist, Fish truncates it (empties it before writing).
# Write the list of files in the current directory to a file
ls > contents.txt
# Overwrite a configuration file with new settings
echo "host = example.com" > config.ini
# Create an empty file (or truncate an existing one)
> empty.log
Fish also supports the familiar 1> syntax (explicit stdout) if you prefer being explicit, but the simple > is idiomatic.
Appending with >>
When you want to add content to the end of an existing file rather than overwriting it, use the double arrow >>.
# Append a timestamped log entry
echo "(date): Backup started" >> backup.log
# Collect output from multiple commands into one file
echo "--- Header ---" >> report.txt
cat chapter1.txt >> report.txt
cat chapter2.txt >> report.txt
Clobber Protection: noclobber
Fish has a safety feature called noclobber that prevents accidental overwrites. When enabled, > will refuse to overwrite an existing file. You can toggle it with:
# Check current status
set --show noclobber
# Enable protection
set -U noclobber
# Disable protection
set -U noclobber false
When noclobber is active, you can force an overwrite with a ! after the redirect operator:
echo "forced overwrite" >! protected-file.txt
Standard Error Redirection
Redirecting stderr Alone
To capture only error messages while letting stdout still print to the terminal, redirect file descriptor 2 explicitly:
# Send errors to a log, keep normal output visible
find / -name "*.log" 2> errors.log
# Append errors to a growing log
npm install 2>> npm-errors.log
Redirecting Both stdout and stderr
Fish offers two clean ways to merge stderr into stdout so both streams end up in the same place.
Method 1 — Redirect stderr to stdout's target:
# Both streams go to the file
make all > build.log 2>&1
Method 2 — The Fish-only &> shortcut:
# Identical result, more concise
make all &> build.log
# Append both streams
make all &>> build.log
Both methods are valid. The &> form is a Fish extension that many users find more readable.
Discarding Output: /dev/null
To throw away output entirely, redirect to /dev/null:
# Run quietly, discard all output
wget -q https://example.com/file.tar.gz > /dev/null 2>&1
# Discard only errors
find / -name "*.tmp" 2> /dev/null
Standard Input Redirection
Reading stdin from a File with <
The < operator feeds the contents of a file into a command as its standard input:
# Count lines in a file
wc -l < data.csv
# Feed a SQL script into the client
psql -U alice -d mydb < migration.sql
# Provide input to a program that reads from stdin
grep "error" < logfile.txt
Notice the subtle difference: when you run grep "error" logfile.txt, grep opens the file directly and knows its name. When you use <, grep sees only a stream of bytes with no filename — this matters for commands that behave differently based on whether they receive a filename argument or raw stdin.
Combining Redirections
You can mix input and output redirections freely:
# Process a template and save the result
sed 's/__NAME__/Alice/g' < template.txt > welcome.txt
# Run a command that reads from one file, writes to another, and logs errors
sort < unsorted.txt > sorted.txt 2> sort-errors.log
Pipes in Fish
Basic Piping
Pipes connect the stdout of one command to the stdin of the next. Fish uses the familiar | character:
# Classic pipeline
cat access.log | grep "404" | cut -d' ' -f1 | sort | uniq -c
# Pipe into a builtin
echo "scale=2; 355/113" | bc
Redirecting stderr Through a Pipe
By default, only stdout travels through a pipe. To also pipe stderr, merge it first:
# Pipe both stdout and stderr into grep
make 2>&1 | grep -i error
# Fish shortcut for the same thing
make &| grep -i error
The &| syntax is unique to Fish and is equivalent to 2>&1 | in other shells.
Piping to a File Descriptor Destination
Sometimes you want to pipe into a redirect target rather than a command. Fish handles this naturally:
# Pipe stdout into a file, let stderr pass through
make |&> build.log
# This is equivalent to: make 2>&1 | cat > build.log
Here Documents and Here Strings
Here Documents (<<)
Fish supports multiline string literals passed as stdin via here documents. The delimiter word can be anything, and the block ends when that word appears alone on a line:
cat << EOF
This is line one.
This is line two.
Variables like $HOME are expanded.
EOF
To suppress variable expansion, quote the delimiter:
cat << 'EOF'
Literal text — $HOME will NOT be expanded.
EOF
Here Strings (<<<)
For single-line input, Fish provides the here string, which is cleaner than echo-pipe patterns:
# Feed a single string to a command's stdin
read -l first second <<< "hello world"
# Pass a variable's value as stdin
set my_var "some data"
base64 <<< $my_var
Here strings are particularly useful with the read builtin for parsing variables within scripts.
Process Substitution
The psub Function
Fish does not use the Bash-style <() or >() syntax. Instead, it provides the psub command, which returns a file descriptor path representing the output (or input) of a subprocess:
# Compare the output of two commands without temporary files
diff (command_a | psub) (command_b | psub)
# Feed the output of a pipeline as a file argument
vim (grep -rl "TODO" src/ | psub)
Under the hood, psub creates a named pipe (FIFO) in /tmp and returns its path. The command inside the parentheses runs in a subshell, and its output flows through that pipe. This is incredibly handy for commands that expect actual filenames rather than stdin.
# Use process substitution to diff two sorted lists in memory
diff (sort list_a.txt | psub) (sort list_b.txt | psub)
# Feed a compressed archive listing to a file-based tool
tar -tvf (curl -s https://example.com/archive.tar.gz | psub)
Advanced File Descriptor Manipulation
Understanding File Descriptors
Every process starts with at least three open file descriptors: 0 (stdin), 1 (stdout), and 2 (stderr). Fish allows you to redirect any of these, and you can even open additional descriptors for special purposes:
# Open file descriptor 3 for reading
exec 3< datafile.txt
read -l line <&3
exec 3<&- # Close it
# Open file descriptor 4 for writing
exec 4> output.txt
echo "debug info" >&4
exec 4>&- # Close it
This technique is useful for logging within scripts while keeping stdout clean for actual output:
#!/usr/bin/env fish
# Open a dedicated log descriptor
exec 3> script.log
function log
echo "[$(date)] $argv" >&3
end
log "Script started"
# Normal stdout remains untouched for piping or display
echo "Result: 42"
log "Script finished"
Redirecting Multiple Descriptors at Once
Fish supports redirecting several file descriptors in a single command:
# stdout to one file, stderr to another
make > stdout.log 2> stderr.log
# stdout and stderr to separate files, fd 3 to yet another
program 1> out.txt 2> err.txt 3> debug.txt
Practical Script Patterns
Pattern 1: Silent Command with Error Checking
# Run a command quietly but capture errors for later inspection
if not command -v some-tool >/dev/null 2>&1
echo "Error: some-tool is not installed" >&2
exit 1
end
Pattern 2: Logging Wrapper Function
function run_logged
set -l cmd $argv
set -l logfile "task_"(date +%Y%m%d_%H%M%S)".log"
echo "Running: $cmd"
eval $cmd &>> $logfile
set -l exit_code $status
echo "Exit code: $exit_code" >> $logfile
return $exit_code
end
run_logged make clean
run_logged make build
Pattern 3: Reading stdin Conditionally
# Check if stdin is a pipe (not a terminal) and process accordingly
if isatty stdin
echo "Interactive mode — please enter data:"
read -l user_input
else
# Data is being piped in
read -l piped_input
end
Pattern 4: Temporary File Descriptors for In-Place File Editing
# Read and write a file without subshell tricks
function prepend_header
set -l file $argv[1]
set -l header $argv[2]
set -l temp (mktemp)
echo $header > $temp
cat $file >> $temp
mv $temp $file
end
prepend_header config.toml "# Auto-generated configuration"
Pattern 5: Collecting Both Streams Separately
# Capture stdout in one variable, stderr in another
set -l output (command 2>/tmp/.fish_stderr.$$)
set -l errors (cat /tmp/.fish_stderr.$$)
rm -f /tmp/.fish_stderr.$$
echo "STDOUT: $output"
echo "STDERR: $errors"
Best Practices
- Prefer explicit redirects over
/dev/nullshortcuts. Writing&>/dev/nullis clear and intentional. Avoid relying on the fact that a command happens to be quiet by default. - Use
&>and&|in Fish. These Fish-specific operators reduce visual clutter and make your intent immediately obvious to readers familiar with the shell. - Leverage
psubinstead of temporary files. Process substitution eliminates the need for manual cleanup and reduces disk I/O. It's one of Fish's most underused features. - Enable
noclobberin interactive sessions. Set it universally withset -U noclobberto guard against accidental data loss. - Quote your redirect targets. If the filename contains spaces or special characters, wrap it in quotes:
echo "data" > "my file.txt". - Use here strings for single-line input.
read <<< $varis cleaner and faster thanecho $var | read, which spawns an unnecessary subshell. - Close custom file descriptors when done. Leaving dangling descriptors can cause obscure bugs in long-running scripts. Use
exec N<&-orexec N>&-explicitly. - Test
isatty stdinbefore reading interactively. This prevents your script from hanging when it's used in a pipeline and there's no user to provide input.
Common Pitfalls
Pitfall 1: Order of Redirections Matters
When combining redirects, Fish processes them left to right. This can lead to subtle bugs:
# WRONG: stderr goes to the file, but stdout doesn't merge into it
make 2>&1 > build.log
# stderr is pointed to wherever stdout CURRENTLY goes (the terminal),
# THEN stdout is redirected to the file. stderr stays on the terminal.
# CORRECT: redirect stdout first, then point stderr to that same file
make > build.log 2>&1
Pitfall 2: Pipes and Subshells
In Fish, each pipeline component runs in a subshell. Variables set inside a pipeline segment are not visible to the parent:
# This will NOT work as some might expect
echo "hello" | read -l message
echo $message
# Prints nothing — the read happens in a subshell, the variable is lost
# Workaround: use a here string instead
read -l message <<< "hello"
echo $message
# Prints: hello
Pitfall 3: Forgetting that > Truncates
It's easy to accidentally destroy a file by using > when you meant >>. Consider using noclobber as a safety net, or double-check your redirect arrows in scripts that handle irreplaceable data.
Pitfall 4: psub and Command Order
The psub call creates the named pipe and starts the subprocess immediately. Ensure the consuming command reads from it before the producing command finishes and the pipe closes:
# This works because diff starts reading before sort finishes
diff (sort huge1.txt | psub) (sort huge2.txt | psub)
# This might fail if the producer runs too quickly and the pipe buffer fills
# In practice, the OS handles buffering, but be aware of edge cases with
# extremely fast producers and slow consumers.
Conclusion
Fish's I/O redirection system strikes a careful balance between POSIX compatibility and modern ergonomics. The classic operators >, <, >>, and 2> work exactly as you'd expect from any Unix shell, while Fish-exclusive additions like &>, &|, here strings, and psub eliminate boilerplate and reduce cognitive overhead. By mastering these redirection primitives — knowing when to merge streams, how to feed data into commands without temporary files, and how to guard against accidental overwrites — you equip yourself to write Fish scripts that are concise, correct, and a pleasure to maintain. The patterns and pitfalls covered here form a complete mental model for all your input and output routing needs in the Fish shell.