Your First Portable Bun Script

Lesson 0001 · Glossary · Up next: File I/O (Bun.file, Bun.write)

The win, in one line: by the end of this lesson you will have ported one of your daily bash scripts to a Bun script that reads arguments, exits cleanly, and runs identically on any OS.

You already know JavaScript. So replacing bash is mostly about learning where things live in Bun — how scripts run, how to read the parts bash gave you with $1, $?, and >&2.

1. Running a script

Bun runs .ts (and .js) files directly — no config, no build step, no separate TypeScript install. Bun transpiles on the fly with its native transpiler.

bun run script.ts      // explicit — preferred
bun script.ts          // "naked" — identical

Top-level await is allowed, so a script can read files or hit an HTTP endpoint without wrapping everything in an async function — the way promises let you write. If you run the whole thing through bun run -, Bun even reads code from stdin. (Bun docs: CLI run)

2. Making a script directly executable

Bash scripts are convenient because you can ./greet.sh them. Bun scripts can too — two steps, identical to what you do in bash:

# first line of the file:
#!/usr/bin/env bun

# then, once:
chmod +x script.ts

Note the extension matters: Bun treats a .sh file as a Bun Shell script. For JavaScript/TypeScript, keep .ts (or .js). Verified on Bun 1.4.1: shebang + chmod +x + ./script.ts works. (Bun docs: core runtime)

3. Reading arguments — the $1 replacement

Where bash gives you $0, $1, $@, Bun gives you Bun.argv — a plain array (an alias of process.argv). (Bun docs: utils — Bun.argv)

// bun run greet.ts Ada Lovelace
Bun.argv[0] // path to the bun binary
Bun.argv[1] // path to the script
Bun.argv[2] // "Ada"          <- arguments start here
Bun.argv[3] // "Lovelace"
const args = Bun.argv.slice(2); // ["Ada", "Lovelace"]

4. Exit codes and stderr — the $? and >&2 replacement

Schedulers and the caller judge your script by its exit code: 0 means success, anything else means failure. Where bash writes usage: ... >&2 and exit 1, write:

console.error("usage: greet.ts <name>"); // stderr, keeps stdout clean
process.exit(1);                          // nonzero = failure

Prefer process.exitCode = 1 when you still have work in flight; process.exit(1) stops immediately.

The bash → Bun map

bashBun
$1, $2Bun.argv[2], Bun.argv[3] — or destructure Bun.argv.slice(2)
$@ (all args)Bun.argv.slice(2)
echo ... >&2console.error(...)
exit 1 / $?process.exit(1) / process.exitCode = 1
"hi $name"`hi ${name}`
#!/usr/bin/env bash#!/usr/bin/env bun

The portability payoff: the bash version needs bash installed and its [[ ]] / >&2 syntax differs in sh. The Bun version runs identically with one binary on macOS, Linux, and Windows.

Practice

Recall check (retrieval practice — from memory, no peeking)

1. Which command runs main.ts?

bun run main.ts (or the naked bun main.ts) runs TypeScript directly. tsc only type-checks; it doesn't execute.

2. In Bun.argv, where do your arguments start?

Index 0 is the bun binary, index 1 is the script path, so your arguments start at index 2.

3. Which makes ./script.ts executable directly?

A #!/usr/bin/env bun shebang as line one, then chmod +x script.ts. The file must keep a .ts/.js extension, not .sh.

Hands-on: port your first script

Here is the bash you use daily. Your job: rebuild it in greet.ts using only what this lesson taught.

#!/usr/bin/env bash
name=$1
if [[ -z "$name" ]]; then
  echo "usage: $0 <name>" >&2
  exit 1
fi
echo "hello, $name"
  1. Write greet.ts with the Bun shebang, reading the name from Bun.argv, using a template literal for the greeting, console.error for the usage message, and an exit code of 1 when the name is missing.
  2. chmod +x greet.ts, then run ./greet.ts Ada and ./greet.ts (no args).
  3. Extend it: accept an optional --shout flag after the name and uppercase the greeting when present.

Check your work (immediate feedback):

./greet.ts Ada            # prints: hello, Ada        exit 0
./greet.ts                # prints usage to stderr     exit 1
bash -c './greet.ts Ada'  # prove it works from bash too

If your no-arg run returned exit code 1 and printed to stderr, you've built a script that schedulers, CI, and humans can trust — running it on macOS or Windows would behave the same.

Read this next

Your primary source for this lesson is the official page you should skim now: Bun Docs: Core Runtime. It frames the engine behind everything, and its sibling pages (CLI run, utils) back the exact claims above.

Also file away the mapping you just practised — it's the entire migration mindset: find the bash habit, locate its Bun twin, keep the contract (exit codes, streams) identical.