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
Replacing a Build Script
Before (build.sh):
#!/bin/bash
set -e
rm -rf dist
tsc --noEmit
cp -r public dist/
echo "Build complete"
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');
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');
}
}
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);
}
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' });
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
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();
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:
- 🚀 AI SaaS Starter Kit — Full-stack Next.js + Stripe + Auth in one repo ($99)
- ⚡ Ship Fast Skill Pack — 50+ Claude Code skills for 10x faster development ($49)
- 🔒 MCP Security Scanner — Audit your MCP servers before they hit production ($29)






