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:

  1. REPL skeleton. Prompt, readline (or a minimal hand-rolled line editor), history file at ~/.nsh_history, exit and cd as builtins.

  2. Simple external commands. fork + execvp + waitpid. Handle execvp failure with a helpful error.

  3. Pipes. ls | grep foo | wc -l — arbitrary-length pipe chains. This is where 60% of people give up; the fd bookkeeping is fiddly.

  4. Redirects. < in.txt, > out.txt, >> append.txt, 2> err.txt, 2>&1. Use dup2.

  5. Background jobs. sleep 10 & returns the prompt immediately, prints [1] <pid>. jobs builtin lists them.

  6. Signal handling.

    • Ctrl-C sends SIGINT to the foreground process group only, never kills the shell itself.

    • Ctrl-Z suspends the foreground job; fg and bg builtins resume it.

    • SIGCHLD handler reaps zombies asynchronously.

  7. Tab completion (basic). Complete command names from $PATH and file names in the current directory. No fancy fuzzy matching — prefix match is enough.

  8. Tests. A scripted test suite of ≥ 30 scenarios: each is a stdin stream fed to nsh with an expected stdout and exit code.

Repository infrastructure:

  • README.md with a demo GIF (use asciinema + agg to 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+ .sh files, each a scenario, run by make 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:

  1. Process groups and terminal foreground groups (tcsetpgrp).

  2. Signal masking and reentrancy (what you may and may not do in a signal handler).

  3. File descriptor ownership across fork (who closes what).

  4. Blocking vs non-blocking waitpid (WNOHANG in your SIGCHLD handler).

  5. The difference between a builtin and an external (why cd cannot 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 green

  • No zombie processes remain after ANY test run (verified by ps in 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 lsof before/after)

  • Background jobs listed by jobs, resumable by fg/bg

  • ASan 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_Programming for 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

  1. Zombies you don’t see. Your test suite runs 30 scenarios, but you never check ps afterward. There are 12 zombie shells lurking. Detection: teardown step in make test grep’s ps -o stat output for Z and fails if any found.

  2. The SIGCHLD race. You reap in the main loop instead of the handler, and slow-writing children get zombied because your waitpid runs before they finish. Detection: a test that spawns 10 fast background jobs and checks jobs output is empty within 200ms.

  3. Pipe fd leak. You forget to close the write end of a pipe in the parent, and downstream read never sees EOF. Test hangs. Detection: every test scenario has a timeout 5 wrapper; a hang is a fail.

  4. 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 10 in 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 — read jobs.c for a production signal-handling reference. It is small and readable by shell-source standards.

  • fish-shell src/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