← Back to DevBytes

Fish Scripting: I/O Redirection Complete Guide

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:

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

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.

🚀 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