This repository contains a minimal POSIX-style shell implemented in Rust as part of CodeCrafters' "Build Your Own Shell" challenge. It features a readline-powered REPL, a small set of built-in commands, external command execution, pipelines, output redirection, and comprehensive tab completion.
Challenge details: https://app.codecrafters.io/courses/shell/overview
The shell starts an interactive loop, reads a line, parses it into one or more commands (a pipeline), and then executes either built-ins or external programs. It supports:
- Interactive prompt with history and tab completion
- Built-in commands:
cd,echo,exit,pwd,type,history,jobs - External commands resolved via
PATHor absolute paths - Pipelines (
cmd1 | cmd2 | ...) - Output redirection for stdout, stderr, and both together
- Background execution with
&and job control viajobs - History persistence via
HISTFILE - Tab completion for commands, filenames, and nested paths
src/main.rs- Entry point. Sets up the rustyline editor and config, loads/saves history (
$HISTFILE), and drives the REPL loop. Each iteration reaps finished background jobs, reads a line, adds it to history, and delegates toexecutor::execute_pipeline. Saves history onexitor EOF.
- Entry point. Sets up the rustyline editor and config, loads/saves history (
src/parser.rs- Tokenizer and parser for a single input line. Produces a vector of
ParsedCommandstructs forming a pipeline. Handles quoting rules, backslash escapes inside and outside quotes, pipe splitting, and output redirection targets/flags. - Constants used across the shell (prompt string, command names, environment variable names, file-descriptor tokens like
1,2, and&). - Escape expansion helper used by
echo -e.
- Tokenizer and parser for a single input line. Produces a vector of
src/executor.rs- Pipeline execution engine. Defines
ShellContext(carries the rustyline editor reference and history-append cursor) andexecute_pipeline. - Iterates over pipeline stages, wires
os_pipebetween consecutive stages, resolves each command as a built-in or external process, and applies file redirections for the final stage. - For single commands, spawns the child and either registers it as a background job or waits for it. For multi-stage pipelines, spawns all children then waits for them in order.
- Pipeline execution engine. Defines
src/commands.rs- Implementations of built-in commands and the external command runner.
- Built-ins dispatched via
dispatch_builtin:cd [dir]— changes directory. Defaults to$HOME. Interprets~as home.echo [-e] [args...]— prints arguments; with-eexpands\n,\t,\r,\\,\0,\",\'.exit [code]— terminates the shell with an optional numeric exit code (default 0).pwd— prints the current working directory.type <name>— reports whether<name>is a shell builtin or the full path of an external command.history [N] | -r <file> | -a <file> | -w <file>— prints recent history, reads entries from a file, appends only new entries, or writes the full history respectively.jobs— delegates toJobManager::list_jobsto list all background jobs.
- External command execution via
run_executable: resolves via$PATHor absolute path, supports captured or inherited stdout/stderr. - Output redirection helper
get_redirection: opens files in truncate or append mode.
src/jobs.rs- Background job management. Defines
BackgroundJob(id, pid, command string,Childhandle) andJobManager. JobManager::add— registers a new background job and prints[id] pid.JobManager::reap— called before each prompt; non-blocking checks all jobs and printsDonefor finished ones, then removes them.JobManager::list_jobs— used by thejobsbuilt-in; printsRunning/Donestatus with+/-markers, removes done entries after display.JobManager::wait_all— blocks until all remaining background jobs finish (called at REPL exit).- Job IDs are the lowest available positive integers, recycled when jobs finish.
- Background job management. Defines
src/shell_helper.rs- Glue code for
rustyline: helper and completer implementations. ShellHelperstruct integrating with rustyline'sHelper,Completer,Hinter, andValidatortraits.ShellCompleterproviding tab completion for:- Built-in commands and PATH executables
- Filenames and directories in the current working directory
- Nested path completion (e.g.,
cat foo/bar/)
compute_lcpfunction: computes the longest common prefix of matching entries for progressive completion.find_matching_entriesfunction: searches directories for entries matching a prefix, distinguishing files from directories.
- Glue code for
- Quoting and escaping
- Single quotes preserve literal text.
- Double quotes allow certain backslash-escaped characters (e.g.,
\",\\,\`,$,!). - Outside quotes,
\escapes the next character.
- Pipelines
- The input is split on unescaped, unquoted
|into a sequence ofParsedCommands.
- The input is split on unescaped, unquoted
- Background execution
- Appending
&to a command runs it as a background job. The shell prints[<job-id>] <pid>and immediately returns to the prompt. Job IDs are the lowest available positive integers and are recycled when jobs finish.
- Appending
- Redirection
1> fileredirects stdout,2> fileredirects stderr,&> fileredirects both.>>sets append mode; a single>truncates.
- History
- Uses
rustylinein-memory history. IfHISTFILEis set, the file is loaded on startup and written back on exit.history -aappends only the new entries since the last write,history -wrewrites the whole file, andhistory -rloads entries from a file.
- Uses
The shell provides comprehensive tab completion for commands and filenames:
- Press TAB after typing a partial command to complete it
- Matches built-in commands and PATH executables (sorted alphabetically)
- Multiple matches displayed as a list; single match auto-completed with trailing space
- Press TAB after typing a partial filename to complete it
- Matches files and directories in the current working directory
- Directories: Completed with trailing
/ - Files: Completed with trailing space
- Completion works in subdirectories
- Example:
cat foo/bar/<TAB>completes files infoo/bar/ - Recursive directory traversal supported
- First TAB: Rings bell (
\x07) if no unique match exists - Second TAB: Lists all matching entries sorted alphabetically (case-insensitive)
- Entries separated by two spaces for readability
- When multiple matches share a common prefix longer than the current input, auto-completes to the LCP
- Allows progressive completion by typing more characters
- Example with files
xyz_dog/,xyz_dog_cow/,xyz_dog_cow_pig.txt:xyz_<TAB>→ auto-completes toxyz_dog(LCP of all matches)xyz_dog_<TAB>→ auto-completes toxyz_dog_cow(LCP of remaining matches)xyz_dog_cow_<TAB>→ auto-completes toxyz_dog_cow_pig.txt(with space, single match)
- The trailing
/or space is only added when exactly one match remains
Prerequisites: Rust toolchain (edition 2021; see Cargo.toml for rust-version).
- Run with Cargo:
cargo run
- Or via the helper script used by CodeCrafters runners:
./your_program.sh
- External command:
$ ls -la
- Pipeline:
$ ls | grep rs | wc -l
- Redirection:
$ echo hello > out.txt $ &> errors_and_output.log ls /no/such/path $ echo append >> out.txt
- Built-ins:
$ pwd $ cd ~/projects $ echo -e "line1\nline2" $ type echo $ history 20
- Background jobs:
$ sleep 10 & [1] 12345 $ jobs [1]+ Running sleep 10 & $ jobs # after sleep finishes [1]+ Done sleep 10
- Tab completion:
$ ech<TAB> # Completes to: echo $ cat fi<TAB> # Completes to: filename.txt (with space) $ cat foo/<TAB> # Completes to: foo/bar/ (directory with trailing slash) $ xyz_<TAB><TAB> # First TAB rings bell, second lists all matches
Defined in Cargo.toml:
rustyline— line editing, history, completion. Features enabled:with-file-history,derive.os_pipe— portable OS pipe creation used for pipeline wiring.
- This is an educational implementation focusing on clarity over complete POSIX compliance.
- Basic job control is implemented: background execution (
&),jobslisting, and automatic reaping. Foreground job resumption (fg/bg), signal forwarding, andwaitare not implemented. - Environment variable expansion, globbing, subshells, and advanced redirection are not implemented.
- Tab completion is limited to the current working directory and explicitly typed paths; it does not follow
$PATHfor filename completion. - The completion system uses a simple LCP algorithm; it may not handle edge cases with Unicode filenames or complex path patterns.
- Behavior may differ from
bash/zshin edge cases, quoting/escaping rules, and error handling.
See the repository license (if any) or treat this as example code for educational purposes.