Shell
Use Bun's shell scripting API to run shell commands from JavaScript
Bun Shell makes shell scripting with JavaScript & TypeScript fun. It's a cross-platform bash-like shell with JavaScript interop.
Quickstart:
import { $ } from "bun";
const response = await fetch("https://example.com");
// Use Response as stdin.
await $`cat < ${response} | wc -c`; // 1256Features
- Cross-platform: works on Windows, Linux & macOS. Instead of installing
rimraforcross-env, you can use Bun Shell. Common shell commands likels,cd, andrmare implemented natively. - Familiar: Bun Shell is a bash-like shell that supports redirection, pipes, and environment variables.
- Globs: Glob patterns are supported natively, including
**,*, and{expansion}. - Template literals: Template literals execute shell commands and interpolate variables and expressions.
- Safety: Bun Shell escapes all strings by default, preventing shell injection attacks.
- JavaScript interop: Use
Response,ArrayBuffer,Blob,Bun.file(path)and other JavaScript objects as stdin, stdout, and stderr. - Shell scripting: Bun Shell runs shell scripts (
.bun.shfiles). - Custom interpreter: Bun Shell is a small programming language with its own lexer, parser, and interpreter, written in Rust.
Getting started
The simplest shell command is echo. To run it, use the $ template literal tag:
import { $ } from "bun";
await $`echo "Hello World!"`; // Hello World!By default, shell commands print to stdout. To quiet the output, call .quiet():
import { $ } from "bun";
await $`echo "Hello World!"`.quiet(); // No outputTo read the output of the command as text, use .text():
import { $ } from "bun";
// .text() automatically calls .quiet() for you
const welcome = await $`echo "Hello World!"`.text();
console.log(welcome); // Hello World!\nBy default, awaiting returns stdout and stderr as Buffers.
import { $ } from "bun";
const { stdout, stderr } = await $`echo "Hello!"`.quiet();
console.log(stdout); // Buffer(7) [ 72, 101, 108, 108, 111, 33, 10 ]
console.log(stderr); // Buffer(0) []Error handling
By default, a non-zero exit code throws an error. The ShellError contains information about the command that ran.
import { $ } from "bun";
try {
const output = await $`something-that-may-fail`.text();
console.log(output);
} catch (err) {
console.log(`Failed with code ${err.exitCode}`);
console.log(err.stdout.toString());
console.log(err.stderr.toString());
}.nothrow() disables throwing. Check the result's exitCode yourself.
import { $ } from "bun";
const { stdout, stderr, exitCode } = await $`something-that-may-fail`.nothrow().quiet();
if (exitCode !== 0) {
console.log(`Non-zero exit code ${exitCode}`);
}
console.log(stdout);
console.log(stderr);To change the default for all commands, call .nothrow() or .throws(boolean) on the $ function itself.
import { $ } from "bun";
// shell promises will not throw, meaning you will have to
// check for `exitCode` manually on every shell command.
$.nothrow(); // equivalent to $.throws(false)
// default behavior, non-zero exit codes will throw an error
$.throws(true);
// alias for $.nothrow()
$.throws(false);
await $`something-that-may-fail`; // No exception thrownRedirection
Redirect a command's input or output with the typical Bash operators:
<redirect stdin>or1>redirect stdout2>redirect stderr&>redirect both stdout and stderr>>or1>>redirect stdout, appending to the destination, instead of overwriting2>>redirect stderr, appending to the destination, instead of overwriting&>>redirect both stdout and stderr, appending to the destination, instead of overwriting1>&2redirect stdout to stderr (writes to stdout go to stderr instead)2>&1redirect stderr to stdout (writes to stderr go to stdout instead)
Bun Shell also supports redirecting from and to JavaScript objects.
Example: Redirect output to JavaScript objects (>)
To redirect stdout to a JavaScript object, use the > operator:
import { $ } from "bun";
const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\nYou can redirect output to these JavaScript objects:
Buffer,Uint8Array,Uint16Array,Uint32Array,Int8Array,Int16Array,Int32Array,Float32Array,Float64Array,ArrayBuffer,SharedArrayBuffer(writes to the underlying buffer)Bun.file(path),Bun.file(fd)(writes to the file)
Example: Redirect input from JavaScript objects (<)
To use a JavaScript object as stdin, use the < operator:
import { $ } from "bun";
const response = new Response("hello i am a response body");
const result = await $`cat < ${response}`.text();
console.log(result); // hello i am a response bodyYou can redirect from these JavaScript objects:
Buffer,Uint8Array,Uint16Array,Uint32Array,Int8Array,Int16Array,Int32Array,Float32Array,Float64Array,ArrayBuffer,SharedArrayBuffer(reads from the underlying buffer)Bun.file(path),Bun.file(fd)(reads from the file)Response(reads from the body)
Example: Redirect stdin -> file
import { $ } from "bun";
await $`cat < myfile.txt`;Example: Redirect stdout -> file
import { $ } from "bun";
await $`echo bun! > greeting.txt`;Example: Redirect stderr -> file
import { $ } from "bun";
await $`bun run index.ts 2> errors.txt`;Example: Redirect stderr -> stdout
import { $ } from "bun";
// redirects stderr to stdout, so all output
// will be available on stdout
await $`bun run ./index.ts 2>&1`;Example: Redirect stdout -> stderr
import { $ } from "bun";
// redirects stdout to stderr, so all output
// will be available on stderr
await $`bun run ./index.ts 1>&2`;Piping (|)
Like in bash, you can pipe the output of one command to another:
import { $ } from "bun";
const result = await $`echo "Hello World!" | wc -w`.text();
console.log(result); // 2\nYou can also pipe with JavaScript objects:
import { $ } from "bun";
const response = new Response("hello i am a response body");
const result = await $`cat < ${response} | wc -w`.text();
console.log(result); // 6\nCommand substitution ($(...))
Command substitution inserts the output of another command into the current script:
import { $ } from "bun";
// Prints out the hash of the current commit
await $`echo Hash of current commit: $(git rev-parse HEAD)`;The output is inserted as text, so you can use it to declare a shell variable:
import { $ } from "bun";
await $`
REV=$(git rev-parse HEAD)
docker built -t myapp:$REV
echo Done building docker image "myapp:$REV"
`;Because Bun internally uses the special raw property on the input template literal, using the backtick syntax for command substitution won't work:
import { $ } from "bun";
await $`echo \`echo hi\``;Instead of printing:
hiIt prints:
`echo hi`Use the $(...) syntax instead.
Environment variables
Set environment variables like in bash:
import { $ } from "bun";
await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\nUse string interpolation to set the value:
import { $ } from "bun";
const foo = "bar123";
await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\nInput is escaped by default, preventing shell injection attacks:
import { $ } from "bun";
const foo = "bar123; rm -rf /tmp";
await $`FOO=${foo} bun -e 'console.log(process.env.FOO)'`; // bar123; rm -rf /tmp\nChanging the environment variables
By default, all commands use process.env as their environment variables.
To change the environment variables for a single command, call .env():
import { $ } from "bun";
await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // barTo change the default environment variables for all commands, call $.env:
import { $ } from "bun";
$.env({ FOO: "bar" });
// the globally-set $FOO
await $`echo $FOO`; // bar
// the locally-set $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // bazTo reset the environment variables to the default, call $.env() with no arguments:
import { $ } from "bun";
$.env({ FOO: "bar" });
// the globally-set $FOO
await $`echo $FOO`; // bar
// the locally-set $FOO
await $`echo $FOO`.env(undefined); // ""Changing the working directory
To change the working directory of a command, pass a string to .cwd():
import { $ } from "bun";
await $`pwd`.cwd("/tmp"); // /tmpTo change the default working directory for all commands, call $.cwd:
import { $ } from "bun";
$.cwd("/tmp");
// the globally-set working directory
await $`pwd`; // /tmp
// the locally-set working directory
await $`pwd`.cwd("/"); // /Reading output
To read the output of a command as a string, use .text():
import { $ } from "bun";
const result = await $`echo "Hello World!"`.text();
console.log(result); // Hello World!\nReading output as JSON
To read the output of a command as JSON, use .json():
import { $ } from "bun";
const result = await $`echo '{"foo": "bar"}'`.json();
console.log(result); // { foo: "bar" }Reading output line-by-line
To read the output of a command line-by-line, use .lines():
import { $ } from "bun";
for await (let line of $`echo "Hello World!"`.lines()) {
console.log(line); // Hello World!
}You can also use .lines() on a completed command:
import { $ } from "bun";
const search = "bun";
for await (let line of $`cat list.txt | grep ${search}`.lines()) {
console.log(line);
}Reading output as a Blob
To read the output of a command as a Blob, use .blob():
import { $ } from "bun";
const result = await $`echo "Hello World!"`.blob();
console.log(result); // Blob(13) { size: 13, type: "text/plain" }Builtin Commands
For cross-platform compatibility, Bun Shell implements a set of builtin commands, in addition to reading commands from the PATH environment variable.
cd: change the working directoryls: list files in a directory (supports-lfor long listing format)rm: remove files and directoriesecho: print textpwd: print the working directorybun: run bun in buncattouchmkdirwhichmvexittruefalseyesseqdirnamebasename
Partially implemented:
mv: move files and directories (missing cross-device support)
Not implemented yet, but planned:
- See Issue #9716 for the full list.
Utilities
Bun Shell also implements a set of utilities for working with shells.
$.braces (brace expansion)
$.braces implements brace expansion for shell commands:
import { $ } from "bun";
await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]$.escape (escape strings)
Exposes Bun Shell's escaping logic as a function:
import { $ } from "bun";
console.log($.escape('$(foo) `bar` "baz"'));
// => "\$(foo) \`bar\` \"baz\""To skip escaping, wrap the string in a { raw: 'str' } object:
import { $ } from "bun";
await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;
// => bun: command not found: foo
// => bun: command not found: bar
// => baz.sh file loader
For simple shell scripts, you can use Bun Shell instead of /bin/sh. Pass a file with the .sh extension to bun:
$ echo "Hello World! pwd=$(pwd)"$ bun ./script.sh
Hello World! pwd=/home/demoBun Shell scripts are cross-platform, so they work on Windows:
> bun .\script.shHello World! pwd=C:\Users\DemoImplementation notes
Bun Shell is a small programming language implemented in Rust, with a handwritten lexer, parser, and interpreter. Unlike bash, zsh, and other shells, Bun Shell runs operations concurrently.
Security in the Bun shell
By design, Bun Shell does not invoke a system shell like /bin/sh. It's a
re-implementation of bash that runs in the same Bun process.
When parsing command arguments, it treats all interpolated variables as single, literal strings.
This protects against command injection:
import { $ } from "bun";
const userInput = "my-file.txt; rm -rf /";
// SAFE: `userInput` is treated as a single quoted string
await $`ls ${userInput}`;Here, userInput is treated as a single string, so ls tries to read the
contents of a single directory named my-file.txt; rm -rf /.
Security considerations
While command injection is prevented by default, you are still responsible for security in certain scenarios.
Similar to the Bun.spawn or node:child_process.exec() APIs, you can intentionally
execute a command which spawns a new shell (for example, bash -c) with arguments.
When you do this, you hand off control, and Bun's built-in protections no longer apply to the string interpreted by that new shell.
import { $ } from "bun";
const userInput = "world; touch /tmp/pwned";
// UNSAFE: You have explicitly started a new shell process with `bash -c`.
// This new shell will execute the `touch` command. Any user input
// passed this way must be rigorously sanitized.
await $`bash -c "echo ${userInput}"`;Argument injection
Bun Shell cannot know how an external command interprets its own command-line arguments. An attacker can supply input that the target program recognizes as one of its own options or flags, leading to unintended behavior.
import { $ } from "bun";
// Malicious input formatted as a Git command-line flag
const branch = "--upload-pack=echo pwned";
// UNSAFE: While Bun safely passes the string as a single argument,
// the `git` program itself sees and acts upon the malicious flag.
await $`git ls-remote origin ${branch}`;Recommendation: Always sanitize user-provided input before passing it as an argument to an external command. Validating arguments is your application's responsibility.
Credits
Large parts of this API were inspired by zx, dax, and bnx. Thank you to the authors of those projects.