Bun

$ Shell

Bun Shell makes shell scripting with JavaScript & TypeScript fun. It's a cross-platform bash-like shell with seamless JavaScript interop.

Quickstart:

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 rimraf or cross-env', you can use Bun Shell without installing extra dependencies. Common shell commands like ls, cd, rm are implemented natively.
  • Familiar: Bun Shell is a bash-like shell, supporting redirection, pipes, environment variables and more.
  • Globs: Glob patterns are supported natively, including **, *, {expansion}, and more.
  • Template literals: Template literals are used to execute shell commands. This allows for easy interpolation of 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 can be used to run shell scripts (.bun.sh files).
  • Custom interpreter: Bun Shell is written in Zig, along with it's lexer, parser, and interpreter. Bun Shell is a small programming language.

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

What if you want to access 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 will return stdout and stderr as Buffers.

import { $ } from "bun";

const { stdout, stderr } = await $`echo "Hello World!"`.quiet();

console.log(stdout); // Buffer(6) [ 72, 101, 108, 108, 111, 32 ]
console.log(stderr); // Buffer(0) []

Error handling

By default, non-zero exit codes will throw an error. This ShellError contains information about the command run.

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());
}

Throwing can be disabled with .nothrow(). The result's exitCode will need to be checked manually.

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);

The default handling of non-zero exit codes can be configured by calling .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(); // equivilent 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

A command's input or output may be redirected using 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 (all writes to stdout will instead be in stderr)
  • 2>&1 redirect stderr to stdout (all writes to stderr will instead be in stdout)

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

The following JavaScript objects are supported for redirection to:

  • 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 redirect the output from JavaScript objects to 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

The following JavaScript objects are supported for redirection from:

  • 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 allows you to substitute the output of another script 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)`;

This is a textual insertion of the command's output and can be used to, for example, 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"
`;

NOTE: 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

The above will print out:

echo hi

We instead recommend sticking to the $(...) syntax.

Environment variables

Environment variables can be set like in bash:

import { $ } from "bun";

await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\n

You can use string interpolation to set environment variables:

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, process.env is used as the environment variables for all commands.

You can change the environment variables for a single command by calling .env():

import { $ } from "bun";

await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // bar

You can change the default environment variables for all commands by calling $.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

You can reset the environment variables to the default by calling $.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

You can change the working directory of a command by passing a string to .cwd():

import { $ } from "bun";

await $`pwd`.cwd("/tmp"); // /tmp

You can change the default working directory for all commands by calling $.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
  • 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)

This function implements simple 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\"

If you do not want your string to be escaped, wrap it 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, instead of /bin/sh, you can use Bun Shell to run shell scripts.

To do so, just run the script with bun on a file with the .sh extension.

script.sh
echo "Hello World! pwd=$(pwd)"
bun ./script.sh
Hello World! pwd=/home/demo

Scripts with Bun Shell are cross platform, which means they work on Windows:

bun .\script.sh
Hello World! pwd=C:\Users\Demo

Implementation notes

Bun Shell is a small programming language in Bun that is implemented in Zig. It includes a handwritten lexer, parser, and interpreter. Unlike bash, zsh, and other shells, Bun Shell runs operations concurrently.

Credits

Large parts of this API were inspired by zx, dax, and bnx. Thank you to the authors of those projects.