The Pipe Manifold
A pipeline is not three steps in order. It is three processes running at once.
What you're wiring up
A pipe is a kernel-managed byte buffer with a write end and a read end, handed to you as two file descriptors. When you run cat log | grep err | wc -l, the shell is not running three commands in sequence and passing text along. It creates the pipes, forks all three children, cross-plugs their fd slots, and then gets out of the way while all three run simultaneously.
That concurrency is the point. It is why you can pipe a gigabyte through without a gigabyte of memory, and why an infinite producer like yes terminates the moment head has seen enough.
There is one bug on this stage that every person writing a shell hits, and it is worth hitting. End of file on a pipe does not mean the writer finished. It means every copy of the write end is closed. Fork duplicates descriptors, so one forgotten copy anywhere, including in the parent, and the reader waits forever.
# cat f | tr a-z A-Z | wc -l two pipes, three children pipe A: child1 stdout → child2 stdin pipe B: child2 stdout → child3 stdin # every copy of a write end must be closed parent closes both ends, each child closes what it did not dup2
Assembly steps
hint
Reuse the Stage 3 parser per segment. Precedence is simple: redirections bind inside a segment.
hint
After the dup2s, close both pipe fds in both children and in the parent. Count the copies; any surviving write end is a hang.
hint
A loop carrying prev_read_fd between iterations is cleaner than an array of every pipe.
hint
waitpid each pid you collected; store only the final one's status.
hint
Simplest correct answer: run a builtin inside a pipeline in a forked child like anything else. It cannot mutate shell state there, and that matches real shells.
Go deeper (after it passes)
The Unix pipes philosophy section of DDIA Chapter 10 is literally this stage: a shell pipeline is single-machine MapReduce. Follow it into DDIA M10 — Batch: Your Own MapReduce.