I/O and Pipelines#
Three byte streams (stdin, stdout, stderr) are the
universal interface every Unix program agrees on. Zsh’s job is
to wire them up. Redirection points one stream at a file or
another file descriptor. MULTIOS is zsh’s own twist that lets
one redirection fan out to many destinations. Pipes chain
programs together. Process substitution (<(cmd) and
zsh-only =(cmd)) turns a command’s output into a path.
Named pipes persist a FIFO on disk.
This page covers the wiring layer. For the language-level reading
and writing (read, print, printf, positional
parameters), see Overview.
Streams#
Every process the kernel hands the operator inherits three open file descriptors.
FD |
Name |
Used for |
|---|---|---|
|
|
Input the program reads |
|
|
Normal output (the result) |
|
|
Diagnostics, warnings, errors (a separate channel so pipelines don’t eat them) |
flowchart LR
IN[(stdin · fd 0)] --> P[process]
P --> OUT[(stdout · fd 1)]
P --> ERR[(stderr · fd 2)]
stdout is for the answer; stderr is for the commentary.
The discipline matters because pipelines consume stdout; a
script that prints “log: starting” to stdout pollutes whatever
reads it.
$ print -- "answer" # to stdout
$ print -u2 -- "log: starting" # to stderr (zsh's idiom for >&2)
Redirection#
Redirection rewires a file descriptor for one command before the command runs. Spaces around the operator are optional.
Operator |
Effect |
|---|---|
|
Write stdout to |
|
Append stdout |
|
Read stdin from |
|
Write stderr to |
|
Append stderr |
|
Merge stderr into stdout |
|
Write stdout to stderr |
|
Both streams to |
|
Both streams, appending |
|
Disconnect stdin |
|
Discard stdout |
|
Discard stderr (use sparingly) |
|
Redirect arbitrary fd |
|
Point fd |
|
Close fd |
|
Here-string |
|
Heredoc |
Order matters. cmd > all.log 2>&1 sends both streams to
all.log. cmd 2>&1 > all.log sends stderr to the terminal
and only stdout to the file, because zsh processes redirections
left to right and 2>&1 is evaluated before > all.log
rebinds fd 1.
$ cmd > all.log 2>&1 # both streams → all.log
$ cmd 2>&1 > all.log # stderr → terminal, stdout → all.log
$ cmd &> all.log # shorthand for the first form
MULTIOS#
Zsh’s MULTIOS option (on by default) makes one redirection
fan out to many destinations. Repeat > to tee, repeat <
to concatenate inputs. Cleaner than a real tee call when the
operator just wants two copies of the output.
$ print -- "hello" > a.txt > b.txt # writes both files
$ cat < a.txt < b.txt # concatenates both inputs
The behavior surprises operators coming from bash. unsetopt
MULTIOS reverts to one-target redirection.
exec and persistent redirection#
exec without a command applies a redirection to the rest of
the shell or script. Useful at the top of a script for logging.
$ exec 3< input.txt # open fd 3 for reading
$ read -r first <&3 # read one line from fd 3
$ exec 3<&- # close fd 3
$ exec >script.log 2>&1 # log the rest of the script
Heredocs#
A heredoc uses the next lines of the script as the command’s stdin, up to a delimiter line. Variables and command substitution expand by default; quote the delimiter to turn that off.
$ cat <<EOF
$ user is $USER
$ today is $(date +%F)
$ EOF
user is operator
today is 2026-05-15
Quote the delimiter (<<'EOF') for a literal, unexpanded body.
$ cat <<'EOF' > /etc/cron.d/backup
$ # variables left literal
$ 0 3 * * * root /usr/local/bin/backup --target=$HOME
$ EOF
<<-EOF strips leading tabs from every line so the heredoc
can be indented with the surrounding code.
Here-string (<<<) is the one-line cousin.
$ wc -w <<< "the operator has the conch"
$ grep -E '^[0-9]+$' <<< "$candidate"
Pipes#
A pipeline chains commands so the stdout of each stage feeds
the stdin of the next. Zsh sets up the kernel pipes, forks one
process per stage, and waits for all of them. By default the
pipeline’s exit code is the last stage; setopt PIPE_FAIL
makes it the rightmost non-zero exit.
$ ps -ef | grep sshd | grep -v grep
flowchart LR
A[ps -ef] -->|stdout → stdin| B[grep sshd]
B -->|stdout → stdin| C[grep -v grep]
C --> OUT[(terminal stdout)]
Exit code of a pipeline. $? is the last stage’s exit
code. $pipestatus (1-based array) holds every stage’s exit
code. setopt PIPE_FAIL rewrites $? so silent failures
stop hiding.
$ false | true; echo "$?" # 0 — last stage masked the failure
$ false | true; echo "$pipestatus" # 1 0
$ setopt pipe_fail
$ false | true; echo "$?" # 1
The subshell trap that goes away. In bash, each stage of a pipeline runs in its own subshell, so loop variables vanish. In zsh, the last stage runs in the current shell, so this works out of the box:
$ count=0
$ printf 'a\nb\nc\n' | while read -r _; do
$ (( count++ ))
$ done
$ echo "$count" # 3
The earlier stages still run in subshells, so mutate state only on the right end of the pipe.
Merge stderr into the pipeline. |& is zsh / bash
shorthand for 2>&1 |.
$ build.sh |& tee build.log # stdout + stderr captured
Process Substitution#
Process substitution turns a command’s output (or input) into a
path. Zsh ships two flavors. <(cmd) and >(cmd) are
the same as bash, backed by /dev/fd/N pipes. =(cmd) is
zsh-only and backs the substitution with a real temporary
file, useful for tools that demand seekable input.
Form |
Effect |
|---|---|
|
|
|
|
|
|
flowchart LR
subgraph SUB["<(cmd)"]
direction TB
C[cmd] --> FIFO["/dev/fd/N"]
end
FIFO --> CALLER[caller reads it as a file]
Compare two pipelines without temp files.
$ diff <(sort a.txt) <(sort b.txt)
Feed a tool that wants a seekable file. =( ) cures the
“this binary mmap’s its input” class of bug.
$ less =(curl -fsSL https://example.com/big.json | jq .)
Tee to multiple consumers. tee >(...) is the standard
fan-out form (works in bash too); zsh’s MULTIOS gets you most of
the way without it.
$ build_output |& tee >(grep -E 'ERROR|WARN' > issues.log) \
$ >(gzip > full.log.gz) \
$ > /dev/null
Named Pipes (FIFOs)#
mkfifo creates an on-disk named pipe. Reads block until a
writer arrives and vice versa. The reach when two independent
processes (not one pipeline) need to talk.
$ mkfifo /tmp/work.fifo
$ ( while read -r job; do # consumer
$ print -- "processing $job"
$ done < /tmp/work.fifo ) &
$ for j in alpha bravo charlie; do
$ print -- "$j" > /tmp/work.fifo # producer
$ done
$ rm /tmp/work.fifo
Backgrounding#
Trailing & runs a command in the background; the shell
prints [job-id] pid and returns to the prompt. jobs
lists work; wait blocks until the named job (or all) finish;
$! is the PID of the most recent background job.
$ long-task &
$ jobs
$ wait $! # wait for the last one
$ wait # wait for every job
Fan out work across a list and wait for the whole batch.
$ pids=()
$ for h in web01 db01 cache01; do
$ ssh "$h" 'uptime' &
$ pids+=($!)
$ done
$ for p in $pids; do wait "$p"; done
Zsh’s coproc keyword opens a bidirectional pipe to a
background command, exposing it through >&p (write) and
<&p (read).
$ coproc bc -l
$ print -p "scale=4; 22/7"
$ read -p result
$ print -- "$result" # 3.1428
Common Tasks#
Tee output to a file while keeping it on the terminal.
$ cmd | tee out.log
Write the same output to two files at once (MULTIOS).
$ cmd > a.log > b.log
Tee both streams to a file.
$ cmd |& tee out.log
Silence stdout but keep errors.
$ cmd > /dev/null
Silence everything.
$ cmd > /dev/null 2>&1
Send stdout to a file, stderr to a different file.
$ cmd > out.log 2> err.log
Send a script’s whole output to a logfile from line one.
$ exec >script.log 2>&1
$ # everything below this line goes to script.log
Read a file line by line, mutating outer state.
$ count=0
$ while IFS= read -r line; do
$ (( count++ ))
$ done < input.txt
$ echo "$count"
Compare two sorted streams without temp files.
$ diff <(sort a.txt) <(sort b.txt)
Feed a seekable consumer from a pipeline.
$ less =(curl -fsSL https://example.com/big.csv)
Stream both stderr and stdout into ``grep``.
$ make 2>&1 | grep -E 'error|warning'
$ make |& grep -E 'error|warning' # shorthand
Probe a pipeline for which stage failed.
$ setopt pipe_fail
$ cmd1 | cmd2 | cmd3; echo "$pipestatus"
References#
man 1 zsh,man 1 zshmisc(theREDIRECTIONandCOMMAND EXECUTIONsections).man 7 fifo,man 2 pipe(kernel-level documentation).Overview for the language-level reading and writing (
read,print,printf, positional parameters).Patterns for strict-mode and
trapidioms that pair with pipeline error handling.Standard I/O for the platform-level view of standard streams.
Coreutils for
tee,grep,sort,cut,awk.