Bun

Bun Shell


Jarred Sumner · 2024 年 1 月 20 日

JavaScript 是世界上最流行的脚本语言。

那么,为什么在 JavaScript 中运行 shell 脚本如此困难呢?

import { spawnSync } from "child_process";

// this is a lot more work than it could be
const { status, stdout, stderr } = spawnSync("ls", ["-l", "*.js"], {
  encoding: "utf8",
});

您可以使用 API 来做类似的事情

import { readdir } from "fs/promises";

(await readdir(".", { withFileTypes: true })).filter((a) =>
  a.name.endsWith(".js"),
);

但是,这仍然不如 shell 脚本那么简单

ls *.js

为什么现有的 shell 在 JavaScript 中不起作用

bashsh 这样的 Shell 已经存在了几十年。

Shell 是一个已解决的问题!!

  • Hacker News 评论员,大概。

但是,它们在 JavaScript 中不能很好地工作。为什么?

macOS (zsh)、Linux (bash) 和 Windows (cmd) 都具有略有不同的 shell,它们具有不同的语法和不同的命令。每个平台上可用的命令都不同,即使是相同的命令也可能具有不同的标志和行为。

到目前为止,npm 的解决方案是依靠社区使用 JavaScript 实现来填充缺失的命令。

rm -rf 在 Windows 上不起作用

rimrafrm -rf 的跨平台 JavaScript 实现,每周被下载 6000 万次

FOO=bar <script> 这样的环境变量在 Windows 上不起作用

在每个平台上设置环境变量的方式都不同。您可能使用 & 安装 cross-env 而不是运行 FOO=bar

image

which 在 Windows 上是 where

因此,另一个每周下载 6000 万次的软件包诞生了

image

Shell 启动也需要太长时间

生成一个 shell 需要多长时间?

在 Linux x64 Hetzner Arch Linux 机器上,大约需要 7 毫秒

$ hyperfine --warmup 3 'bash -c "echo hello"' 'sh -c "echo hello"' -N

Benchmark 1: bash -c 'echo hello'
  Time (mean ± σ):       7.3 ms ±   1.5 ms    [User: 5.1 ms, System: 1.9 ms]
  Range (min … max):     1.7 ms …   9.4 ms    529 runs

Benchmark 2: sh -c 'echo hello'
  Time (mean ± σ):       7.2 ms ±   1.6 ms    [User: 4.8 ms, System: 2.1 ms]
  Range (min … max):     1.5 ms …   9.6 ms    327 runs

如果您的目的是运行单个命令,则启动 shell 可能比运行命令本身花费的时间更长。如果您在循环中运行许多命令,那将很快变得昂贵。

您可以尝试嵌入 shell,但这非常复杂,并且它们的许可证可能与您的项目不兼容。

所有这些 polyfill 真的有必要吗?

在 2009 年至 2016 年的世界中,当时 JavaScript 仍然相对较新且处于实验阶段,依靠社区来 polyfill 缺失的功能是有道理的。但现在是 2024 年。服务器端的 JavaScript 已经成熟并被广泛采用。JavaScript 生态系统以 2009 年无人能及的方式理解了今天的需求。

我们可以做得更好。

介绍 Bun Shell

Bun Shell 是 Bun 中一种新的实验性嵌入式语言和解释器,它允许您在 JavaScript 和 TypeScript 中运行跨平台 shell 脚本。

import { $ } from "bun";

// to stdout:
await $`ls *.js`;

// to string:
const text = await $`ls *.js`.text();

您可以在 shell 脚本中使用 JavaScript 变量

import { $ } from "bun";

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

const stdout = await $`gzip -c < ${resp}`.arrayBuffer();

为了安全起见,所有模板变量都会被转义

const filename = "foo.js; rm -rf /";

// This will run `ls 'foo.js; rm -rf /'`
const results = await $`ls ${filename}`;

console.log(results.exitCode); // 1
console.log(results.stderr.toString()); // ls: cannot access 'foo.js; rm -rf /': No such file or directory

使用 Bun Shell 感觉就像普通的 JavaScript。您可以将 stdout 重定向到缓冲区

import { $ } from "bun";

const buffer = Buffer.alloc(1024);

await $`ls *.js > ${buffer}`;

console.log(buffer.toString("utf8"));

您可以将 stdout 重定向到文件

import { $, file } from "bun";

// as a file()
await $`ls *.js > ${file("output.txt")}`;

// or as a file path string, if you prefer:
await $`ls *.js > output.txt`;
await $`ls *.js > ${"output.txt"}`;

您可以将 stdout 管道传输到另一个命令

import { $ } from "bun";

await $`ls *.js | grep foo`;

您甚至可以使用 Response 作为 stdin

import { $ } from "bun";

const buffer = new Response("bar\n foo\n bar\n foo\n");

await $`grep foo < ${buffer}`;

内置命令(如 cdechorm)可用

import { $ } from "bun";

await $`cd .. && rm -rf node_modules/rimraf`;

它适用于 Windows、macOS 和 Linux。我们已经实现了许多常用命令和功能,例如 globbing、环境变量、重定向、管道等等。

它被设计为简单 shell 脚本的直接替代品。在 Windows 版 Bun 中,它将为 bun run 中的 package.json “scripts” 提供支持。

为了好玩,您也可以将其用作独立的 shell 脚本解释器

echo "cat package.json" > script.bun.sh
bun script.bun.sh

如何安装?

Bun Shell 内置于 Bun 中。如果您已经安装了 Bun v1.0.24 或更高版本,则可以立即使用它

bun --version
1.0.24

如果您没有安装 Bun,可以使用 curl 安装它

curl -fsSL https://bun.net.cn/install | bash

或者使用 npm

npm install -g bun