Skip to content

Bun Shell 讓使用 JavaScript 和 TypeScript 進行 shell 腳本編寫變得有趣。它是一個跨平台的類 bash shell,具有無縫的 JavaScript 互操作性。

快速入門:

ts
import { $ } from "bun";

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

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

特性

  • 跨平台:適用於 Windows、Linux 和 macOS。無需安裝 rimrafcross-env 等額外依賴,你可以直接使用 Bun Shell。常見的 shell 命令如 lscdrm 都是原生實現的。
  • 熟悉:Bun Shell 是類 bash shell,支持重定向、管道、環境變量等。
  • 通配符:原生支持通配符模式,包括 ***{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。要運行它,使用 $ 模板字面量標簽:

js
import { $ } from "bun";

await $`echo "Hello World!"`; // Hello World!

默認情況下,shell 命令輸出到 stdout。要靜默輸出,調用 .quiet()

js
import { $ } from "bun";

await $`echo "Hello World!"`.quiet(); // 無輸出

如果你想將命令輸出作為文本訪問怎麼辦?使用 .text()

js
import { $ } from "bun";

// .text() 自動為你調用 .quiet()
const welcome = await $`echo "Hello World!"`.text();

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

默認情況下,await 將返回 stdout 和 stderr 作為 Buffer

js
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 包含有關運行命令的信息。

js
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

js
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) 來配置非零退出碼的默認處理。

js
import { $ } from "bun";
// shell promise 不會拋出,意味著你必須
// 在每個 shell 命令上手動檢查 `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 對象重定向。

示例:將輸出重定向到 JavaScript 對象(>

要將 stdout 重定向到 JavaScript 對象,使用 > 運算符:

js
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,使用 < 運算符:

js
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(從 body 讀取)

示例:重定向 stdin -> 文件

js
import { $ } from "bun";

await $`cat < myfile.txt`;

示例:重定向 stdout -> 文件

js
import { $ } from "bun";

await $`echo bun! > greeting.txt`;

示例:重定向 stderr -> 文件

js
import { $ } from "bun";

await $`bun run index.ts 2> errors.txt`;

示例:重定向 stderr -> stdout

js
import { $ } from "bun";

// 將 stderr 重定向到 stdout,因此所有輸出
// 都將在 stdout 上可用
await $`bun run ./index.ts 2>&1`;

示例:重定向 stdout -> stderr

js
import { $ } from "bun";

// 將 stdout 重定向到 stderr,因此所有輸出
// 都將在 stderr 上可用
await $`bun run ./index.ts 1>&2`;

管道(|

像在 bash 中一樣,你可以將一個命令的輸出管道到另一個命令:

js
import { $ } from "bun";

const result = await $`echo "Hello World!" | wc -w`.text();

console.log(result); // 2\n

你也可以使用 JavaScript 對象進行管道:

js
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

命令替換($(...)

命令替換允許你將另一個腳本的輸出替換到當前腳本中:

js
import { $ } from "bun";

// 打印當前提交的哈希
await $`echo Hash of current commit: $(git rev-parse HEAD)`;

這是命令輸出的文本插入,可用於例如聲明 shell 變量:

js
import { $ } from "bun";

await $`
  REV=$(git rev-parse HEAD)
  docker built -t myapp:$REV
  echo Done building docker image "myapp:$REV"
`;

NOTE

因為 Bun 在輸入模板字面量上內部使用特殊的 raw 屬性,使用反引號語法進行命令替換將不起作用:

ts
import { $ } from "bun";

await $`echo \`echo hi\``;

上面的代碼不會打印:

hi

而是會打印:

echo hi

因此,我們建議堅持使用 $(...) 語法。


環境變量

可以像在 bash 中一樣設置環境變量:

js
import { $ } from "bun";

await $`FOO=foo bun -e 'console.log(process.env.FOO)'`; // foo\n

你可以使用字符串插值來設置環境變量:

js
import { $ } from "bun";

const foo = "bar123";

await $`FOO=${foo + "456"} bun -e 'console.log(process.env.FOO)'`; // bar123456\n

輸入默認被轉義,防止 shell 注入攻擊:

js
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() 來更改單個命令的環境變量:

js
import { $ } from "bun";

await $`echo $FOO`.env({ ...process.env, FOO: "bar" }); // bar

你可以通過調用 $.env 來更改所有命令的默認環境變量:

js
import { $ } from "bun";

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

// 全局設置的 $FOO
await $`echo $FOO`; // bar

// 本地設置的 $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // baz

你可以通過不帶參數調用 $.env() 來將環境變量重置為默認值:

js
import { $ } from "bun";

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

// 全局設置的 $FOO
await $`echo $FOO`; // bar

// 本地設置的 $FOO
await $`echo $FOO`.env(undefined); // ""

更改工作目錄

你可以通過傳遞字符串給 .cwd() 來更改命令的工作目錄:

js
import { $ } from "bun";

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

你可以通過調用 $.cwd 來更改所有命令的默認工作目錄:

js
import { $ } from "bun";

$.cwd("/tmp");

// 全局設置的工作目錄
await $`pwd`; // /tmp

// 本地設置的工作目錄
await $`pwd`.cwd("/"); // /

讀取輸出

要將命令輸出作為字符串讀取,使用 .text()

js
import { $ } from "bun";

const result = await $`echo "Hello World!"`.text();

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

將輸出讀取為 JSON

要將命令輸出作為 JSON 讀取,使用 .json()

js
import { $ } from "bun";

const result = await $`echo '{"foo": "bar"}'`.json();

console.log(result); // { foo: "bar" }

逐行讀取輸出

要逐行讀取命令輸出,使用 .lines()

js
import { $ } from "bun";

for await (let line of $`echo "Hello World!"`.lines()) {
  console.log(line); // Hello World!
}

你也可以在已完成的命令上使用 .lines()

js
import { $ } from "bun";

const search = "bun";

for await (let line of $`cat list.txt | grep ${search}`.lines()) {
  console.log(line);
}

將輸出讀取為 Blob

要將命令輸出作為 Blob 讀取,使用 .blob()

js
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 命令實現簡單的 大括號展開

js
import { $ } from "bun";

await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]

$.escape(轉義字符串)

將 Bun Shell 的轉義邏輯作為函數公開:

js
import { $ } from "bun";

console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"

如果你不希望字符串被轉義,將其包裝在 { raw: 'str' } 對象中:

js
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

為此,只需使用 bun 運行帶有 .sh 擴展名的文件。

sh
echo "Hello World! pwd=$(pwd)"
sh
bun ./script.sh
txt
Hello World! pwd=/home/demo

使用 Bun Shell 的腳本是跨平台的,這意味著它們在 Windows 上也能工作:

powershell
bun .\script.sh
txt
Hello World! pwd=C:\Users\Demo

實現說明

Bun Shell 是 Bun 中的一種小型編程語言,用 Zig 實現。它包括手寫的詞法分析器、解析器和解釋器。與 bash、zsh 和其他 shell 不同,Bun Shell 並發運行操作。


Bun Shell 中的安全性

根據設計,Bun shell 不調用系統 shell(如 /bin/sh),而是作為 bash 的重新實現在同一個 Bun 進程中運行,專為安全性而設計。

解析命令參數時,它將所有_插值變量_視為單個字面量字符串。

這保護 Bun shell 免受命令注入

js
import { $ } from "bun";

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

// 安全:`userInput` 被視為單個帶引號的字符串
await $`ls ${userInput}`;

在上面的示例中,userInput 被視為單個字符串。這導致 ls 命令嘗試讀取名為 "my-file; rm -rf /" 的單個目錄的內容。

安全注意事項

雖然默認情況下防止命令注入,但開發者在某些場景下仍需對安全負責。

類似於 Bun.spawnnode:child_process.exec() API,你可以有意地執行生成新 shell 的命令(例如 bash -c)並帶參數。

當你這樣做時,你交出了控制權,Bun 的內置保護不再適用於由該新 shell 解釋的字符串。

js
import { $ } from "bun";

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

// 不安全:你已明確啟動了一個帶有 `bash -c` 的新 shell 進程。
// 這個新 shell 將執行 `touch` 命令。任何以這種方式傳遞的用戶輸入
// 都必須經過嚴格清理。
await $`bash -c "echo ${userInput}"`;

參數注入

Bun shell 無法知道外部命令如何解釋其自己的命令行參數。攻擊者可以提供目標程序識別為其自身選項或標志的輸入,導致意外行為。

js
import { $ } from "bun";

// 格式化為 Git 命令行標志的惡意輸入
const branch = "--upload-pack=echo pwned";

// 不安全:雖然 Bun 安全地將字符串作為單個參數傳遞,
// 但 `git` 程序本身會看到並執行惡意標志。
await $`git ls-remote origin ${branch}`;

NOTE

**建議** — 作為每種語言的最佳實踐,在將用戶提供的輸入作為參數傳遞給外部命令之前,始終對其進行清理。驗證參數的責任在於你的應用程序代碼。

致謝

此 API 的大部分內容受到 zxdaxbnx 的啟發。感謝這些項目的作者。

Bun學習網由www.bunjs.com.cn整理維護