I/O and Pipelines#
Three byte streams (stdin, stdout, stderr) are the
universal interface every Unix program agrees on. Bash’s job is to
wire them up. Redirection points one stream at a file or
another file descriptor. Pipes wire one program’s stdout into
the next program’s stdin. Process substitution turns a
command’s output into a path that looks like a file. Get these
three right and most shell scripts stop being copy-paste magic and
start being readable plumbing.
This page covers the wiring layer. For language-level reading and
writing (read, printf, the positional parameters table),
see Overview. For the long-running side of subprocesses
(backgrounding, job control, wait), see Backgrounding
near the bottom.
Streams#
Every process the kernel hands the operator inherits three open file descriptors. Bash assigns them the names below; tools rely on the convention without ever asking.
FD |
Name |
Used for |
|---|---|---|
|
|
Input the program reads |
|
|
Normal output (the result the operator wanted) |
|
|
Diagnostics, warnings, errors (a separate channel so they can be filtered without affecting the result) |
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
pipeline reads it.
$ echo "answer" # to stdout
$ echo "log: starting" >&2 # to stderr, stays out of pipelines
Redirection#
Redirection rewires a file descriptor for one command, before the
command runs. The operator sits between the command and its
arguments. Spaces around the operator are optional; > file and
>file parse the same.
Operator |
Effect |
|---|---|
|
Write stdout to |
|
Append stdout to |
|
Read stdin from |
|
Write stderr to |
|
Append stderr to |
|
Merge stderr into stdout (point fd 2 at the same target as fd 1) |
|
Write stdout to stderr (the operator-voice “to stderr” idiom) |
|
Bash shorthand for |
|
Same, appending |
|
Disconnect stdin (the program reads EOF immediately) |
|
Discard stdout |
|
Discard stderr (swallow errors; use sparingly) |
|
Redirect arbitrary fd |
|
Point fd |
|
Close fd |
|
Here-string: |
|
Heredoc: everything until |
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 the shell 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 # bash shorthand for the first form
Capture vs discard vs split. The standard moves.
$ cmd # stdout to terminal, stderr to terminal
$ cmd > out.log # stdout to file, stderr to terminal
$ cmd 2> err.log # stdout to terminal, stderr to file
$ cmd > out.log 2> err.log # split each to its own file
$ cmd > all.log 2>&1 # merged, both to one file
$ cmd 2>/dev/null # swallow errors
$ cmd >/dev/null 2>&1 # silence everything
``exec`` persists a redirection for the whole shell. Once
exec runs, the redirection applies to every later command in
the same 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 # redirect 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' or <<"EOF") for a literal,
unexpanded body. The standard way to embed config snippets, SQL,
JSON, or scripts that the shell must not interpret.
$ cat <<'EOF' > /etc/cron.d/backup
$ # variables left literal
$ 0 3 * * * root /usr/local/bin/backup --target=$HOME
$ EOF
<<-EOF (note the dash) strips leading tabs from every
line of the body and from the delimiter, so the heredoc can be
indented to match the surrounding code. Spaces are not stripped;
the indentation must be tabs.
$ if true; then
$ cat <<-EOF
$ all tabs in front of these lines
$ are stripped before bash hands the
$ heredoc to cat
$ EOF
$ fi
Here-string (<<<) is the one-line cousin; the operator
hands the command a single string as stdin.
$ 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. Bash sets up the kernel pipes, forks one
process per stage, and waits for all of them. The pipeline’s exit
code defaults to the last stage; set -o pipefail makes it
the first 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. By default, $? is the exit code
of the last stage. ${PIPESTATUS[@]} holds every stage’s
exit code. set -o pipefail rewrites $? to the rightmost
non-zero stage so silent failures stop hiding.
$ false | true; echo "$?" # 0 — last stage masked the failure
$ false | true; echo "${PIPESTATUS[@]}" # 1 0
$ set -o pipefail
$ false | true; echo "$?" # 1 — pipefail picks up the failure
Merge stderr into the pipeline. |& is bash shorthand for
2>&1 |; the next stage sees both streams.
$ build.sh |& tee build.log # stdout + stderr captured
The subshell trap. Each stage of a pipeline runs in its own
subshell, so variables a while or read loop sets on the
right of a pipe vanish when the pipe ends. See the While section
of Overview for the standard two fixes (process
substitution or shopt -s lastpipe).
$ count=0
$ printf 'a\nb\nc\n' | while read -r _; do
$ count=$((count + 1))
$ done
$ echo "$count" # prints 0
Process Substitution#
Process substitution turns a command’s output (or input) into a
path that looks like a file. The shell creates a named-pipe
endpoint at /dev/fd/N and substitutes that path on the command
line. The standard reach for commands that demand file
arguments rather than reading stdin.
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)
Tee to multiple consumers. tee >(...) is the standard
fan-out form.
$ build_output |& tee >(grep -E 'ERROR|WARN' > issues.log) \
$ >(gzip > full.log.gz) \
$ > /dev/null
Feed a loop without losing variables to a subshell.
$ count=0
$ while read -r _; do count=$((count + 1)); done < <(printf 'a\nb\nc\n')
$ echo "$count" # 3
Named Pipes (FIFOs)#
mkfifo creates an on-disk named pipe. Reads block until a
writer arrives and vice versa. The standard reach when two
independent processes (not a single pipeline) need to talk and
the operator wants the kernel to handle buffering and
synchronisation.
$ mkfifo /tmp/work.fifo
$ ( while read -r job; do # consumer
$ echo "processing $job"
$ done < /tmp/work.fifo ) &
$ for j in alpha bravo charlie; do
$ echo "$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 immediately. jobs
lists background work; wait blocks until the named job (or all
of them) finishes; $! is the PID of the most recent
backgrounded command.
$ 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
Common Tasks#
Tee output to a file while keeping it on the terminal.
$ cmd | tee out.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 without spawning a subshell.
$ while IFS= read -r line; do
$ echo "$line"
$ done < input.txt
Compare two sorted streams without temp files.
$ diff <(sort a.txt) <(sort b.txt)
Stream both stderr and stdout into ``grep``.
$ make 2>&1 | grep -E 'error|warning'
$ make |& grep -E 'error|warning' # bash shorthand
Probe a pipeline for which stage failed.
$ set -o pipefail
$ cmd1 | cmd2 | cmd3; echo "${PIPESTATUS[@]}"
References#
man 1 bash(theREDIRECTIONandPIPELINESsections cover the operator surface in full).man 7 fifo,man 2 pipe(kernel-level documentation of pipes and named pipes).Overview for the language-level reading and writing (
read,printf,IFS, the 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 the utilities that live on the ends of most pipelines (
tee,grep,sort,cut,awk).