Rung 4 — Mini-Shell (nsh)¶
Headline. A small POSIX shell written in C that fork/execs, pipes multi-stage commands, redirects file descriptors, manages background jobs, and reaps its children without leaving zombies — proof that you understand the Unix process model, not just Unix commands.
Month target. M7 (January 2027). Publish by the last day of M7.
What You Build¶
Repo name: nsh (short, greppable, memorable). This is the first rung where a demo GIF in the README is mandatory — shells are inherently visual.
Features, in the order you should implement them:
REPL skeleton. Prompt,
readline(or a minimal hand-rolled line editor), history file at~/.nsh_history,exitandcdas builtins.Simple external commands.
fork+execvp+waitpid. Handleexecvpfailure with a helpful error.Pipes.
ls | grep foo | wc -l— arbitrary-length pipe chains. This is where 60% of people give up; the fd bookkeeping is fiddly.Redirects.
< in.txt,> out.txt,>> append.txt,2> err.txt,2>&1. Usedup2.Background jobs.
sleep 10 &returns the prompt immediately, prints[1] <pid>.jobsbuiltin lists them.Signal handling.
Ctrl-C sends SIGINT to the foreground process group only, never kills the shell itself.
Ctrl-Z suspends the foreground job;
fgandbgbuiltins resume it.SIGCHLDhandler reaps zombies asynchronously.
Tab completion (basic). Complete command names from
$PATHand file names in the current directory. No fancy fuzzy matching — prefix match is enough.Tests. A scripted test suite of ≥ 30 scenarios: each is a stdin stream fed to
nshwith an expected stdout and exit code.
Repository infrastructure:
README.mdwith a demo GIF (useasciinema+aggto generate).docs/PROCESS_MODEL.md— a diagram (ASCII is fine) showing what fork/exec/dup2 do in the pipe case. This is the pedagogical artifact that makes the repo interesting to read.tests/— 30+.shfiles, each a scenario, run bymake test.CI on Ubuntu-latest. macOS optional — signal semantics differ, be honest in the README about which platforms you tested.
Target size: ~5000-8000-4000 LOC of C, plus tests and docs.
Why This Rung, Why Now¶
Every C programmer eventually writes a shell. Most write it badly the first time and never revisit it — zombies pile up, Ctrl-C kills the shell, pipes leak fds. Rung 4 exists because writing a shell that is actually clean forces you to internalize five concepts that show up in every subsequent systems-programming job:
Process groups and terminal foreground groups (
tcsetpgrp).Signal masking and reentrancy (what you may and may not do in a signal handler).
File descriptor ownership across
fork(who closes what).Blocking vs non-blocking
waitpid(WNOHANG in your SIGCHLD handler).The difference between a builtin and an external (why
cdcannot be external).
You ship in M7 because M6 was spent in 05_systems_programming/ learning exactly these concepts. Rung 4 is where that knowledge stops being trivia and becomes muscle memory.
Acceptance Criteria¶
Passes 30+ scripted test scenarios via
make test; CI greenNo zombie processes remain after ANY test run (verified by
psin a teardown check)Ctrl-C in the shell kills the foreground command’s process group and returns to prompt — never kills the shell
Multi-stage pipe (≥ 3 stages) works and closes all intermediate fds (verified by
lsofbefore/after)Background jobs listed by
jobs, resumable byfg/bgASan and UBSan clean under the test suite
Demo GIF in README shows: multi-stage pipe, background job, Ctrl-C recovery, tab completion
Blog post published (personal blog + dev.to) explaining the pipe fd bookkeeping with a diagram
Posted to
r/C_Programmingfor review with a specific ask (“is my SIGCHLD handler race-free?”)
Where to Publish¶
GitHub: pinned on profile. Topics:
c,shell,unix,posix,systems-programming.Personal blog + dev.to: long-form walkthrough of the pipe implementation, ~1500 words with the fd-flow diagram. This is your first serious blog post; polish it.
Reddit —
r/C_Programming: post the blog link with a title like “Wrote a mini-shell in C — walking through the pipe fd bookkeeping”.Reddit —
r/unix: a different post, a different angle: “Writing nsh taught me tcsetpgrp — here’s what I got wrong 3 times.”Hacker News: hold off. Save HN for Rung 5.
Signal to Recruiter / Employer¶
“This candidate understands the Unix process model — not the surface syntax of shell commands, but the semantics of fork, exec, dup2, signals, and process groups. They could work on a container runtime, a build tool, an init system, or a job scheduler and not need three months of ramp-up.”
Rung 4 is the first rung that opens systems-programming study loops. Companies that build init systems, container runtimes (containerd, runc), or process supervisors (systemd-adjacent, s6, runit) will engage with this artifact.
Common Failure Modes¶
Zombies you don’t see. Your test suite runs 30 scenarios, but you never check
psafterward. There are 12 zombie shells lurking. Detection: teardown step inmake testgrep’sps -o statoutput forZand fails if any found.The SIGCHLD race. You reap in the main loop instead of the handler, and slow-writing children get zombied because your
waitpidruns before they finish. Detection: a test that spawns 10 fast background jobs and checksjobsoutput is empty within 200ms.Pipe fd leak. You forget to close the write end of a pipe in the parent, and downstream
readnever sees EOF. Test hangs. Detection: every test scenario has atimeout 5wrapper; a hang is a fail.Ctrl-C kills the shell. You never set up your own process group, so the terminal sends SIGINT to your whole group. Detection: a test that spawns a
sleep 10in the foreground, sends SIGINT via a helper, and asserts the shell PID is still alive afterward.
Estimated Hours¶
REPL + builtins: 6h
fork/exec/wait: 4h
Pipes: 12h (this is the pain point)
Redirects: 5h
Background jobs + signal handling: 12h (SIGCHLD is where you’ll bleed)
Tab completion: 6h
Test suite (30 scenarios): 10h
Blog post + demo GIF: 8h
Debug + polish: 12h
Total: ~75 hours across M6-M7. ~9-10h/week for 8 weeks. Sustainable but tight; do not add features.
Prior-Art / Inspirations to Study First¶
Stephen Brennan’s “Write a Shell in C” blog post (
brennan.io/2015/01/16/write-a-shell-in-c/) — the canonical starter walkthrough. Read once for structure; do not copy code.dash(Debian Almquist Shell) source — readjobs.cfor a production signal-handling reference. It is small and readable by shell-source standards.fish-shellsrc/parser.cpp— not C, but the pipe/job data structures are worth studying. Bring back only concepts.APUE (Stevens & Rago) chapters 8, 9, 10, 15 — fork, signals, terminal I/O, interprocess communication. This book is the textbook Rung 4 rehearses.
Return to README.md · Previous: 03_rung_3_neetcode75_in_c.md · Next: 05_rung_5_epoll_http_server.md