Skip to main content
Bun includes a built-in, cross-platform shell that you can use directly in JavaScript and TypeScript files. It uses the $ template literal for running shell commands.

Basic Usage

import { $ } from "bun";

// Run a command
await $("echo Hello World");

// Capture output
const output = await $("ls -la").text();
console.log(output);

// Check exit code
const result = await $("git status");
if (result.exitCode === 0) {
  console.log("Git working directory is clean");
}

Template Interpolation

Safe Variable Substitution

Variables are automatically escaped:
const filename = "my file.txt";

// Variables are safely quoted
await $("cat ${filename}");
// Executes: cat "my file.txt"

const dir = "/home/user";
const pattern = "*.js";
await $("ls ${dir}/${pattern}");

Raw Strings

Use .raw to pass values without escaping:
const flags = "-la";
await $("ls ${flags.raw}");

// For complex expressions
const sorting = process.env.REVERSE ? "-r" : "";
await $("ls ${sorting.raw} -t");

Piping and Redirection

Pipes

Connect commands with pipes:
await $("cat file.txt | grep pattern | sort | uniq");

// Multi-line for readability
await $(`
  cat access.log |
  grep ERROR |
  wc -l
`);

Input/Output Redirection

// Write to file
await $("echo Hello > output.txt");

// Append to file
await $("echo World >> output.txt");

// Read from file
await $("wc -l < input.txt");

// Redirect stderr
await $("command 2> errors.log");

// Redirect both stdout and stderr
await $("command > output.log 2>&1");

Here Documents

await $(`
  cat << EOF > config.txt
  line 1
  line 2
  line 3
  EOF
`);

Control Flow

Conditional Execution

// AND - run second command only if first succeeds
await $("mkdir temp && cd temp && touch file.txt");

// OR - run second command only if first fails
await $("test -f config.json || cp default.json config.json");

Command Grouping

// Sequential commands
await $("cd src; pwd; ls");

// Background jobs
await $("npm run dev &");

Working with Output

Text Output

const text = await $("cat file.txt").text();
console.log(text);

JSON Output

const data = await $("curl https://api.example.com/data").json();
console.log(data);

Binary Data

const buffer = await $("cat image.png").arrayBuffer();

Line-by-Line

const lines = await $("cat file.txt").lines();
for (const line of lines) {
  console.log(line);
}

Streaming

const proc = $("tail -f /var/log/system.log");

for await (const chunk of proc.stdout) {
  console.log(new TextDecoder().decode(chunk));
}

Error Handling

Checking Exit Codes

const result = await $("git pull");

if (result.exitCode !== 0) {
  console.error("Git pull failed:", result.stderr.toString());
  process.exit(1);
}

Exceptions

By default, commands don’t throw on non-zero exit:
// Check manually
const result = await $("false");
console.log(result.exitCode); // 1

// Or use .quiet() to suppress stderr
const result = await $("command-that-might-fail").quiet();

Try/Catch

try {
  await $("risky-command");
} catch (err) {
  if (err.exitCode) {
    console.error(`Command failed with code ${err.exitCode}`);
  }
}

Environment Variables

Setting Variables

// For a single command
await $("NODE_ENV=production npm run build");

// Using .env()
const result = await $("node script.js").env({
  NODE_ENV: "production",
  API_KEY: process.env.API_KEY,
});

Accessing Variables

const home = await $("echo $HOME").text();
console.log(home.trim());

Options

Change Directory

await $("ls").cwd("/tmp");

// Or inline
await $("cd /tmp && ls");

Quiet Mode

Suppress stderr output:
const result = await $("command").quiet();

Nothrow

Prevent throwing on errors:
const result = await $("might-fail").nothrow();
if (!result.success) {
  console.log("Command failed but we handled it");
}

Built-in Commands

Bun’s shell includes cross-platform implementations of common commands:
  • cd - Change directory
  • echo - Print text
  • ls - List files
  • cat - Concatenate files
  • rm - Remove files
  • mkdir - Create directories
  • mv - Move files
  • cp - Copy files
  • pwd - Print working directory
  • which - Locate commands
  • exit - Exit with code
These work identically on Windows, macOS, and Linux.

Advanced Features

Glob Patterns

// Match multiple files
await $("cat src/**/*.ts | wc -l");

// Brace expansion
await $("touch file{1,2,3}.txt");

Command Substitution

const count = await $("ls | wc -l").text();
await $("echo There are ${count} files");

Process Substitution

// Compare outputs
await $("diff <(ls dir1) <(ls dir2)");

Running Scripts

Inline Scripts

import { $ } from "bun";

await $(`
  echo "Starting build..."
  rm -rf dist
  mkdir -p dist
  bun build src/index.ts --outdir dist
  echo "Build complete!"
`);

Script Files

Create executable shell scripts with Bun:
#!/usr/bin/env bun
import { $ } from "bun";

const branch = await $("git branch --show-current").text();
console.log(`Current branch: ${branch.trim()}`);

if (branch.includes("main")) {
  console.log("Deploying to production...");
  await $("npm run deploy:prod");
} else {
  console.log("Deploying to staging...");
  await $("npm run deploy:staging");
}
Make it executable:
chmod +x deploy.ts
./deploy.ts

Performance

Bun’s shell is optimized for:
  • Fast startup - No subprocess overhead for built-in commands
  • Memory efficiency - Streams data instead of buffering
  • Concurrent execution - Multiple commands run in parallel where possible

Differences from Bash

Not a Full Shell

Bun’s shell supports common scripting patterns but is not POSIX-compliant:
  • No functions or aliases
  • Limited job control
  • Simplified parameter expansion

Web Standards

Bun uses Web APIs:
  • ReadableStream instead of Unix pipes
  • TextDecoder for encoding
  • fetch() for HTTP

Comparison with Other Tools

vs. Node.js child_process

// Bun Shell - concise
await $("ls | grep .ts | wc -l");

// Node - verbose
const { spawn } = require('child_process');
const ls = spawn('ls');
const grep = spawn('grep', ['.ts']);
const wc = spawn('wc', ['-l']);
ls.stdout.pipe(grep.stdin);
grep.stdout.pipe(wc.stdin);

vs. Zx (Google)

Bun’s shell is similar to zx but:
  • Built-in (no extra dependency)
  • Faster (no subprocess for simple commands)
  • Cross-platform (works on Windows natively)

Best Practices

  1. Use template literals for readability
    await $(`
      npm install &&
      npm run build &&
      npm test
    `);
    
  2. Check exit codes for critical commands
    const result = await $("npm test");
    if (result.exitCode !== 0) process.exit(1);
    
  3. Stream large outputs
    for await (const line of $("tail -f log.txt").lines()) {
      console.log(line);
    }
    
  4. Use quiet mode for noisy commands
    await $("npm install").quiet();
    

Build docs developers (and LLMs) love