DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Bun Shell API: Replace Your Bash Scripts with Type-Safe TypeScript

Bash scripts are a necessary evil in most projects — fragile, untyped, impossible to test. Bun 1.2 ships a Shell API that lets you write shell scripts in TypeScript with full type safety and real error handling.

What Is Bun Shell?

Bun.$ is a tagged template literal that executes shell commands with native performance and TypeScript ergonomics.

import { $ } from 'bun';

const output = await $`echo "Hello from Bun Shell"`.text();
console.log(output); // Hello from Bun Shell
Enter fullscreen mode Exit fullscreen mode

Replacing a Build Script

Before (build.sh):

#!/bin/bash
set -e
rm -rf dist
tsc --noEmit
cp -r public dist/
echo "Build complete"
Enter fullscreen mode Exit fullscreen mode

After (TypeScript):

import { $ } from 'bun';

await $`rm -rf dist`;
await $`tsc --noEmit`;
await $`bun build ./src/index.ts --outdir dist`;
await $`cp -r public dist/`;
console.log('Build complete');
Enter fullscreen mode Exit fullscreen mode

Database Migrations with Error Handling

import { $ } from 'bun';

async function runMigrations(env: 'staging' | 'production') {
  const { DATABASE_URL } = process.env;
  if (!DATABASE_URL) throw new Error('DATABASE_URL required');

  // Backup first
  const ts = Date.now();
  await $`pg_dump ${DATABASE_URL} > backups/backup-${ts}.sql`;

  // Run migrations, don't throw on failure
  const result = await $`drizzle-kit migrate`
    .env({ DATABASE_URL, NODE_ENV: env })
    .nothrow();

  if (result.exitCode !== 0) {
    console.error('Migration failed:', result.stderr.text());
    await $`psql ${DATABASE_URL} < backups/backup-${ts}.sql`;
    throw new Error('Migration failed, backup restored');
  }
}
Enter fullscreen mode Exit fullscreen mode

Parallel Tasks

import { $ } from 'bun';

const [lint, test, build] = await Promise.all([
  $`bun run lint`.nothrow(),
  $`bun test`.nothrow(),
  $`bun run build`.nothrow(),
]);

const failures = [
  { name: 'lint', r: lint },
  { name: 'test', r: test },
  { name: 'build', r: build },
].filter(({ r }) => r.exitCode !== 0);

if (failures.length > 0) {
  console.error('Failed:', failures.map(f => f.name).join(', '));
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

Key API Reference

// Text output
const text = await $`ls -la`.text();

// JSON output
const pkg = await $`cat package.json`.json();

// Stream lines (great for long processes)
for await (const line of $`tail -f app.log`.lines()) {
  console.log(line);
}

// Don't throw on non-zero exit
const result = await $`some-command`.nothrow();
console.log(result.exitCode);

// Working directory
await $`npm install`.cwd('/path/to/project');

// Environment variables
await $`node server.js`.env({ PORT: '3000', NODE_ENV: 'production' });
Enter fullscreen mode Exit fullscreen mode

Safe Variable Interpolation

Bun Shell automatically escapes variables — no injection possible:

// SAFE: user input automatically escaped
const userInput = "'; rm -rf /; echo '";
const result = await $`echo ${userInput}`.text();
// Outputs the literal string
Enter fullscreen mode Exit fullscreen mode

Migration from execa

// Before (execa)
import { execa } from 'execa';
const { stdout } = await execa('git', ['log', '--oneline', '-5']);

// After (Bun Shell)
import { $ } from 'bun';
const stdout = await $`git log --oneline -5`.text();
Enter fullscreen mode Exit fullscreen mode

Performance

On a typical CI pipeline (lint + test + build):

  • bash + separate processes: ~8-12s startup overhead
  • Bun Shell: ~0.3s startup, runs in the same Bun process

For CI scripts, dev tooling, deployment helpers, and database scripts — Bun Shell is strictly better than bash.


Ship Your SaaS Faster

Stop reinventing the wheel. whoffagents.com has everything you need:

Top comments (0)