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 實作 polyfill 遺失的命令。

rm -rf 在 Windows 上無法運作

rimrafrm -rf 的跨平台 JavaScript 實作,每週下載次數達 6 千萬次

FOO=bar <script> 這樣的環境變數在 Windows 上無法運作

在每個平台上設定環境變數的方式都不同。您可能使用並安裝 cross-env,而不是執行 FOO=bar

image

which 在 Windows 上是 where

因此,另一個每週下載次數達 6 千萬次的套件就此誕生

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.dev.org.tw/install | bash

或使用 npm

npm install -g bun