執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

Shell

使用 Bun 的 shell 指令碼 API 從 JavaScript 執行 shell 命令

Bun Shell 讓使用 JavaScript 和 TypeScript 編寫 shell 指令碼變得有趣。它是一個支援 JavaScript 互操作的跨平臺類 bash shell。

快速開始:

index.ts
import { $ } from "bun";

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

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

特性

  • 跨平臺:可在 Windows、Linux 和 macOS 上執行。無需安裝 rimraf 或 cross-env,你可以使用 Bun Shell。它原生實現了 ls、cd 和 rm 等常見 shell 命令。
  • 熟悉易用:Bun Shell 是一個類似 bash 的 shell,支援重定向、管道和環境變數。
  • Glob:Bun Shell 原生支援 glob 模式,包括 **、* 和 {expansion}。
  • 模板字面量:模板字面量可執行 shell 命令,並插入變數和表示式。
  • 安全性:Bun Shell 預設會轉義所有字串,防止 shell 注入攻擊。
  • JavaScript 互操作:使用 Response、ArrayBuffer、Blob、Bun.file(path) 和其他 JavaScript 物件作為 stdin、stdout 和 stderr。
  • Shell 指令碼:Bun Shell 可執行 shell 指令碼(.sh 檔案)。
  • 自定義直譯器:Bun Shell 是一種使用 Rust 編寫的小型程式語言,擁有自己的詞法分析器、解析器和直譯器。

快速入門

從 echo 開始。要執行它,請使用 $ 模板字面量標籤:

JavaScript
import { $ } from "bun";

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

預設情況下,shell 命令列印到 stdout。如需關閉輸出,可呼叫 .quiet():

JavaScript
import { $ } from "bun";

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

要將命令的輸出讀取為文本,請使用 .text():

JavaScript
import { $ } from "bun";

// .text() 會自動呼叫 .quiet()
const welcome = await $`echo "Hello World!"`.text();

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

預設情況下,使用 await 會將 stdout 和 stderr 作為 Buffer 傳回。

JavaScript
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 包含有關已執行命令的資訊。

JavaScript
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。

JavaScript
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)。

JavaScript
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 物件:

JavaScript
import { $ } from "bun";

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

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

你可以將輸出重定向到以下 JavaScript 物件:

  • Buffer、Uint8Array、Uint16Array、Uint32Array、Int8Array、Int16Array、Int32Array、Float32Array、Float64Array、ArrayBuffer、SharedArrayBuffer(寫入底層緩衝區)
  • Bun.file(path)、Bun.file(fd)(寫入檔案)

範例:從 JavaScript 物件重定向輸入 (<)

要將 JavaScript 物件用作 stdin,請使用 < 運算子:

JavaScript
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 物件重定向輸入:

  • Buffer、Uint8Array、Uint16Array、Uint32Array、Int8Array、Int16Array、Int32Array、Float32Array、Float64Array、ArrayBuffer、SharedArrayBuffer(從底層緩衝區讀)
  • Bun.file(path)、Bun.file(fd)(從檔案讀)
  • Response(從 body 讀)

範例:重定向 stdin -> 檔案

JavaScript
import { $ } from "bun";

await $`cat < myfile.txt`;

範例:重定向 stdout -> 檔案

JavaScript
import { $ } from "bun";

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

範例:重定向 stderr -> 檔案

JavaScript
import { $ } from "bun";

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

範例:重定向 stderr -> stdout

JavaScript
import { $ } from "bun";

// 將 stderr 重定向到 stdout,所有輸出將顯示在 stdout
await $`bun run ./index.ts 2>&1`;

範例:重定向 stdout -> stderr

JavaScript
import { $ } from "bun";

// 將 stdout 重定向到 stderr,所有輸出將顯示在 stderr
await $`bun run ./index.ts 1>&2`;

管道 (|)

和 bash 類似,你可以將一個命令的輸出通過管道傳給另一個命令:

JavaScript
import { $ } from "bun";

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

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

你也可以用 JavaScript 物件做管道:

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

命令替換 ($(...))

命令替換會將另一個命令的輸出插入當前指令碼中:

JavaScript
import { $ } from "bun";

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

Bun Shell 會將輸出作為文本插入,因此你可以用它來宣告 shell 變數:

JavaScript
import { $ } from "bun";

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

環境變數

像在 bash 中一樣設定環境變數:

JavaScript
import { $ } from "bun";

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

使用字串插值來設定值:

JavaScript
import { $ } from "bun";

const foo = "bar123";

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

Bun Shell 預設會轉義輸入,防止 shell 注入攻擊:

JavaScript
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():

JavaScript
import { $ } from "bun";

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

要修改所有命令的預設環境變數,請呼叫 $.env:

JavaScript
import { $ } from "bun";

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

// 全域性設定的 $FOO
await $`echo $FOO`; // bar

// 區域性設定的 $FOO
await $`echo $FOO`.env({ FOO: "baz" }); // baz

要將環境變數重置為預設值,請不帶引數呼叫 $.env():

JavaScript
import { $ } from "bun";

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

// 全域性設定的 $FOO
await $`echo $FOO`; // bar

$.env();

// 預設的 $FOO
await $`echo $FOO`; // ""

修改工作目錄

要修改命令的工作目錄,請將字串傳遞給 .cwd():

JavaScript
import { $ } from "bun";

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

要修改所有命令的預設工作目錄,請呼叫 $.cwd:

JavaScript
import { $ } from "bun";

$.cwd("/tmp");

// 全域性設定的工作目錄
await $`pwd`; // /tmp

// 區域性設定的工作目錄
await $`pwd`.cwd("/"); // /

讀取輸出

使用 .text() 以字串形式讀取命令輸出:

JavaScript
import { $ } from "bun";

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

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

讀取 JSON 格式的輸出

使用 .json() 讀取命令的 JSON 輸出:

JavaScript
import { $ } from "bun";

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

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

按行讀取輸出

使用 .lines() 按行讀取命令輸出:

JavaScript
import { $ } from "bun";

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

你也可以在管道命令上使用 .lines():

JavaScript
import { $ } from "bun";

const search = "bun";

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

將輸出讀取為 Blob

使用 .blob() 以 Blob 形式讀取輸出:

JavaScript
import { $ } from "bun";

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

console.log(result); // Blob(13) { size: 13, type: "text/plain" }

內建命令

為了相容跨平臺環境,除了從 PATH 環境變數中讀取命令外,Bun Shell 還實現了一組內建命令。

  • cd:切換工作目錄
  • ls:列出目錄檔案
  • rm:刪除檔案和目錄
  • echo:列印文本
  • pwd:列印當前工作目錄
  • bun:在 bun 中執行 bun
  • cat
  • touch
  • mkdir
  • which
  • mv
  • exit
  • true
  • false
  • yes
  • seq
  • dirname
  • basename

尚未實現,但已列入計劃:


工具函式

Bun Shell 也實現了一些用於處理 shell 的工具函式。

$.braces(花括號擴充套件)

$.braces 為 shell 命令實現了花括號擴充套件:

JavaScript
import { $ } from "bun";

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

$.escape(跳脫字元串)

暴露了 Bun Shell 的字串轉義邏輯:

JavaScript
import { $ } from "bun";

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

若要跳過轉義,請將字串包裝在 { raw: 'str' } 物件中:

JavaScript
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。將副檔名為 .sh 的檔案傳遞給 bun:

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

Bun Shell 指令碼是跨平臺的,因此可以在 Windows 上執行:

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

實現說明

Bun Shell 是一種使用 Rust 實現的小型程式語言,擁有手寫的詞法分析器、解析器和直譯器。與 bash、zsh 及其他 Shell 不同,Bun Shell 會並行執行操作。


Bun Shell 的安全性

根據設計,Bun Shell 不會呼叫系統 shell,例如 /bin/sh。它是 Bash 的重新實現,執行在同一個 Bun 行程中。

解析命令引數時,Bun Shell 會將所有_插值變數_視為單個字面字串。

這可以防止命令注入:

JavaScript
import { $ } from "bun";

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

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

在這裡,Bun Shell 會將 userInput 視為單個字串,因此 ls 會嘗試讀取名為 my-file.txt; rm -rf / 的單個目錄的內容。

安全注意事項

雖然 Bun Shell 預設會防止命令注入,但在某些情況下,你仍需自行負責安全性。

與 Bun.spawn 或 node:child_process.exec() API 類似,你可以有意執行一個會使用引數生成新 shell 的命令(例如 bash -c)。

此時,你交出了控制權,Bun 內建的保護機制不再適用,字串將由新 shell 解釋執行。

JavaScript
import { $ } from "bun";

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

// 不安全:顯式呼叫新 shell 行程 `bash -c`。
// 此新 shell 會執行 `touch` 命令。
// 傳入的使用者輸入必須嚴格過濾。
await $`bash -c "echo ${userInput}"`;

引數注入

Bun Shell 無法知曉外部命令如何解釋其自身的命令列引數。攻擊者可以提供目標程式會識別為自身選項或標誌的輸入,從而導致非預期行為。

JavaScript
import { $ } from "bun";

// 惡意輸入,格式化為 Git 命令列標誌
const branch = "--upload-pack=echo pwned";

// 不安全:Bun 作為單個引數傳遞字串
// 但 `git` 程式會識別並執行這個惡意標誌。
await $`git ls-remote origin ${branch}`;

致謝

該 API 很大程度上受到 zx、dax 和 bnx 啟發,感謝這些專案的作者。