This document is a step-by-step guide that mirrors the incremental feature stages of building a small shell.
🙏 Acknowledgement: This guide is inspired by the CodeCrafters "Build your own Shell" roadmap — many thanks to the CodeCrafters team for the exercises and guidance.
The goal is to explain:
- the theory behind each feature
- the minimal implementation approach
- where the final code lives in this repo
Tip: Keep each stage small. Prefer correctness and clear interfaces over feature creep.
Theory: A shell is a REPL; the prompt is the UI boundary.
Code pointers:
- Prompt printing:
src/repl/repl.c
Theory: If a command isn’t found on $PATH, print a friendly error.
Code pointers:
- PATH resolution:
src/path/path.c - Execution:
src/exec/exec.c
Theory: Loop: prompt → read → parse → execute → repeat.
Code pointers:
- REPL orchestration:
src/repl/repl.c
Theory: Some commands must modify the shell itself, not a child process.
Code pointers:
- Builtin dispatch:
src/builtins/builtins.c - REPL exit handling:
src/repl/repl.c
Theory: Most builtins are pure functions: argv → stdout.
Code pointers:
echobuiltin:src/builtins/builtins.c
Theory: type tells whether something is a builtin or a path-resolved executable.
Code pointers:
typebuiltin:src/builtins/builtins.c- PATH resolver:
src/path/path.c
Theory: Search each directory in $PATH for an executable regular file.
Code pointers:
$PATHscanning:src/path/path.c
Theory: Use fork() + execv(); parent waitpid().
Code pointers:
- Execution:
src/exec/exec.c
Theory: pwd is just getcwd().
Code pointers:
pwdbuiltin:src/builtins/builtins.c
Theory: cd must run in the parent process or it won’t affect the shell.
Code pointers:
cdbuiltin:src/builtins/builtins.c
Theory: Tokenization rules decide what becomes argv and how strings are joined.
Code pointers:
- Tokenizer:
src/tokenizer/tokenizer.c
Theory: Redirection is per-command file descriptor plumbing. Implement by:
- parsing redirection operators and removing them from argv
- using
open()+dup2()before executing the command
Code pointers:
- Redirection extraction/application:
src/redir/redir.c - Parser integration:
src/parser/parser.c - Execution application:
src/exec/exec.c
Theory: Completion is a UI feature that queries:
- known builtins
- executables available on
$PATH
Code pointers:
- Completion engine:
src/completion/completion.c - Raw-mode input integration:
src/line_edit/line_edit.c
Theory: Pipelines connect stdout of stage
Implementation sketch:
- create
$N-1$ pipes - fork
$N$ children - in child
$i$ :-
dup2()stdin/stdout appropriately - close all pipe fds
- run builtin or exec
-
Code pointers:
- Pipeline execution:
src/exec/exec.c - Pipeline parsing:
src/parser/parser.c
Theory: History is two parts:
- storage (a list of strings)
- UI (navigation while editing the line)
Code pointers:
- History storage/printing:
src/history/history.c - Arrow-key navigation:
src/line_edit/line_edit.c
Theory: Persistence maps to:
- read on startup
- write/append on demand
- write on exit
Code pointers:
- History load/save/append:
src/history/history.c - Startup/exit integration:
src/main.c,src/repl/repl.c
- Add unit tests for redirection parsing (see
src/redir/redir.cfor parsing/apply logic:src/redir/redir.c). - Extend completion to complete beyond the first token.
- Make
exit 0exit as well (if you want bash-like behavior).