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):

  1. Lexer — tokenizes the input: words, operators (|, &&, ||, ;, &), redirections (>, >>, <, 2>&1), quotes, and expansions.
  2. Parser — builds an AST of pipelines and command lists.
  3. 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

  • Pipelinesa | b | c, wiring each command's stdout to the next's stdin.
  • Lists & conditionals;, &&, ||.
  • Redirection>, >>, <, and file-descriptor duplication like 2>&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 & historyshell/completer.ts and shell/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.