Why this matters
The single most powerful idea in Unix is this: small programs that each do one thing, connected so that the output of one becomes the input of the next. "Which 10 IP addresses hit our web server most often today?" is not a program anyone wrote; it is five small tools in a row, typed in thirty seconds. The connector is the pipe, |. This lesson explains how it works, because the details (concurrency, buffers, what happens when one side stops) explain how pipelines behave on real data.
The idea
$ ls /usr/bin | wc -l
512
| connects the standard output of ls to the standard input of wc. ls writes names; wc counts lines of whatever arrives on its stdin. Neither program knows the other exists. You could do the same with a temporary file:
$ ls /usr/bin > /tmp/names.txt
$ wc -l < /tmp/names.txt
512
but the pipe needs no file, no cleanup, and it starts producing results immediately.
A pipeline is any number of commands joined by |:
$ sort -nr sizes.txt | head -n 3
15360 video.mp4
4096 database.sqlite
2048 backup.tar
Programs written to read stdin and write stdout, so that they can sit in the middle of a pipeline, are called filters: sort, head, tail, wc, grep, cut, tr, sed, awk, uniq. The next module is all about them.
What happens underneath
A pipe is a small buffer inside the kernel with two ends: a write end and a read end, each a file descriptor. Bytes written into one end come out of the other in the same order. bash sets up ls /usr/bin | wc -l like this:
bash
│ pipe() → kernel creates a buffer; bash gets fds: 3 = read end, 4 = write end
│ fork ─▶ child A: dup2(4, 1) stdout becomes the write end
│ close 3, 4 (tidy up the originals)
│ exec ls
│ fork ─▶ child B: dup2(3, 0) stdin becomes the read end
│ close 3, 4
│ exec wc -l
│ close 3, 4 the parent keeps no ends open
│ wait for both
▼
┌──────┐ fd 1 ┌───────────────┐ fd 0 ┌──────┐
│ ls │ ──────▶ │ kernel buffer │ ──────▶ │ wc │ ──▶ terminal (fd 1)
└──────┘ └───────────────┘ └──────┘
It is the same trick as redirection: rearrange file descriptors between fork and exec. And it is the reason any program can join a pipeline without special support.
Several things follow from this design:
All commands run at the same time. bash starts every process of the pipeline at once. They are concurrent, not sequential. grep handles the first lines while cat is still reading the rest of the file.
The buffer is small, and that is fine. On Linux a pipe holds 64 KiB by default. If the writer is faster than the reader and the buffer fills up, the kernel simply pauses the writer (its write call blocks) until the reader catches up. If the reader is faster and the buffer is empty, the reader waits. This automatic speed matching is called backpressure, and it is why a pipeline uses little memory even on a 50 GB log file.
End of input is "all writers closed". The reader sees end-of-file when the buffer is empty and no process holds the write end any more. That is why bash closes its own copies: if the parent kept the write end open, wc would wait forever.
When the reader quits early, the writer is killed. If the last reader closes the pipe and the writer writes again, the kernel sends the writer the signal SIGPIPE, which terminates it by default. That makes this instant, even though seq would produce a billion lines:
$ seq 1 1000000000 | head -n 3
1
2
3
Only stdout goes into the pipe. Standard error is not connected, so errors still reach the terminal:
$ ls /etc/hostname /nope | wc -l
ls: cannot access '/nope': No such file or directory
1
To pipe both, merge them first: ls /etc/hostname /nope 2>&1 | wc -l (prints 2). bash also accepts |& as shorthand for 2>&1 |.
Every part runs in its own process. This matters later: a variable set inside a pipeline stage vanishes when that stage's process ends. Module 10 shows the classic bug this causes with while read loops.
Programs behave differently in a pipe
Notice that ls prints names in columns on your terminal but one per line into a pipe. Programs can ask the kernel whether their stdout is a terminal (the isatty check), and many adjust: ls switches to one name per line, grep and ls drop their colors, and many programs switch from line buffering to block buffering. You can see it:
$ ls /etc/apt
apt.conf.d auth.conf.d keyrings preferences.d sources.list.d trusted.gpg.d
$ ls /etc/apt | cat
apt.conf.d
auth.conf.d
keyrings
preferences.d
sources.list.d
trusted.gpg.d
This is helpful (one name per line is exactly what wc -l wants), but remember it when output in a pipeline looks different from the screen.
Building a pipeline step by step
Write pipelines incrementally. Run the first command and look at its output, then add one stage at a time. For example, "the three most common shells in /etc/passwd":
$ head -n 3 /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
$ cut -d: -f7 /etc/passwd | head -n 3
/bin/bash
/usr/sbin/nologin
/usr/sbin/nologin
$ cut -d: -f7 /etc/passwd | sort | uniq -c | sort -nr
16 /usr/sbin/nologin
2 /bin/bash
1 /bin/sync
You will learn cut and uniq properly in the next module; the habit to take away now is: look at the data after every stage.
Common mistakes
- Piping into a program that does not read stdin.
ls | rmdoes nothing useful:rmtakes file names as arguments, not on stdin. (Module 14 showsxargs, which converts one into the other.) - Expecting errors in the pipe.
cmd | grep erroronly searches stdout. Usecmd 2>&1 | grep error. sortwithout-nfor numbers. Text order puts100before20and9after15360.- Reading and writing the same file in one pipeline.
sort data.txt | uniq > data.txttruncatesdata.txtimmediately, possibly before sort has read it. Same rule as before: write to a new file. - Useless use of cat.
cat file | grep xworks, butgrep x fileorgrep x < filesaves a process. Harmless, but you will see people point it out.
Exercises
Two quizzes on the mechanism, two tasks in the shell, and one script that acts as a filter: it reads its standard input, which the checker fills with numbers.
Recap
a | bconnects a's stdout to b's stdin through a kernel pipe buffer.- bash creates the pipe, forks one child per command, rewires fds 0 and 1, and execs; all commands run concurrently.
- A full buffer pauses the writer; an empty one pauses the reader; a closed reader kills the writer with SIGPIPE.
- Only stdout goes through the pipe; use
2>&1 |to include stderr. - Build pipelines one stage at a time, looking at the output after each.
Going further: run sleep 30 | sleep 30 & and then ps -o pid,ppid,stat,comm: both sleeps are alive at the same time. Then ls -l /proc/self/fd | cat and look at what fd 1 of ls points to: pipe:[...], with a number identifying the pipe.