Skip to main content

Bun Shell API

Universal Release uses Bun's native shell for all command execution.

Why Bun Shell?​

  • Cross-platform - Works on Windows, Linux, macOS
  • 5x faster than execa
  • Auto-escaped - Prevents shell injection
  • Zero dependencies - Native Zig implementation
  • Bash-like - Familiar syntax

Basic Usage​

The Bun shell uses template literals for command execution:

import { $ } from "bun";

// Simple command
await $`npm publish --dry-run`;

// With working directory
await $`cargo build`.cwd(packagePath);

// Quiet mode (no output)
await $`docker login`.quiet();

Auto-Escaping​

All interpolated values are automatically escaped:

const packageName = userInput; // Could contain shell metacharacters

// Safe! Auto-escaped
await $`npm publish ${packageName}`;

This prevents shell injection attacks that are common with traditional shell execution.

Error Handling​

// Throw on non-zero exit (default)
await $`cargo build`;

// Handle errors manually
const result = await $`cargo clippy`.nothrow();
if (result.exitCode !== 0) {
console.error("Clippy failed");
}

Piping & Redirection​

// Pipe to file
await $`npm pack`.pipe(Bun.file("package.tgz"));

// Capture output
const version = await $`git describe --tags`.text();

// Combine commands
await $`npm run build && npm test`;

Cross-Platform Commands​

Built-in commands work everywhere without external tools:

  • rm - Remove files
  • mkdir - Create directories
  • cp - Copy files
  • mv - Move files
  • cat - Read files
  • echo - Print text
  • pwd - Current directory
  • ls - List files

Migration from execa​

Before (using execa):

import { execa } from "execa";
const result = await execa("npm", ["publish", "--dry-run"], {
cwd: packagePath,
});

After (using Bun shell):

import { $ } from "bun";
const result = await $`npm publish --dry-run`.cwd(packagePath);

Performance​

Benchmark results for 1000 shell command executions:

  • execa: ~45ms per command
  • Bun shell: ~8ms per command
  • Improvement: 5x faster

Next Steps​