An educational Unix-like shell written in Python, featuring a custom tokenizer, built-in commands, pipelines, redirection, background jobs, history, variables, and tab completion.
I built this project while working through the CodeCrafters “Build Your Own Shell” challenge. The goal was to look beneath the familiar command-line interface and learn how a shell parses input, locates executables, starts processes, connects pipes, and maintains interactive state.
Note
This is a learning project rather than a POSIX-complete or production-ready shell.
- Interactive read-evaluate-execute loop with a
$prompt - External executable discovery through
PATH - Process creation using Python's
subprocessmodule - Hand-written state-machine tokenizer supporting:
- single and double quotes
- backslash escaping
- malformed quote detection
- Multi-command pipelines using OS pipes
- Output redirection with
>,1>, and2> - Append redirection with
>>,1>>, and2>> - Background command execution and job tracking
- In-memory command history with optional
HISTFILEpersistence - Shell-local variable declaration and expansion
- Readline/libedit tab completion for commands, files, and directories
- Programmable, command-specific completion through external scripts
uv can download the pinned Python version automatically if it is not already installed.
git clone https://github.com/vaibhav135/shell-from-scratch.git
cd shell-from-scratch
./your_program.shThe launcher uses uv to run the app.main module with the repository configured on PYTHONPATH.
You can leave the shell by entering exit or pressing Ctrl+C.
$ ./your_program.sh
$ echo "hello from my shell"
hello from my shell
$ type echo
echo is a shell builtin
$ type python3
python3 is /usr/bin/python3
$ pwd
/path/to/shell-from-scratch
$ echo hello | cat
hello
$ sleep 5 &
[1] 12345
$ jobs
[1]+ Running sleep 5 &
$ exitExecutable paths, process IDs, spacing, and working directories will vary by system.
| Command | Description |
|---|---|
echo [arguments] |
Print tokenized arguments and expand declared variables |
type NAME |
Report whether a command is built in or resolve it through PATH |
pwd |
Print the current working directory |
cd PATH |
Change the shell's working directory; cd ~ uses HOME |
exit |
Persist new history when configured, then exit the shell |
jobs |
Display tracked background jobs and their current state |
history [N] |
Show all history entries or the most recent N entries |
history -r FILE |
Read history entries from a file |
history -w FILE |
Save the current history by appending it to a file |
history -a FILE |
Append new history entries to a file |
declare NAME=VALUE |
Create a shell-local variable |
declare -p NAME |
Print a declared variable |
complete -C FILE COMMAND |
Register an external completion program for a command |
complete -p COMMAND |
Print a command's completion specification |
complete -r COMMAND |
Remove a command's completion specification |
Variables created with declare are local to this shell implementation and can be expanded with $NAME or ${NAME}:
$ declare PROJECT=shell-from-scratch
$ echo $PROJECT
shell-from-scratch
$ declare -p PROJECT
declare -- PROJECT="shell-from-scratch"Set HISTFILE before launching the program to load history at startup and append new entries when exiting normally. The history file must already exist:
export HISTFILE="$HOME/.shell_from_scratch_history"
touch "$HISTFILE"
./your_program.shThe shell is organized into focused modules instead of placing the entire implementation in one script:
- REPL and dispatch —
app/main.pyreads input, records history, classifies commands, and coordinates execution. - Tokenization —
app/tokenizer.pyuses explicit states to process unquoted text, quoted text, and escaped characters. - Built-ins —
app/commands.pyimplements commands that need access to shell-owned state, such ascd,history, anddeclare. - External processes — executable lookup and
subprocesscalls run programs found throughPATH. - Pipelines —
app/pipe.pyconnects process output and input withsubprocess.PIPE. - Jobs and state — dedicated modules track background processes, history, and shell-local declarations.
- Completion — the
app/completer/package integrates with readline/libedit and supports built-in, executable, path, and programmable completion.
.
├── app/
│ ├── completer/ # Readline and programmable completion
│ ├── main.py # Interactive loop and command dispatch
│ ├── commands.py # Built-in command handlers
│ ├── tokenizer.py # State-machine tokenizer
│ ├── pipe.py # Pipeline construction
│ ├── jobs.py # Background job registry
│ ├── history.py # In-memory and file-backed history
│ ├── declare.py # Shell-local variables
│ ├── redirection.py # Overwrite redirection helpers
│ └── append.py # Append redirection helpers
├── codecrafters.yml # CodeCrafters project configuration
├── pyproject.toml # Python project metadata
├── uv.lock # Reproducible uv environment
└── your_program.sh # Local launcher
The project implements a deliberately constrained subset of shell behavior. It currently does not support:
- POSIX-complete parsing or scripting
- command operators such as
&&,||, and; - command substitution, arithmetic expansion, or filename globbing
- input redirection, here-documents, or file descriptor duplication
- exported shell variables or general environment-variable expansion
- process groups and full terminal job control such as
fgandbg - aliases, functions, startup files, or a configurable prompt
Some quoting, expansion, redirection, and pipeline combinations also remain intentionally narrower than their Bash or Zsh equivalents.
Building a shell turned everyday terminal behavior into concrete systems concepts. The project gave me hands-on experience with:
- designing a tokenizer as a finite-state machine
- distinguishing shell built-ins from external executables
- managing child processes and connecting their standard streams
- preserving state across commands in a long-running REPL
- implementing history, completion, variables, and background job bookkeeping
- handling the behavioral edge cases hidden behind a small command prompt
CodeCrafters provided the staged challenge and remote tests; the implementation and its organization evolved as I worked through those requirements and explored additional shell behavior.
This project is available under the MIT License.