Skip to main content

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:
1

Basic Command Execution

Parse and execute simple commands
2

Built-in Commands

cd, exit, pwd, history, etc.
3

I/O Redirection

>, >>, < operators
4

Pipes

Command pipelines with |
5

Job Control

Background processes, Ctrl+C handling

Phase 1: Basic Shell Loop


Phase 2: Command Parsing


Phase 3: Built-in Commands


Phase 4: External Command Execution

The fork() + exec() pattern is one of the most fundamental concepts in Unix systems programming. Here is what happens when you type ls -la:
  1. The shell calls fork(), which creates an exact copy of the shell process (same code, same memory, same file descriptors).
  2. In the child process, the shell calls execvp("ls", ["ls", "-la", NULL]), which replaces the child’s entire memory image with the ls program.
  3. The parent shell calls waitpid() to block until the child exits.
  4. When ls finishes, the shell prints the next prompt.
This two-step dance (fork then exec) seems wasteful — why copy everything just to throw it away? The answer is that fork gives the child a chance to set up its environment (redirect I/O, close file descriptors, change directories) before exec replaces the program. This separation of concerns is why Unix I/O redirection and piping are so elegant.

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 write cat 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?.txt

Environment Variables

Support $VAR expansion and export

Command Substitution

Implement $(command) or backtick substitution

Tab Completion

Add readline-style tab completion

Scripting

Execute shell scripts with conditionals and loops

Job Control

Full jobs, fg, bg implementation

Next Up

Build a Database

Build a key-value store with persistence