rsh is a small UNIX-like shell that implements a practical subset of POSIX
shell behavior: quoting, variables, command substitution, globbing,
redirections, pipelines, control flow, aliases and a persistent command
history. It is a teaching project, so the code is intentionally small, readable
and heavily commented.
Documentation is also available in Spanish.
cargo build --release
./target/release/rshOr directly with:
cargo runRun the tokenizer/expander test suite:
cargo testsrc/
main.rs -> main loop (rustyline): prompt + tab-completer helper
tokenizer.rs -> raw text -> tokens (quotes, escapes, comments)
parser.rs -> tokens -> Jobs (pipelines joined by && || ;, groups, functions)
expand.rs -> expansion of $VAR, ${VAR}, $?, $(...), $((...)), ~, globs, IFS split
builtins.rs -> cd, pwd, exit, export, unset, echo, alias, unalias, which/type,
test/[, jobs, fg, bg — plus the shell state (vars, functions,
positional params, job table)
executor.rs -> runs pipelines, applies redirects, honors &&/||/;/&, job control
jobctl.rs -> process groups, terminal control and signals (^C/^Z), waitpid
- Prompt showing the current directory (with
~instead of the home path) and a✗marker when the last command failed. - Persistent history in
~/.rsh_history, navigable with the arrow keys (viarustyline). - POSIX-style quotes: single quotes are 100% literal (neither
$VARnor$(...)nor globs are touched inside); double quotes expand$VARand$(...)but do NOT perform pathname or tilde expansion. Backslash escapes work outside quotes. - Comments with
#. - Pipelines:
cmd1 | cmd2 | cmd3. - Redirections:
>,>>,<,2>. - Connectors:
&&,||,;. - Background with
&(does not wait; prints the PID). - Command substitution:
$(cmd)and backticks (rewritten to$(...)internally). The command runs with its real stdout redirected to a temporary file, so it works with external binaries and not only builtins. It simulates a subshell:export X=1inside does not leak into the parent. - Variables:
VAR=value(plain assignment),export VAR=value,$VAR,${VAR},unset. - Extended parameter expansion:
${VAR:-default},${VAR:=default}(also assigns),${VAR:?message}(error if unset),${VAR:+alt},${#VAR}(length). - Exit code accessible as
$?(works both inside and outside double quotes). - Arithmetic expansion:
$(( 1 + 2 * 3 ))with+ - * / %, parentheses, unary signs and variable references ($n,n,$1...). It is the natural building block for counter loops. - Field splitting: the unquoted result of
$VAR,$(cmd),$@and arithmetic is split into separate words on whitespace (IFS behavior). Quoted whitespace is preserved inside a single word. ~and glob expansion (*.txt,?,[...]).- Aliases:
alias ll='ls -la',unalias. which/typeto tell whether a command is a builtin or an external binary.- Shell functions and positional parameters:
name() { ... ; }with$1,$2, ...,$0,$#,$@. Functions are callable recursively and are also available inside$(...). test/[as a builtin: unary (-z,-n,-f,-d,-r,-w,-x), binary (=,!=,-eq,-ne,-lt,-le,-gt,-ge) and!negation.- Control structures:
if/then/elif/else/fi,for VAR in ...; do ... done,while ... do ... done,until ... do ... done,case ... in pattern) ... ;; esac(casepatterns support*,?,[...]just like a glob).{ ...; }command groups. - Job control: background jobs with
&are tracked in a job table;jobslists them,fg %Nbrings one to the foreground andbg %Nresumes a stopped job in the background.waitblocks until jobs finish,disownremoves them from the table andkill [-s sig]sends signals. Job specifiers%+/%%(most recent),%-(second most recent),%Nand a bare PID are accepted byfg/bg/wait/disown/kill. Finished background jobs are reaped automatically before every prompt and, in interactive sessions, announced with a[N] Done cmdline. readbuiltin:read [-r] name1 name2 ...parses a line from stdin into variables (the last one receives the rest), sowhile read x; do ...; doneworks.- Builtins and functions everywhere in a pipe: stages that resolve to a
builtin or shell function run in a forked subshell connected to the pipe
(
echo hi | wc -c,printf 'a\nb\n' | grep a,export X=1 | cat), so they no longer need an external binary of the same name. - Here-documents:
cat <<EOF/<<-(tab-stripping) /<<'EOF'(no expansion); bodies expand$VARand$(cmd), gather with a>continuation prompt, and feed any command, pipeline stage or compound (while read l; ...; done <<E). - Multi-line input (PS2): unfinished constructs (
if ..., unclosed quotes, dangling|,$(...spanning lines) keep reading with a>prompt until they parse. - Compound commands as pipeline stages:
if/for/while/until/case,{ }groups and( )subshells can be piped or backgrounded (if ...; fi | cat,echo x | while read l; do ...; done,mycommand &). A stage that is compound runs in a forked subshell connected to the pipe, like POSIX shells. - Pipeline-wide job control: all stages of a pipeline run in a single
process group that owns the terminal while it runs, so ^C interrupts and
^Z stops the whole pipeline; it becomes one job you can
fg/bg. - Tab completion for command names and file paths (
rustyline::Helper). - Finer signal handling: every foreground child runs in its own process
group and receives the terminal, so Ctrl+C interrupts only the command and
Ctrl+Z suspends it (it becomes a job you can
fg/bg). The shell itself survives both. - Ctrl+C at the prompt cancels the current line without closing the shell; Ctrl+D closes it (like bash).
Working examples:
for f in *.rs; do
echo "file: $f"
done
if [ -z "$FOO" ]; then
echo "FOO is not set"
fi
GREET=hello
echo "greeting=$GREET" # variables assigned without `export`
false
echo "exit code was $?" # $? expands inside double quotes too
v=$(echo sub) ; echo "v=$v" # command substitution into a variable
fact() { if [ "$1" -le 1 ]; then echo 1;
else r=$(fact $(($1 - 1))); echo $(($1 * r)); fi }
echo "fact 5 = $(fact 5)" # shell functions + recursion + arithmetic
sleep 5 &
jobs # [ 1] Running sleep 5
fg %1 # bring it back to the foreground$0/$@/$#positional parameters exist, butshift/getopts, arithmeticfor ((...))loops and$(...)inside heredoc bodies are not implemented yet.
getopts/shiftand arithmeticfor ((...)).
MIT.