rgoussu@goussu: ~/library/system-administration/exercises
~/library/system-administration/exercises cat build-your-own-shell-subject.md

Build your own shell — subject

# The feature spec for an interactive Unix shell — REPL, exec with PATH search, builtins, quoting, N-stage pipelines, redirections, background jobs, and signal handling — precise enough to implement and test.

Subjectsaved 2026-08-08source #linux#shell#processes#systems-programming#exercise#cli#subject

Brief

You use a shell every day and treat it as magic. Strip the magic: write your own. It prints a prompt, reads a line, parses it, and runs it — external programs via fork/exec, builtins in-process, pipelines wired with real pipes, I/O redirected with dup2, and signals routed so Ctrl-C kills the command and not your shell. Build it in any systems-capable language (C, Rust, Go, Zig…) with direct access to the POSIX process syscalls.

Instructions

Grow the shell in the milestone order below; each stage is independently runnable and testable.

1. REPL + external commands

  • Loop: print a prompt, read a line, tokenize on whitespace, and if the first token names a program, fork and execvp it in the child, waitpid in the parent, then loop.
  • Resolve bare command names via PATH search (this is what execvp does — or implement the search yourself). Handle an empty line (re-prompt, no fork) and a not-found command (print a clean error, do not crash). EOF (Ctrl-D) on an empty line exits.

2. Builtins

  • Implement the commands that must run in the shell process because a forked child could not achieve them: cd (change the shell's own working directory — a forked cd changes nothing), exit (with optional status), pwd, and export/unset (mutate the shell's own environment, inherited by future children). Dispatch builtins before the fork path.

3. Quoting

  • Implement a real tokenizer, not split(" "): single quotes (literal, no interpretation), double quotes (group with spaces, allow variable expansion inside), and backslash escaping. echo "a b" is one argument a b; echo 'a b' likewise; unquoted collapses whitespace.

4. Redirections

  • Parse and apply < (stdin from file), > (stdout to file, truncate), >> (stdout append), and 2> (stderr to file). In the child, open the target and dup2 it onto fd 0/1/2 before exec; confirm the child inherits the redirected descriptors. Redirection operators are not arguments to the command.

5. Pipelines

  • Support a | b | c for arbitrary N stages: create N-1 pipes, fork each stage, dup2 the correct pipe ends onto stdin/stdout, and close every unused descriptor in both parent and children (leaked fds hang pipelines — the classic bug). Wait on all stages; the pipeline's status is the last stage's. Redirections combine with pipes at the ends.

6. Signals and job control

  • Put the foreground command in its own process group and give it the terminal so SIGINT (Ctrl-C) and SIGTSTP (Ctrl-Z) hit the child, not the shell. The shell itself ignores/handles those signals and reclaims the terminal afterward.
  • Support & to run a command in the background (do not wait synchronously); implement a jobs listing and fg to bring a background/stopped job to the foreground. Reap finished background children (handle SIGCHLD) so they do not linger as zombies.

Examples

Behaviors the shell must exhibit:

$ echo "hello   world"          # one arg, spaces preserved
hello   world
$ cd /tmp && pwd                 # builtin cd changes the shell's cwd
/tmp
$ cat < in.txt | sort | uniq -c > out.txt   # redirection + 3-stage pipe
$ sleep 30 &                     # background; prompt returns immediately
[1] 4123
$ jobs
[1]  Running   sleep 30 &
$ sleep 30                       # Ctrl-C here kills sleep, shell survives
^C
$

Constraints

  • Use the OS process primitives directly (fork/exec/wait/pipe/dup2/open/ setpgid/tcsetpgrp/signal handling) — not a language's system() or high-level "run a shell command" helper, which would skip the whole lesson.
  • Builtins that alter shell state (cd, export, exit) must run in the shell process, never a child.
  • Close all unused pipe descriptors in every process.

Acceptance

  • Milestone 1 (REPL + exec): runs external commands found on PATH and returns to the prompt; empty line re-prompts, unknown command errors cleanly, Ctrl-D exits.
  • Milestone 2 (builtins): cd changes the shell's cwd as seen by a subsequent pwd; exit leaves with the given status; export sets a variable visible to a later child.
  • Milestone 3 (quoting): single/double quotes and backslash produce the correct argument vector, including spaces preserved inside quotes and expansion inside double quotes only.
  • Milestone 4 (redirection): >, >>, <, and 2> create/append/read the right files and the child sees the redirected descriptors; operators are not passed as args.
  • Milestone 5 (pipelines): an N-stage pipeline produces correct output, terminates cleanly with no hung/leaked fds, and reports the last stage's status; pipes compose with end redirections.
  • Milestone 6 (signals + jobs): Ctrl-C interrupts the foreground child while the shell survives; & backgrounds a job, jobs lists it, fg resumes it, and finished background jobs are reaped (no zombies).

Related