Shell & interpreter
The bash-like shell — lexer, parser, and interpreter — with pipes, redirection, job control, globbing, and TTY/raw-mode handling.
The Shell is the bash-like layer that turns a command line into running
commands against the kernel. It's what a
terminal is wired to, and what sandbox.commands.run() drives under the hood.
The pipeline: lexer → parser → interpreter
A command line goes through three stages (shell/lexer.ts, shell/parser.ts,
shell/interpreter.ts):
- Lexer — tokenizes the input: words, operators (
|,&&,||,;,&), redirections (>,>>,<,2>&1), quotes, and expansions. - Parser — builds an AST of pipelines and command lists.
- Interpreter — walks the AST, expands words (
shell/expander.ts), sets up stdin/stdout/stderr per command, and invokes the command from the registry.
What the shell supports
- Pipelines —
a | b | c, wiring each command's stdout to the next's stdin. - Lists & conditionals —
;,&&,||. - Redirection —
>,>>,<, and file-descriptor duplication like2>&1,>&2,1>&2. (Output redirection appends per write, matching a real stream.) - Background jobs — trailing
&, tracked by the job table. - Globbing —
*,?, character classes, expanded against the VFS. - Variables & expansion — environment variables,
$(command)substitution. - Tab completion & history —
shell/completer.tsandshell/history.ts.
Startup files
On start, the shell sources /etc/profile and the user's ~/.liforc (Lifo's
.bashrc), so aliases and environment set there apply to the session — the same
way a login shell would.
TTY and raw mode
When a box is attached to an interactive terminal, the shell manages the TTY.
shell/terminal-stdin.ts feeds keystrokes to the foreground command, and
ctx.setRawMode(true) switches to raw mode so full-screen and prompt-based CLIs
(editors, create-expo-app, spinners) get keypresses directly instead of
line-buffered input.
Gotcha
Raw mode is the source of a subtle class of bugs: a CLI that leaves stdin in raw
mode with a stray keypress listener can look "busy" after it's done. The
Node runtime handles this by tracking real work
(wall-clock, not tick counters) and silencing output after process.exit() so a
finished CLI returns to the prompt cleanly.
Running commands programmatically
sandbox.commands.run(cmd, opts) runs a full command line through the shell and
collects the result — with options for cwd, env, timeout, an AbortSignal,
streaming onStdout/onStderr, and stdin. Internally each execution threads
its own stream defaults so concurrent runs don't clobber each other's I/O.