Bun Shell 让使用 JavaScript 和 TypeScript 进行 shell 脚本编写变得有趣。它是一个跨平台的类 bash shell,具有无缝的 JavaScript 互操作性。
快速入门:
import { $ } from "bun";
const response = await fetch("https://example.com");
// 将 Response 用作 stdin。
await $`cat < ${response} | wc -c`; // 1256特性
- 跨平台:适用于 Windows、Linux 和 macOS。无需安装
rimraf或cross-env等额外依赖,你可以直接使用 Bun Shell。常见的 shell 命令如ls、cd、rm都是原生实现的。 - 熟悉:Bun Shell 是类 bash shell,支持重定向、管道、环境变量等。
- 通配符:原生支持通配符模式,包括
**、*、{expansion}等。 - 模板字面量:使用模板字面量执行 shell 命令。这使得变量和表达式的插值变得容易。
- 安全:Bun Shell 默认转义所有字符串,防止 shell 注入攻击。
- JavaScript 互操作:将
Response、ArrayBuffer、Blob、Bun.file(path)和其他 JavaScript 对象用作 stdin、stdout 和 stderr。 - Shell 脚本:Bun Shell 可用于运行 shell 脚本(
.bun.sh文件)。 - 自定义解释器:Bun Shell 用 Zig 编写,包括其词法分析器、解析器和解释器。Bun Shell 是一种小型编程语言。
入门
最简单的 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 不会抛出,意味着你必须
// 在每个 shell 命令上手动检查 `exitCode`。
$.nothrow(); // 等同于 $.throws(false)
// 默认行为,非零退出码将抛出错误
$.throws(true);
// $.nothrow() 的别名
$.throws(false);
await $`something-that-may-fail`; // 不抛出异常重定向
命令的_输入_或_输出_可以使用典型的 Bash 运算符进行_重定向_:
<重定向 stdin>或1>重定向 stdout2>重定向 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 对象重定向。
示例:将输出重定向到 JavaScript 对象(>)
要将 stdout 重定向到 JavaScript 对象,使用 > 运算符:
import { $ } from "bun";
const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\n支持以下 JavaScript 对象用于重定向到:
Buffer、Uint8Array、Uint16Array、Uint32Array、Int8Array、Int16Array、Int32Array、Float32Array、Float64Array、ArrayBuffer、SharedArrayBuffer(写入底层缓冲区)Bun.file(path)、Bun.file(fd)(写入文件)
示例:从 JavaScript 对象重定向输入(<)
要将 JavaScript 对象的输出重定向到 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支持以下 JavaScript 对象用于重定向从:
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"
`;NOTE
因为 Bun 在输入模板字面量上内部使用特殊的 raw 属性,使用反引号语法进行命令替换将不起作用:
import { $ } from "bun";
await $`echo \`echo hi\``;上面的代码不会打印:
hi而是会打印:
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 中运行 buncattouchmkdirwhichmvexittruefalseyesseqdirnamebasename
部分实现:
mv:移动文件和目录(缺少跨设备支持)
尚未实现但计划中的:
- 查看 Issue #9716 获取完整列表。
工具
Bun Shell 还实现了一组用于处理 shell 的工具。
$.braces(大括号展开)
此函数为 shell 命令实现简单的 大括号展开:
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 运行 shell 脚本,而不是 /bin/sh。
为此,只需使用 bun 运行带有 .sh 扩展名的文件。
echo "Hello World! pwd=$(pwd)"bun ./script.shHello World! pwd=/home/demo使用 Bun Shell 的脚本是跨平台的,这意味着它们在 Windows 上也能工作:
bun .\script.shHello World! pwd=C:\Users\Demo实现说明
Bun Shell 是 Bun 中的一种小型编程语言,用 Zig 实现。它包括手写的词法分析器、解析器和解释器。与 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 命令尝试读取名为 "my-file; rm -rf /" 的单个目录的内容。
安全注意事项
虽然默认情况下防止命令注入,但开发者在某些场景下仍需对安全负责。
类似于 Bun.spawn 或 node:child_process.exec() API,你可以有意地执行生成新 shell 的命令(例如 bash -c)并带参数。
当你这样做时,你交出了控制权,Bun 的内置保护不再适用于由该新 shell 解释的字符串。
import { $ } from "bun";
const userInput = "world; touch /tmp/pwned";
// 不安全:你已明确启动了一个带有 `bash -c` 的新 shell 进程。
// 这个新 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}`;NOTE
**建议** — 作为每种语言的最佳实践,在将用户提供的输入作为参数传递给外部命令之前,始终对其进行清理。验证参数的责任在于你的应用程序代码。