A small interactive shell written from scratch in Rust. It runs as a raw-terminal REPL: it reads the keyboard one keystroke at a time, supports line editing, history navigation, and TAB autocompletion, then parses and executes builtins, external programs, pipelines, redirections, and background jobs.
$ declare name=world
$ echo "hello, $name" | tr a-z A-Z
HELLO, WORLD
$ ls /nope 2> errors.txt
$ sleep 5 &
[1] 48213
$ jobs
[1]+ Running sleep 5 &
$ ec<TAB> # → completes to "echo "
- Language: Rust 2021, MSRV 1.82
- Dependencies:
termion(raw mode),regex(variable expansion),anyhow(error handling) - Tests: 99 unit tests (
cargo test)
- Character-by-character input with a live prompt.
- History navigation with ↑ / ↓.
- Backspace line editing.
- TAB completion — two-stage, bash-style:
- First TAB completes the longest common prefix (or rings the bell
if ambiguous); appends a trailing space for commands/files and
/for directories. - Second TAB lists all candidates.
- Completes command names (builtins + everything on
$PATH) when at the start of a line, and arguments (filesystem paths, or a programmable completer) otherwise.
- First TAB completes the longest common prefix (or rings the bell
if ambiguous); appends a trailing space for commands/files and
- Ctrl-D exits cleanly.
- Variable expansion:
$VARand${VAR}, expanded from shell variables set viadeclare(process environment variables are not read for expansion). Quote-aware — single quotes suppress expansion, so afterdeclare x=hithe lineecho '$x'prints the literal$xwhileecho "$x"expands tohi. An undefined variable expands to the empty string. - Quoting: single quotes (fully literal), double quotes (with
\$,\`,\\,\!,\"escape rules), and backslash escapes outside quotes.
- Builtins:
echo,exit,type,pwd,cd,history,jobs,complete,declare(see Builtins below). - External programs resolved against
$PATH. - Pipelines:
a | b | c, mixing builtins and external programs. - Redirections:
>/1>,>>/1>>(stdout, truncate/append),2>,2>>(stderr, truncate/append). Missing parent directories are created automatically. - Background jobs: trailing
&runs a command in the background;jobslists them and reports completion.
- Loaded from / saved to the file named by
$HISTFILE. history -r <file>reads,-w <file>writes the full list,-a <file>appends only the entries added since the last append.
The shell needs a real TTY for raw mode, so run it in a terminal.
cargo run # debug build (extra diagnostics)
cargo run --release # release buildcargo testAll tests are inline #[cfg(test)] unit tests covering the pure logic: the
lexer, variable expansion, the command grammar, history index math and file I/O,
job bookkeeping, and autocomplete slicing. There is no TTY or child-process
dependency in the test suite (background-job tests spawn true).
| Builtin | Usage | Notes |
|---|---|---|
echo |
echo [args...] |
Joins args with spaces + newline. |
exit |
exit [code] |
Exits with code (default 0). |
type |
type <name> |
Reports whether name is a builtin or a $PATH executable. |
pwd |
pwd |
Prints the current working directory. |
cd |
cd <dir> |
Changes directory; bare ~ maps to $HOME. |
history |
history [n] · -r/-w/-a <file> |
Lists history (last n), or reads/writes/appends a file. |
jobs |
jobs |
Lists background jobs with +/- markers and status. |
declare |
declare KEY=VALUE · -p KEY |
Sets / prints a shell variable (used for $ expansion). |
complete |
complete -C <prog> <cmd> · -p <cmd> · -r <cmd> |
Registers / inspects / removes a programmable completer. |
complete -C <program> <command> registers an external program as the argument
completer for <command>. On TAB, the shell invokes that program with the
bash-style COMP_LINE and COMP_POINT environment variables set, and reads
whitespace-separated candidates from its stdout. See
completer_script.py for a sample.
The flow for each submitted line is:
read keys → expand variables → tokenize → split into Commands → run → flush pipeline
src/
├── main.rs Entry point: build a Shell, run it, exit with its code.
├── console/
│ ├── shell.rs Shell: the raw-mode key loop and per-line orchestration.
│ ├── pipeline.rs Pipeline: stdout/stderr routing, child chaining, jobs.
│ ├── memory.rs Memory: shell variables + quote-aware $var expansion.
│ ├── job_ui.rs Display formatting for `jobs` output.
│ └── mod.rs Piped trait + InputWord (buffer-slicing for completion).
├── command/
│ ├── mod.rs Command enum: Builtin | Executable.
│ ├── builtin.rs Builtin structs + IDENT/IDENTS name registry.
│ ├── parser.rs Command::parse — the grammar (tokens → one Command).
│ └── runner.rs Command::run — executes every variant.
├── parser/mod.rs parse_args — the lexer (raw line → Vec<String> tokens).
├── autocomplete/ TAB completion: entries, sources, and Completer scripts.
├── history.rs History: ring of commands + $HISTFILE persistence.
└── utils.rs $PATH lookup, directory enumeration, env helpers.
The shell deliberately separates lexing from grammar:
parser::parse_args(the lexer) splits a raw line into words, handling quotes and backslash escapes. Output isVec<String>tokens.Command::parse(the grammar) consumes aVecDeque<String>of tokens and produces oneCommand, stopping at operator tokens (>,>>,2>,2>>,|). Called in a loop, one line yields a sequence of Commands joined by those operators.
Builtins never print to stdout directly. All output flows through the Piped
trait, implemented by console::Pipeline, which routes text, spawns external
children for |, applies redirections, captures stderr on worker threads, and
tracks background jobs. Pipeline::finish flushes and waits at the end of each
line.
- Add a struct +
Builtinenum variant incommand/builtin.rs(register its name in theregister_builtins!table). - Add a parse arm in
command/parser.rs. - Add an exec arm in
command/runner.rs.
This is a learning project, not a daily-driver shell. Known gaps:
- No
&&,||, or;command sequencing, and no glob (*/?) expansion. - No
export—declarevariables are shell-local and not passed to children as environment variables. - Line editing is append/backspace only (no Left/Right cursor movement, Home/End, or word-delete).
- No Ctrl-C / SIGINT handling.
&must be a separate trailing token, and only external commands can be backgrounded.
Bootstrapped from the CodeCrafters "Build Your Own Shell" challenge.
