Bun Shell 让使用 JavaScript & TypeScript 编写 shell 脚本变得有趣。它是一个跨平台的类 bash shell,具备无缝的 JavaScript 互操作性。
快速开始:

index.ts
import { $ } from "bun";
const response = await fetch("https://example.com");
// 使用 Response 作为 stdin。
await $`cat < ${response} | wc -c`; // 1256
- 跨平台:支持 Windows、Linux 和 macOS。无需安装额外依赖,也能使用 Bun Shell 替代
rimraf 或 cross-env。诸如 ls、cd、rm 等常见 shell 命令均为原生实现。
- 熟悉感:Bun Shell 类似 bash,支持重定向、管道、环境变量等多种功能。
- 通配符:原生支持通配符模式,包括
**、*、{扩展} 等。
- 模板字面量:通过模板字面量执行 shell 命令,方便插值变量和表达式。
- 安全性:默认对所有字符串进行转义,防止 shell 注入攻击。
- JavaScript 互操作:可将
Response、ArrayBuffer、Blob、Bun.file(path) 及其它 JS 对象用作 stdin、stdout 和 stderr。
- Shell 脚本:支持运行
.bun.sh 后缀的 shell 脚本文件。
- 自定义解释器:Bun Shell 用 Zig 编写,包含词法分析器、解析器和解释器,是一个小型编程语言。
快速入门
最简单的 shell 命令是 echo。执行时使用 $ 模板字面量标签:
import { $ } from "bun";
await $`echo "Hello World!"`; // Hello World!
默认情况下,shell 命令打印到 stdout。如需关闭输出,可调用 .quiet():
import { $ } from "bun";
await $`echo "Hello World!"`.quiet(); // 无输出
如果想以文本形式访问命令输出,使用 .text():
import { $ } from "bun";
// .text() 会自动调用 .quiet()
const welcome = await $`echo "Hello World!"`.text();
console.log(welcome); // Hello World!\n
默认情况下,await 返回的是 stdout 和 stderr 的 Buffer。
import { $ } from "bun";
const { stdout, stderr } = await $`echo "Hello!"`.quiet();
console.log(stdout); // Buffer(7) [ 72, 101, 108, 108, 111, 33, 10 ]
console.log(stderr); // Buffer(0) []
错误处理
默认情况下,非零退出码会抛出错误。该 ShellError 包含执行命令的信息。
import { $ } from "bun";
try {
const output = await $`something-that-may-fail`.text();
console.log(output);
} catch (err) {
console.log(`失败,退出码 ${err.exitCode}`);
console.log(err.stdout.toString());
console.log(err.stderr.toString());
}
可用 .nothrow() 禁用抛出,返回结果的 exitCode 需手动检查。
import { $ } from "bun";
const { stdout, stderr, exitCode } = await $`something-that-may-fail`.nothrow().quiet();
if (exitCode !== 0) {
console.log(`非零退出码 ${exitCode}`);
}
console.log(stdout);
console.log(stderr);
也可通过在 $ 函数上调用 .nothrow() 或 .throws(boolean) 来配置默认的非零退出码处理行为。
import { $ } from "bun";
// shell promise 不会抛出异常,需要手动检查 `exitCode`
$.nothrow(); // 等价于 $.throws(false)
// 默认行为,非零退出码会抛错
$.throws(true);
// $.nothrow() 的别名
$.throws(false);
await $`something-that-may-fail`; // 不抛异常
重定向
可使用典型 Bash 操作符重定向命令的 输入 和 输出:
< 重定向 stdin
> 或 1> 重定向 stdout
2> 重定向 stderr
&> 同时重定向 stdout 和 stderr
>> 或 1>> 追加重定向 stdout(而非覆盖)
2>> 追加重定向 stderr(而非覆盖)
&>> 追加重定向 stdout 和 stderr(而非覆盖)
1>&2 将 stdout 重定向到 stderr(所有写入 stdout 的内容将进入 stderr)
2>&1 将 stderr 重定向到 stdout(所有写入 stderr 的内容将进入 stdout)
Bun Shell 还支持 JavaScript 对象之间的重定向。
示例:重定向输出到 JavaScript 对象 (>)
使用 > 操作符将 stdout 重定向到 JS 对象:
import { $ } from "bun";
const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\n
以下 JS 对象支持作为重定向目标:
Buffer、Uint8Array、Uint16Array、Uint32Array、Int8Array、Int16Array、Int32Array、Float32Array、Float64Array、ArrayBuffer、SharedArrayBuffer(写入底层缓冲区)
Bun.file(path)、Bun.file(fd)(写入文件)
示例:重定向输入自 JavaScript 对象 (<)
使用 < 操作符将 JS 对象作为 stdin 重定向输入:
import { $ } from "bun";
const response = new Response("hello i am a response body");
const result = await $`cat < ${response}`.text();
console.log(result); // hello i am a response body
以下 JS 对象支持从中重定向:
Buffer、Uint8Array、Uint16Array、Uint32Array、Int8Array、Int16Array、Int32Array、Float32Array、Float64Array、ArrayBuffer、SharedArrayBuffer(从底层缓冲区读)
Bun.file(path)、Bun.file(fd)(从文件读)
Response(从 body 读)
示例:重定向 stdin -> 文件
import { $ } from "bun";
await $`cat < myfile.txt`;
示例:重定向 stdout -> 文件
import { $ } from "bun";
await $`echo bun! > greeting.txt`;
示例:重定向 stderr -> 文件
import { $ } from "bun";
await $`bun run index.ts 2> errors.txt`;
示例:重定向 stderr -> stdout
import { $ } from "bun";
// 将 stderr 重定向到 stdout,所有输出将显示在 stdout
await $`bun run ./index.ts 2>&1`;
示例:重定向 stdout -> stderr
import { $ } from "bun";
// 将 stdout 重定向到 stderr,所有输出将显示在 stderr
await $`bun run ./index.ts 1>&2`;
管道 (|)
和 bash 类似,你可以将一个命令的输出通过管道传给另一个命令:
import { $ } from "bun";
const result = await $`echo "Hello World!" | wc -w`.text();
console.log(result); // 2\n
你也可以用 JavaScript 对象做管道:
import { $ } from "bun";
const response = new Response("hello i am a response body");
const result = await $`cat < ${response} | wc -w`.text();
console.log(result); // 6\n
命令替换 ($(...))
命令替换允许将另一个脚本的输出嵌入当前脚本:
import { $ } from "bun";
// 打印当前提交的哈希
await $`echo Hash of current commit: $(git rev-parse HEAD)`;
这是一种将命令输出作为文本插入的方法,例如声明 shell 变量:
import { $ } from "bun";
await $`
REV=$(git rev-parse HEAD)
docker built -t myapp:$REV
echo Done building docker image "myapp:$REV"
`;
因为 Bun 内部使用了输入模板字面量的特殊 raw 属性,所以使用反引号进行命令替换不起作用:import { $ } from "bun";
await $`echo \`echo hi\``;
不会打印:而是打印:我们建议坚持使用 $(...) 语法。
环境变量
环境变量设置方式类似 bash:
import { $ } from "bun";
await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\n
可以通过字符串插值设置环境变量:
import { $ } from "bun";
const foo = "bar123";
await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\n
输入默认被转义,防止 shell 注入攻击:
import { $ } from "bun";
const foo = "bar123; rm -rf /tmp";
await $`FOO=${foo} bun -e 'console.log(process.env.FOO)'`; // bar123; rm -rf /tmp\n
修改环境变量
默认所有命令使用 process.env 作为环境变量。
可以通过 .env() 为单个命令更改环境变量:
import { $ } from "bun";
await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // bar
通过设置 $.env 可改变所有命令的默认环境变量:
import { $ } from "bun";
$.env({ FOO: "bar" });
// 全局设置的 $FOO
await $`echo $FOO`; // bar
// 局部设置的 $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // baz
调用 $.env() 并传入无参数可以重置环境变量为默认值:
import { $ } from "bun";
$.env({ FOO: "bar" });
// 全局设置的 $FOO
await $`echo $FOO`; // bar
// 局部设置的 $FOO
await $`echo $FOO`.env(undefined); // ""
修改工作目录
可通过传入字符串给 .cwd() 改变单个命令的工作目录:
import { $ } from "bun";
await $`pwd`.cwd("/tmp"); // /tmp
可通过设置 $.cwd 更改所有命令的默认工作目录:
import { $ } from "bun";
$.cwd("/tmp");
// 全局设置的工作目录
await $`pwd`; // /tmp
// 局部设置的工作目录
await $`pwd`.cwd("/"); // /
读取输出
使用 .text() 以字符串形式读取命令输出:
import { $ } from "bun";
const result = await $`echo "Hello World!"`.text();
console.log(result); // Hello World!\n
读取 JSON 格式的输出
使用 .json() 读取命令的 JSON 输出:
import { $ } from "bun";
const result = await $`echo '{"foo": "bar"}'`.json();
console.log(result); // { foo: "bar" }
按行读取输出
使用 .lines() 按行读取命令输出:
import { $ } from "bun";
for await (let line of $`echo "Hello World!"`.lines()) {
console.log(line); // Hello World!
}
也能在命令执行完后用 .lines():
import { $ } from "bun";
const search = "bun";
for await (let line of $`cat list.txt | grep ${search}`.lines()) {
console.log(line);
}
读取输出为 Blob
使用 .blob() 以 Blob 形式读取输出:
import { $ } from "bun";
const result = await $`echo "Hello World!"`.blob();
console.log(result); // Blob(13) { size: 13, type: "text/plain" }
内置命令
为保证跨平台兼容,Bun Shell 实现了一套内置命令,并且可以从 PATH 环境变量读取命令。
cd:切换工作目录
ls:列出目录文件
rm:删除文件和目录
echo:打印文本
pwd:打印当前工作目录
bun:在 bun 中运行 bun
cat
touch
mkdir
which
mv
exit
true
false
yes
seq
dirname
basename
部分实现:
尚未实现但计划中的:
工具函数
Bun Shell 也实现了一些用于处理 shell 的工具函数。
$.braces(花括号扩展)
该函数实现了简单的 花括号扩展:
import { $ } from "bun";
await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]
$.escape(转义字符串)
暴露了 Bun Shell 的字符串转义逻辑:
import { $ } from "bun";
console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"
如果不希望字符串被转义,可将其包裹在 { raw: 'str' } 对象中:
import { $ } from "bun";
await $`echo ${{ raw: '$(foo) `bar` "baz"' }}`;
// => bun: command not found: foo
// => bun: command not found: bar
// => baz
.sh 文件加载器
对于简单的 shell 脚本,可以用 Bun Shell 替代 /bin/sh 来执行。
只需用 bun 运行以 .sh 结尾的文件即可。
echo "Hello World! pwd=$(pwd)"
Hello World! pwd=/home/demo
Bun Shell 脚本跨平台,Windows 也支持:
Hello World! pwd=C:\Users\Demo
实现说明
Bun Shell 是一个用 Zig 实现、在 Bun 内的简易编程语言。包含手写词法解析器、语法解析器及解释器。与 bash、zsh 等 shell 不同,Bun Shell 支持并发运行操作。
Bun shell 的安全性
Bun shell 设计时不调用系统 shell(如 /bin/sh),而是重新实现了 bash,直接在 Bun 进程中运行,注重安全设计。
在解析命令参数时,将所有插值变量视为单一字面字符串。
这能防止 Bun shell 出现 命令注入:
import { $ } from "bun";
const userInput = "my-file.txt; rm -rf /";
// 安全:`userInput` 会被视为单个带引号字符串
await $`ls ${userInput}`;
以上例中,userInput 被作为一个单独字符串传给 ls,导致 ls 查找名为 "my-file; rm -rf /" 的单一目录。
安全注意事项
虽然默认防止命令注入,开发者仍需注意某些场景的安全性。
类似于 Bun.spawn 或 node:child_process.exec(),你可选择用参数在新 shell 中执行命令(例如 bash -c)。
此时,你交出了控制权,Bun 内置的保护机制不再适用,字符串将由新 shell 解释执行。
import { $ } from "bun";
const userInput = "world; touch /tmp/pwned";
// 不安全:显式调用新 shell 进程 `bash -c`。
// 此新 shell 会执行 `touch` 命令。
// 传入的用户输入必须严格过滤。
await $`bash -c "echo ${userInput}"`;
参数注入
Bun shell 无法知道外部命令如何解析自己的命令行参数。攻击者可传入被目标程序识别为参数或选项的输入,导致异常行为。
import { $ } from "bun";
// 恶意输入,格式化为 Git 命令行标志
const branch = "--upload-pack=echo pwned";
// 不安全:Bun 作为单个参数传递字符串
// 但 `git` 程序会识别并执行这个恶意标志。
await $`git ls-remote origin ${branch}`;
建议 — 和所有语言中一样,始终在把用户提供的输入作为外部命令参数传递前进行安全检验。参数验证责任由应用代码承担。
该 API 很大程度上受到 zx、dax 和 bnx 启发,感谢这些项目的作者。