Shell
使用 Bun 的 shell 脚本 API 从 JavaScript 运行 shell 命令
Bun Shell 让使用 JavaScript 和 TypeScript 编写 shell 脚本变得有趣。它是一个支持 JavaScript 互操作的跨平台类 bash shell。
快速开始:
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。它原生实现了ls、cd和rm等常见 shell 命令。 - 熟悉易用:Bun Shell 是一个类似 bash 的 shell,支持重定向、管道和环境变量。
- Glob:Bun Shell 原生支持 glob 模式,包括
**、*和{expansion}。 - 模板字面量:模板字面量可执行 shell 命令,并插入变量和表达式。
- 安全性:Bun Shell 默认会转义所有字符串,防止 shell 注入攻击。
- JavaScript 互操作:使用
Response、ArrayBuffer、Blob、Bun.file(path)和其他 JavaScript 对象作为 stdin、stdout 和 stderr。 - Shell 脚本:Bun Shell 可运行 shell 脚本(
.sh文件)。 - 自定义解释器:Bun Shell 是一种使用 Rust 编写的小型编程语言,拥有自己的词法分析器、解析器和解释器。
快速入门
从 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>重定向 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 对象 (>)
使用 > 操作符将 stdout 重定向到 JS 对象:
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)`;
Bun Shell 会将输出作为文本插入,因此你可以用它来声明 shell 变量:
import { $ } from "bun";
await $`
REV=$(git rev-parse HEAD)
docker build -t myapp:$REV .
echo Done building docker image "myapp:$REV"
`;
环境变量
像在 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
Bun Shell 默认会转义输入,防止 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
$.env();
// 默认的 $FOO
await $`echo $FOO`; // ""
修改工作目录
要修改命令的工作目录,请将字符串传递给 .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" }
内置命令
为了兼容跨平台环境,除了从 PATH 环境变量中读取命令外,Bun Shell 还实现了一组内置命令。
cd:切换工作目录ls:列出目录文件rm:删除文件和目录echo:打印文本pwd:打印当前工作目录bun:在 bun 中运行 buncattouchmkdirwhichmvexittruefalseyesseqdirnamebasename
尚未实现,但已列入计划:
- 详见 Issue #9716。
工具函数
Bun Shell 也实现了一些用于处理 shell 的工具函数。
$.braces(花括号扩展)
$.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 来代替 /bin/sh。将扩展名为 .sh 的文件传递给 bun:
echo "Hello World! pwd=$(pwd)"
bun ./script.sh
Hello World! pwd=/home/demo
Bun Shell 脚本是跨平台的,因此可以在 Windows 上运行:
bun .\script.sh
Hello World! pwd=C:\Users\Demo
实现说明
Bun Shell 是一种使用 Rust 实现的小型编程语言,拥有手写的词法分析器、解析器和解释器。与 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}`;
在这里,Bun Shell 会将 userInput 视为单个字符串,因此 ls 会尝试读取名为 my-file.txt; rm -rf / 的单个目录的内容。
安全注意事项
虽然 Bun Shell 默认会防止命令注入,但在某些情况下,你仍需自行负责安全性。
与 Bun.spawn 或 node:child_process.exec() API 类似,你可以有意执行一个会使用参数生成新 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}`;