Master the fundamental concepts of inter-process communication through this focused micro-challenge.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksA shell pipeline wires stdout of command A to stdin of command B via pipe(), fork(), and dup2(). Each child closes unused fd ends so reads block correctly instead of hanging on the wrong descriptor.
Parent for cmd1 | cmd2:
pipe(pfd)dup2(pfd[1], 1), exec cmd1dup2(pfd[0], 0), exec cmd2For example, ls | wc -l connects listing bytes into wc stdin without temporary files.
This is literally how bash, zsh, and dash implement cmd1 | cmd2 | cmd3 internally, using pipe(), fork(), and dup2() in exactly the sequence you'll write here. Forgetting to close the pipe's write end in every process that isn't writing to it is a real, well-documented bug class that causes pipelines to hang forever because the reading command never receives EOF, which is why shell source code is meticulous about closing every unused descriptor after each fork.
Before you call the implementation done, walk failure modes on purpose. Test empty structures, single-element edge cases, maximum concurrency, and errno paths that must not crash the program. OS code usually fails in production when happy-path tests pass but invariants break under contention or memory pressure.
Keep structures small and name fields after kernel counterparts when possible. That lets you read man pages and kernel source side by side while you work. Print observable events during development; remove noisy logs once tests pass reliably.
You will parse two commands and a pipe token, then wire fds before execve. The task asks you to reap both children and propagate exit status of the last command in the pipe.
Implement the plumbing behind a | b | c in a shell. For every stage, fork a child, connect neighbouring stages with pipes, and use dup2 so each child reads its stdin from the previous pipe and writes its stdout into the next. Close every unused pipe end: forget one and the next stage never sees end-of-file and hangs. The parent waits for all children and reports their exit statuses.
Real shells exec external programs. To keep the output identical everywhere, the "programs" here are built-in filters that each child runs on its own stdin/stdout.
The first line is the pipeline, stages separated by |. Everything after it is the data fed to the first stage's stdin.
| Filter | Behaviour (line-oriented) |
|---|---|
cat | copy input to output |
upper | convert letters to upper case |
rev | reverse each line's characters |
sort | sort lines by byte value |
uniq | drop a line equal to the previous one |
grep WORD | keep lines containing WORD |
head N | first N lines |
wc | print LINES WORDS BYTES |
fail N | read and discard all input, exit with status N |
Any other name makes the child print NAME: command not found to stdout and exit 127.
First, the output of the last stage, exactly as the child wrote it. Then, after every child has been reaped, one line per stage in pipeline order:
cLoading…
The pipeline's status is the last stage's exit status, as in a POSIX shell.
Input:
cLoading…
Output:
cLoading…
pipe() per connection and one fork() per stage. Each child dup2s its ends onto 0/1 and closes every pipe descriptor before running its filter. The parent closes all of them after forking.waitpids each child for its status. Careful: after fork, a child's stdin FILE still holds whatever the parent had buffered, so children should read through a fresh fdopen(0, "r") (or raw read).Hidden tests cover a four-stage pipeline using rev, head and wc, and a pipeline with no input whose middle stage fails and whose last command doesn't exist (the pipeline status comes from the last stage).