oven-sh/bun
Shell
Active contributors: Jarred Sumner, Dylan Conway
Bun.$ is an embedded cross-platform shell — POSIX-ish syntax that runs on Windows without cmd.exe/PowerShell/Git-Bash and on POSIX without invoking /bin/sh. The implementation lives entirely in src/shell/.
Purpose
- Execute pipelines, redirections, command substitutions, and globbing portably.
- Avoid the cost and pitfalls of spawning a real shell subprocess for one-shot commands.
- Integrate with the JavaScript runtime:
await $\echo hello`returns aShellOutput` JS object. - Implement the
bun -e '...'shell mode where Bun acts likebash -c.
Directory layout
src/shell/
├── shell.zig # the parser + AST (~175 KB)
├── interpreter.zig # the runtime (~82 KB)
├── braces.zig # brace expansion (`{a,b}`)
├── states/ # state machines per construct (pipeline, if, command, ...)
├── builtin/ # cd, echo, mv, rm, true, false, ... implemented in Zig
├── Builtin.zig # builtin dispatch
├── ParsedShellScript.zig
├── EnvMap.zig # cross-platform env handling (Windows is case-insensitive)
├── EnvStr.zig
├── IO.zig / IOReader.zig / IOWriter.zig # async stdin/stdout/stderr
├── Yield.zig # cooperative-yield primitive
├── RefCountedStr.zig
├── subproc.zig # subprocess management
└── util.zigKey abstractions
| Type | File | Purpose |
|---|---|---|
Shell parser |
src/shell/shell.zig |
Tokenises and parses a script into an AST: pipelines, simple commands, redirections, expansions. |
Interpreter |
src/shell/interpreter.zig |
Runs the AST against the event loop. Each construct is a state machine. |
| State machines | src/shell/states/ |
One file per AST node: cmd.zig, pipeline.zig, if.zig, subshell.zig, script.zig, ... |
Yield |
src/shell/Yield.zig |
A cooperative async primitive — tells the interpreter to suspend and resume when an I/O completes. |
IOReader / IOWriter |
src/shell/IOReader.zig, IOWriter.zig |
Non-blocking pipes wired into the event loop. |
Builtin |
src/shell/Builtin.zig, src/shell/builtin/ |
In-process implementations of common commands (cd, echo, mv, rm, cat, pwd, which, printf, seq, false, true, ...). |
ParsedShellScript |
src/shell/ParsedShellScript.zig |
Cached parsed form of a script template literal. |
Pipeline execution
graph TD
Source["$\`a | b > c.txt\`"] --> Lexer
Lexer --> Parser
Parser --> AST[shell AST]
AST --> Init[Interpreter.init]
Init --> Top[script state]
Top --> Pipe[pipeline state]
Pipe --> CmdA[cmd state: a]
Pipe --> CmdB[cmd state: b]
CmdA -->|stdout pipe| CmdB
CmdB -->|stdout fd| File[c.txt]
CmdA --> Yield1[Yield to event loop]
CmdB --> Yield2[Yield to event loop]Each state owns its inputs and outputs and emits Yields to suspend. The interpreter loop is essentially a coroutine driver tied to the JS event loop, so Bun.$ doesn't block JS execution.
Builtins vs spawned commands
The shell prefers in-process builtins. The list is in src/shell/Builtin.zig. Examples: cd, pwd, echo, printf, mkdir, rm, rmdir, mv, cp, cat, which, true, false, [, seq, read, export, unset, dirname, basename, yes, expr, exit, kill, touch, ls. These are noticeably faster than fork+exec and work uniformly on Windows.
If a name doesn't match a builtin, the shell looks it up on $PATH and spawns it via subproc.zig (which uses Bun's normal spawn machinery: posix_spawn on POSIX, CreateProcessW on Windows).
Expansions
The parser supports:
| Expansion | Implementation |
|---|---|
$VAR, ${VAR} |
parser + EnvMap.zig |
~, ~user |
interpreter.zig |
$(cmd) |
spawn a sub-interpreter, capture stdout |
`cmd` |
same as $(cmd) |
*, ?, [abc] glob |
src/glob/ (bun.glob) |
{a,b,c} braces |
src/shell/braces.zig |
>, >>, <, 2>, &>, here-docs |
interpreter.zig + IO*.zig |
&&, ||, ; |
parser + states/ |
Brace expansion happens before glob expansion. Globs are evaluated via bun.glob (src/glob/) which is the same engine that backs Bun.Glob in JavaScript.
Cross-platform env
EnvMap.zig and EnvStr.zig handle the Windows quirk that environment variables are case-insensitive but case-preserving. Lookups normalise to uppercase on Windows and stay verbatim on POSIX. The shell's view of process.env is an EnvMap, not a std.StringHashMap.
Path semantics are also handled: pipelines on Windows use the same \\.\\pipe\\... named pipes that uSockets uses elsewhere; redirections honour Windows' open-mode flags and reserved file names (CON, NUL, ...).
JavaScript integration
The user-facing Bun.$ API is in src/js/bun/$.ts plus the host bindings in src/bun.js/api/Shell.classes.ts / src/bun.js/api/ParsedShellScript.classes.ts. A tagged template literal is parsed once, cached as a ParsedShellScript, and re-executed with fresh interpolations. The result is a ShellOutput with stdout, stderr, exitCode.
Streaming consumption (for await (const chunk of $\...`.lines())) is supported by piping the interpreter's stdout into a JS ReadableStream`.
bun -e '<shell>' mode
bun -e runs the argument as JS. To run as shell, use bun --shell. The dispatcher in src/cli.zig and src/cli/exec_command.zig decides which interpreter to invoke.
Integration points
- Event loop —
Yieldsuspends the interpreter; the loop schedules continuation when an I/O completes. - Subprocess —
src/shell/subproc.zigcalls intobun.spawn(src/bun.js/api/bun/process.zig). - Glob —
src/glob/is the same engine used byBun.Glob. - Filesystem —
bun.sys.*formv,cp,rm,mkdirbuiltins. - Encoding —
src/string/for case-insensitive matching and Latin-1 / UTF-16 round-trips.
Entry points for modification
- To add a builtin, drop
<name>.ziginsrc/shell/builtin/and register it insrc/shell/Builtin.zig. The builtin should expose a state struct that the interpreter can drive. - To support a new shell construct, extend the parser in
src/shell/shell.zigand add a state insrc/shell/states/. - To change globbing semantics, edit
src/glob/.
Key source files
| File | Purpose |
|---|---|
src/shell/shell.zig |
Lexer + parser + AST. |
src/shell/interpreter.zig |
Runtime driver. |
src/shell/states/ |
One file per AST node kind. |
src/shell/Builtin.zig and src/shell/builtin/ |
In-process commands. |
src/shell/IOReader.zig, src/shell/IOWriter.zig |
Async I/O. |
src/shell/EnvMap.zig |
Cross-platform env. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.