work / systems / custom-shell
Custom UNIX Shell
A UNIX shell in C with pipes, redirection, chaining, history, and signal handling, built from fork(), execvp(), and careful file-descriptor bookkeeping.
- operators
- | > >> < ; &&
- full parsing + execution
- builtins
- cd, history
- plus exit
- signals
- SIGINT
- ctrl+c handled
- language
- C
- no libraries, raw POSIX
system architecture / interactive
Why build a shell
A shell is the best possible tour of UNIX process machinery: to make cat file | grep x | wc -l work you have to genuinely understand fork(), execvp(), pipe(), dup2(), file
descriptors, and process waiting. This project implements a working interactive shell in C
with no parsing or readline libraries, just POSIX.
What it supports
- Command execution of anything on
PATH(ls -l,pwd, ...) - Piping with
|, including multi-stage pipelines likecat test.txt | grep Hello | wc -l - Redirection: output
>, append>>, and input< - Chaining: sequential
;and conditional&&(mkdir temp && cd temp && touch hello.txt) - Builtins:
cdandhistory(which must run in the parent process, since a child changing its own working directory or reading its own history helps nobody) - Signal handling: Ctrl+C interrupts the foreground child without killing the shell itself, verified with a dedicated sleep-based tester program
The parts that taught the most
Pipelines are where the file-descriptor bookkeeping gets honest. Each stage needs its stdout wired to the write end of one pipe and its stdin to the read end of the previous one, and every unused descriptor must be closed in both parent and children, because a single leaked write-end keeps the reader blocked forever waiting for EOF.
Signal handling has a subtle ownership question: SIGINT should kill the running child,
not the shell. The shell installs its own handler that re-prompts, while children restore
default behavior, which is exactly how real shells keep Ctrl+C scoped.
Conditional chaining (&&) requires reading child exit codes via waitpid status
macros and short-circuiting the rest of the line on failure, a small interpreter decision
that makes the shell feel real.
The code is organized as shell.c (main loop, signals, parsing), command.c (tokenization,
builtins, dispatch), and execution.c (redirection and pipe plumbing).
stack