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:

index.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. It implements common shell commands like ls, cd, and rm natively.
  • Familiar: Bun Shell is a bash-like shell that supports redirection, pipes, and environment variables.
  • Globs: Bun Shell supports glob patterns 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 (.sh files).
  • Custom interpreter: Bun Shell is a small programming language with its own lexer, parser, and interpreter, written in Rust.

Getting started#

Start with 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)`;

Bun Shell inserts the output as text, so you can use it to declare a shell variable:

import { $ } from "bun";

await $`
  REV=$(git rev-parse HEAD)
  docker build -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 doesn'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

Bun Shell escapes input 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

$.env();

// the default $FOO
await $`echo $FOO`; // ""

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 piped 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

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, Bun Shell 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, Bun Shell treats userInput as a single string, so ls tries to read the contents of a single directory named my-file.txt; rm -rf /.

Security considerations#

While Bun Shell prevents command injection 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.