Skip to main content
Bun Shell makes shell scripting with JavaScript & TypeScript fun. It’s a cross-platform bash-like shell with JavaScript interop. Quickstart:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bindex.ts
import { $ } from "bun";

const response = await fetch("https://example.com");

// Use Response as stdin.
await $`cat < ${response} | wc -c`; // 1256

Features

  • Cross-platform: works on Windows, Linux & macOS. Instead of installing rimraf or cross-env, you can use Bun Shell. Common shell commands like ls, cd, and rm are 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.sh files).
  • 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 output
To 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!\n
By 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 thrown

Redirection

Redirect a command’s input or output with the typical Bash operators:
  • < redirect stdin
  • > or 1> redirect stdout
  • 2> redirect stderr
  • &> redirect both stdout and stderr
  • >> or 1>> redirect stdout, appending to the destination, instead of overwriting
  • 2>> redirect stderr, appending to the destination, instead of overwriting
  • &>> redirect both stdout and stderr, appending to the destination, instead of overwriting
  • 1>&2 redirect stdout to stderr (writes to stdout go to stderr instead)
  • 2>&1 redirect 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!\n
You 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 body
You 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\n
You 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\n

Command 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:
hi
It 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\n
Use string interpolation to set the value:
import { $ } from "bun";

const foo = "bar123";

await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\n
Input 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\n

Changing 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" }); // bar
To 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" }); // baz
To 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"); // /tmp
To 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!\n

Reading 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 directory
  • ls: list files in a directory (supports -l for long listing format)
  • rm: remove files and directories
  • echo: print text
  • pwd: print the working directory
  • bun: run bun in bun
  • cat
  • touch
  • mkdir
  • which
  • mv
  • exit
  • true
  • false
  • yes
  • seq
  • dirname
  • basename
Partially implemented:
  • mv: move files and directories (missing cross-device support)
Not implemented yet, but planned:

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:
script.sh
echo "Hello World! pwd=$(pwd)"
terminal
bun ./script.sh
Hello World! pwd=/home/demo
Bun Shell scripts are cross-platform, so they work on Windows:
powershell
bun .\script.sh
Hello World! pwd=C:\Users\Demo

Implementation 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.