Project: Build a Unix Shell
Put your C skills to the test by building a functional Unix shell. This project covers process management, signal handling, pipes, and I/O redirection. A shell is fundamentally a loop that does three things: read a command, parse it, and execute it. But the “execute” step involves some of the most important system calls in Unix:fork() to create a new process, exec() to replace that process with a different program, wait() to synchronize with child processes, pipe() to connect processes together, and dup2() to redirect I/O. These are the same primitives that bash, zsh, and fish use under the hood. Understanding them gives you deep insight into how Unix processes work — knowledge that pays off every time you debug process-related issues in production.
Project Overview
We’ll build a shell with these features:Phase 1: Basic Shell Loop
Phase 2: Command Parsing
Phase 3: Built-in Commands
Phase 4: External Command Execution
Thefork() + exec() pattern is one of the most fundamental concepts in Unix systems programming. Here is what happens when you type ls -la:
- The shell calls
fork(), which creates an exact copy of the shell process (same code, same memory, same file descriptors). - In the child process, the shell calls
execvp("ls", ["ls", "-la", NULL]), which replaces the child’s entire memory image with thelsprogram. - The parent shell calls
waitpid()to block until the child exits. - When
lsfinishes, the shell prints the next prompt.
Phase 5: Pipes
Pipes are Unix’s composition mechanism: small programs that each do one thing well, connected together to solve complex problems. When you writecat file.txt | grep "error" | wc -l, three processes run simultaneously, connected by kernel-managed buffers. Each | creates a pipe: a pair of file descriptors where one end writes and the other reads. The kernel handles the synchronization — if the reader is slow, the writer blocks automatically.
The tricky part of implementing pipes in a shell is getting the plumbing right: each process needs the correct file descriptors connected and all the others closed. A common bug is forgetting to close pipe ends in the parent process, which causes the reader to hang forever (it keeps waiting for EOF, which only happens when ALL write ends are closed).
Phase 6: Signal Handling and Job Control
Makefile
Testing Your Shell
Extensions to Implement
Globbing
Expand wildcards like
*.c and file?.txtEnvironment Variables
Support
$VAR expansion and exportCommand Substitution
Implement
$(command) or backtick substitutionTab Completion
Add readline-style tab completion
Scripting
Execute shell scripts with conditionals and loops
Job Control
Full
jobs, fg, bg implementationNext Up
Build a Database
Build a key-value store with persistence