Skip to main content
Bun Shell 让使用 JavaScript & TypeScript 编写 shell 脚本变得有趣。它是一个跨平台的类 bash shell,具备无缝的 JavaScript 互操作性。 快速开始:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79index.ts
import { $ } from "bun";

const response = await fetch("https://example.com");

// 使用 Response 作为 stdin。
await $`cat < ${response} | wc -c`; // 1256

特性

  • 跨平台:支持 Windows、Linux 和 macOS。无需安装额外依赖,也能使用 Bun Shell 替代 rimrafcross-env。诸如 lscdrm 等常见 shell 命令均为原生实现。
  • 熟悉感:Bun Shell 类似 bash,支持重定向、管道、环境变量等多种功能。
  • 通配符:原生支持通配符模式,包括 ***{扩展} 等。
  • 模板字面量:通过模板字面量执行 shell 命令,方便插值变量和表达式。
  • 安全性:默认对所有字符串进行转义,防止 shell 注入攻击。
  • JavaScript 互操作:可将 ResponseArrayBufferBlobBun.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 对象支持作为重定向目标:
  • BufferUint8ArrayUint16ArrayUint32ArrayInt8ArrayInt16ArrayInt32ArrayFloat32ArrayFloat64ArrayArrayBufferSharedArrayBuffer(写入底层缓冲区)
  • 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 对象支持从中重定向:
  • BufferUint8ArrayUint16ArrayUint32ArrayInt8ArrayInt16ArrayInt32ArrayFloat32ArrayFloat64ArrayArrayBufferSharedArrayBuffer(从底层缓冲区读)
  • 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\``;
不会打印:
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 中运行 bun
  • cat
  • touch
  • mkdir
  • which
  • mv
  • exit
  • true
  • false
  • yes
  • seq
  • dirname
  • basename
部分实现:
  • mv:移动文件和目录(尚缺跨设备支持)
尚未实现但计划中的:

工具函数

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 结尾的文件即可。
script.sh
echo "Hello World! pwd=$(pwd)"
terminal
bun ./script.sh
Hello World! pwd=/home/demo
Bun Shell 脚本跨平台,Windows 也支持:
powershell
bun .\script.sh
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.spawnnode: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 很大程度上受到 zxdaxbnx 启发,感谢这些项目的作者。