The Shell API provides a high-performance, cross-platform JavaScript interface for executing shell commands using template literals. It is accessed via the Bun.$ template tag and returns a ShellPromise. The implementation features a custom, non-blocking shell interpreter written in Zig that runs directly within the Bun process to minimize overhead and ensure consistency across Windows, macOS, and Linux.
The Shell API is architected in three distinct layers:
Bun.$ template function and the ShellPromise, ShellOutput, and ShellError classes in src/js/builtins/shell.ts.src/shell_parser/parse.rs (Rust) and the word expansion state machine in src/runtime/shell/states/Expansion.rs (Rust). The JS-visible ParsedShellScript and ShellInterpreter classes are bound via Zig-generated bindings.Sources: src/js/builtins/shell.ts1-10 src/js/builtins/shell.ts106-113 src/shell_parser/parse.rs1-55 src/runtime/shell/states/Expansion.rs1-53
The shell is invoked using the $ template tag, which is created by createBunShellTemplateFunction src/js/builtins/shell.ts1-2 The raw template strings are parsed by createParsedShellScript, producing a ParsedShellScript object that is then run by a ShellInterpreter.
JavaScript values are automatically converted to shell arguments during expansion. Interpolated values are stored out-of-band and referenced via an internal marker byte in the script source, preventing injection of shell syntax.
| Type | Conversion |
|---|---|
string | Passed as single argument (auto-escaped) |
number | Converted to string |
boolean | Converted to "true" or "false" |
null / undefined | Converted to "null" / "undefined" |
Array | Each element becomes a separate argument |
Buffer / Uint8Array | Used as I/O redirection target |
Date | Converted via .toString() |
BigInt | Converted to string |
{ raw: string } | Injected as-is, without quoting |
Sources: test/js/bun/shell/bunshell.test.ts102-117 test/js/bun/shell/bunshell.test.ts141-158 src/js/builtins/shell.ts302-315
The $ object itself is an instance of ShellPrototype src/js/builtins/shell.ts260-300 Configuration methods called directly on $ set defaults for all subsequent invocations:
| Method | Effect |
|---|---|
$.env(obj) | Sets default environment variables |
$.cwd(path) | Sets default working directory |
$.nothrow() | Disables throwing on non-zero exit codes globally |
$.throws(bool) | Explicitly controls throwing behavior globally |
new Bun.$.Shell() creates a new shell constructor with its own independent defaults, without affecting the global $ src/js/builtins/shell.ts317-341:
Sources: src/js/builtins/shell.ts260-300 src/js/builtins/shell.ts317-366 test/js/bun/shell/bunshell.test.ts34-36
The ShellPromise class extends Promise<ShellOutput> and manages the lifecycle of the shell execution src/js/builtins/shell.ts106-112 It captures the stack trace at the point of invocation to provide meaningful error reports src/js/builtins/shell.ts114-118
Sources: src/js/builtins/shell.ts16-72 src/js/builtins/shell.ts74-104 src/js/builtins/shell.ts106-250
These methods configure a single ShellPromise instance and must be called before the promise begins running (i.e., before it is awaited or .then() is called):
.cwd(path): Sets the working directory src/js/builtins/shell.ts149-156.env(object): Sets environment variables src/js/builtins/shell.ts158-166.quiet(bool?): Suppresses stdout/stderr passthrough to the terminal src/js/builtins/shell.ts178-186.nothrow(): Prevents rejection on non-zero exit codes src/js/builtins/shell.ts188-191.throws(boolean): Explicitly controls error throwing src/js/builtins/shell.ts193-196These methods trigger execution and return a promise for the specific format, often defaulting to quiet(true) src/js/builtins/shell.ts198-230:
.text(): Returns stdout as a string src/js/builtins/shell.ts198-201.json(): Parses stdout as JSON src/js/builtins/shell.ts203-206.lines(): Async generator for stdout lines, handling cross-platform line endings src/js/builtins/shell.ts208-216.blob(): Returns stdout as a Blob src/js/builtins/shell.ts227-230The Expansion state machine in src/runtime/shell/states/Expansion.rs handles all word expansions. Expansion order follows POSIX conventions:
| Syntax | Description |
|---|---|
~ | Tilde expansion to $HOME / $USERPROFILE |
$VAR | Variable expansion |
$(cmd) | Command substitution |
{a,b,c} | Brace expansion |
*, **, ? | Glob expansion |
Tilde expansion applies only when ~ appears literally at the start of a word in the script source. A ~ arriving via JS interpolation (${"~"}) is treated as a literal character test/js/bun/shell/bunshell.test.ts509-518
Interpolated values cannot inject glob or brace syntax. Only *, **, {, ,, } written literally in the template literal are treated as expansion metacharacters. The same characters arriving via ${...} interpolation are escaped before being fed to the glob/brace engine src/runtime/shell/states/Expansion.rs284-360 test/js/bun/shell/bunshell.test.ts721-813
Sources: src/runtime/shell/states/Expansion.rs55-67 src/runtime/shell/states/Expansion.rs129-278
Bun Shell supports standard shell redirections. Redirection can target files or JavaScript objects like Buffer, Uint8Array, or BunFile.
Sources: test/js/bun/shell/bunshell.test.ts397-440
Bun implements several shell commands natively (in src/runtime/shell/builtin/) for cross-platform consistency:
| Command | Description |
|---|---|
echo | Prints text to stdout |
cat | Concatenates and prints files |
ls | Lists directory contents |
cd | Changes the working directory |
pwd | Prints the current working directory |
rm | Removes files and directories |
mv | Moves or renames files |
cp | Copies files |
mkdir | Creates directories |
which | Locates a command |
export | Marks variables for export |
exit | Exits with a given code |
true / false | Returns exit code 0 or 1 |
touch | Updates file timestamps / creates files |
dirname / basename | Path utilities |
seq | Sequence generator |
Sources: test/js/bun/shell/bunshell.test.ts60-92 test/js/bun/shell/commands/rm.test.ts27-130 test/js/bun/shell/commands/mv.test.ts10-50
Pipelines (|) connect stdout of one command to stdin of the next. Bun's interpreter manages the underlying pipe file descriptors test/js/bun/shell/leak.test.ts22-23
Sources: test/js/bun/shell/leak.test.ts18-52 test/js/bun/shell/bunshell.test.ts442-453
Brace expansion follows the syntax {a,b,c} and generates multiple words test/js/bun/shell/brace.test.ts5-80 Braces can be nested and combined with glob patterns.
$.braces(pattern) Utility$.braces() is a standalone utility for brace expansion that returns an array of expanded strings test/js/bun/shell/brace.test.ts5-79:
If the pattern contains too many brace groups (exceeds the recursion limit), $.braces() throws instead of crashing test/js/bun/shell/brace.test.ts136-166
The internal brace expansion limit is 65,536 expanded words per pattern src/runtime/shell/states/Expansion.rs314-318
Sources: test/js/bun/shell/brace.test.ts1-168 src/runtime/shell/states/Expansion.rs284-361 src/shell_parser/braces.rs1-50
Glob patterns written literally in the shell template are expanded by GlobWalker src/glob/GlobWalker.zig316-430 When no files match a glob pattern, the shell exits with code 1 and prints bun: no matches found: <pattern> to stderr.
| Pattern | Behavior |
|---|---|
* | Matches any filename in the current directory segment |
** | Matches zero or more directory segments |
? | Matches any single character |
[abc] | Character class |
!pattern | Negation (in glob position) |
Security note: Glob metacharacters (*, **, ?, [...], !) arriving via ${...} interpolation are neutralized and treated as literal characters. Only metacharacters written directly in the template source are active src/runtime/shell/states/Expansion.rs376-420 test/js/bun/shell/bunshell.test.ts721-813
Globs in variable assignment position (FOO=*.txt) do not expand; the pattern is stored as the literal value test/js/bun/shell/bunshell.test.ts697-699
Sources: test/js/bun/shell/bunshell.test.ts690-876 src/runtime/shell/states/Expansion.rs376-420 src/glob/GlobWalker.zig316-546
$.escape(string)Escapes a string so it is safe to embed as a literal value in a shell command using { raw: ... } interpolation test/js/bun/shell/bunshell.test.ts121-158 The escaped form never contains the internal interpolation marker byte sequence (\x08__bunstr_N).
Sources: test/js/bun/shell/bunshell.test.ts121-160
The ShellInterpreter operates as a state machine. When a ShellPromise is awaited or .then() is called, it triggers the internal #run() method src/js/builtins/shell.ts168-176
createShellInterpreter is called with the parsed script and result callbacks src/js/builtins/shell.ts172interp.run() method initiates the native Zig execution loop src/js/builtins/shell.ts174#resolve or #reject callbacks provided during instantiation src/js/builtins/shell.ts110-111#throws is true, a ShellError is initialized and the promise is rejected src/js/builtins/shell.ts124-126Sources: src/js/builtins/shell.ts106-147 src/js/builtins/shell.ts168-176 src/js/builtins/shell.ts241-245
When a command fails (returns a non-zero exit code), Bun throws a ShellError unless .nothrow() was called src/js/builtins/shell.ts124-126
The ShellError object contains:
exitCode: The numeric exit code of the failed command src/js/builtins/shell.ts50stdout: A Buffer containing any captured standard output src/js/builtins/shell.ts48stderr: A Buffer containing any captured error output src/js/builtins/shell.ts49info: An object containing exitCode, stdout, and stderr for backward compatibility src/js/builtins/shell.ts34-43Sources: src/js/builtins/shell.ts16-51 test/integration/bun-types/fixture/index.ts141-146
Refresh this wiki
This wiki was recently refreshed. Please wait 1 day to refresh again.