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,
forkandexecvpit in the child,waitpidin the parent, then loop. - Resolve bare command names via
PATHsearch (this is whatexecvpdoes — 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 forkedcdchanges nothing),exit(with optional status),pwd, andexport/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 argumenta b;echo 'a b'likewise; unquoted collapses whitespace.
4. Redirections
- Parse and apply
<(stdin from file),>(stdout to file, truncate),>>(stdout append), and2>(stderr to file). In the child,openthe target anddup2it onto fd 0/1/2 beforeexec; confirm the child inherits the redirected descriptors. Redirection operators are not arguments to the command.
5. Pipelines
- Support
a | b | cfor arbitrary N stages: create N-1pipes, fork each stage,dup2the 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) andSIGTSTP(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 ajobslisting andfgto bring a background/stopped job to the foreground. Reap finished background children (handleSIGCHLD) 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'ssystem()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
PATHand returns to the prompt; empty line re-prompts, unknown command errors cleanly, Ctrl-D exits. - Milestone 2 (builtins):
cdchanges the shell's cwd as seen by a subsequentpwd;exitleaves with the given status;exportsets 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):
>,>>,<, and2>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,jobslists it,fgresumes it, and finished background jobs are reaped (no zombies).
Related
- Build your own shell — the exercise note this is the subject of.
- Source: John Crickett's Build Your Own Shell
challenge, plus the POSIX
fork/execvp/dup2/setpgidmanual pages, adapted here.