Bun

$ Shell

Bun Shell makes shell scripting with JavaScript & TypeScript fun. It's a cross-platform bash-like shell with seamless JavaScript interop.

快速开始

import { $ } from "bun";

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

// Use Response as stdin.
await $`cat < ${response} | wc -c`; // 1256

特性:

  • 跨平台: 适用于 Windows、Linux 和 macOS。您可以使用 Bun Shell,无需安装 rimrafcross-env 等额外依赖项。lscdrm 等常用 shell 命令均已原生实现。
  • 熟悉: Bun Shell 是一个类似 bash 的 shell,支持重定向、管道、环境变量等。
  • Globs: 原生支持 Glob 模式,包括 ***{expansion} 等。
  • 模板字面量: 使用模板字面量来执行 shell 命令。这可以轻松地插入变量和表达式。
  • 安全: Bun Shell 默认转义所有字符串,防止 shell 注入攻击。
  • JavaScript 互操作: 使用 ResponseArrayBufferBlobBun.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(); // No output

如果您想将命令的输出访问为文本,请使用 .text()

import { $ } from "bun";

// .text() automatically calls .quiet() for you
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(`Failed with code ${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(`Non-zero exit code ${exitCode}`);
}

console.log(stdout);
console.log(stderr);

可以通过在 $ 函数本身上调用 .nothrow().throws(boolean) 来配置非零退出码的默认处理。

import { $ } from "bun";
// shell promises will not throw, meaning you will have to
// check for `exitCode` manually on every shell command.
$.nothrow(); // equivalent to $.throws(false)

// default behavior, non-zero exit codes will throw an error
$.throws(true);

// alias for $.nothrow()
$.throws(false);

await $`something-that-may-fail`; // No exception thrown

重定向

命令的输入输出可以使用典型的 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 对象重定向。

示例: 将输出重定向到 JavaScript 对象 (>)

要将 stdout 重定向到 JavaScript 对象,请使用 > 运算符。

import { $ } from "bun";

const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;

console.log(buffer.toString()); // Hello World!\n

以下 JavaScript 对象支持重定向到:

  • BufferUint8ArrayUint16ArrayUint32ArrayInt8ArrayInt16ArrayInt32ArrayFloat32ArrayFloat64ArrayArrayBufferSharedArrayBuffer(写入底层缓冲区)
  • 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 对象支持重定向自:

  • BufferUint8ArrayUint16ArrayUint32ArrayInt8ArrayInt16ArrayInt32ArrayFloat32ArrayFloat64ArrayArrayBufferSharedArrayBuffer(从底层缓冲区读取)
  • Bun.file(path)Bun.file(fd)(从文件读取)
  • Response(从主体读取)

示例: 将 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";

// redirects stderr to stdout, so all output
// will be available on stdout
await $`bun run ./index.ts 2>&1`;

示例: 将 stdout 重定向到 stderr

import { $ } from "bun";

// redirects stdout to stderr, so all output
// will be available on 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";

// Prints out the hash of the current commit
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" });

// the globally-set $FOO
await $`echo $FOO`; // bar

// the locally-set $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // baz

您可以通过调用不带参数的 $.env() 将环境变量重置为默认值

import { $ } from "bun";

$.env({ FOO: "bar" });

// the globally-set $FOO
await $`echo $FOO`; // bar

// the locally-set $FOO
await $`echo $FOO`.env(undefined); // ""

更改当前工作目录

您可以通过将字符串传递给 .cwd() 来更改命令的当前工作目录

import { $ } from "bun";

await $`pwd`.cwd("/tmp"); // /tmp

您可以通过调用 $.cwd 来更改所有命令的默认当前工作目录

import { $ } from "bun";

$.cwd("/tmp");

// the globally-set working directory
await $`pwd`; // /tmp

// the locally-set working directory
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(花括号扩展)

此函数为 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

为此,只需在具有 .sh 扩展名的文件上使用 bun 运行脚本即可。

script.sh
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 是 Bun 中的一种小型编程语言,用 Zig 实现。它包括一个手写的词法分析器、解析器和解释器。与 bash、zsh 和其他 shell 不同,Bun Shell 并发运行操作。

Bun Shell 中的安全性

根据设计,Bun Shell **不调用系统 shell**(如 /bin/sh),而是 在同一个 Bun 进程中重新实现 bash, 并以安全为设计理念。

在解析命令参数时,它将所有*插值变量*视为单个、字面字符串。

这可以保护 Bun Shell 免受**命令注入**

import { $ } from "bun";

const userInput = "my-file.txt; rm -rf /";

// SAFE: `userInput` is treated as a single quoted string
await $`ls ${userInput}`;

在上面的示例中,userInput 被视为单个字符串。这会导致 ls 命令尝试读取名为 "my-file; rm -rf /" 的单个目录的内容。

安全注意事项

虽然默认情况下会阻止命令注入,但在某些情况下,开发人员仍 需负责安全性。

Bun.spawnnode:child_process.exec() API 类似,您可以故意 执行一个会启动新 shell(例如 bash -c)并带参数的命令。

执行此操作时,您将控制权移交给该新 shell,Bun 内置的保护 将不再适用于该新 shell 解释的字符串。

import { $ } from "bun";

const userInput = "world; touch /tmp/pwned";

// UNSAFE: You have explicitly started a new shell process with `bash -c`.
// This new shell will execute the `touch` command. Any user input
// passed this way must be rigorously sanitized.
await $`bash -c "echo ${userInput}"`;

参数注入

Bun Shell 无法知道外部命令如何解释其自身的 命令行参数。攻击者可以提供目标程序 将其识别为其自己的选项或标志的输入,从而导致意外行为。

import { $ } from "bun";

// Malicious input formatted as a Git command-line flag
const branch = "--upload-pack=echo pwned";

// UNSAFE: While Bun safely passes the string as a single argument,
// the `git` program itself sees and acts upon the malicious flag.
await $`git ls-remote origin ${branch}`;

建议 — 与任何语言中的最佳实践一样,始终对 用户提供的输入进行消毒,然后再将其作为参数传递给外部命令。 验证参数的责任在于您的应用程序代码。

致谢

此 API 的大部分灵感来自 zxdaxbnx。感谢这些项目的作者。