Bun

$ Shell

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

快速入門

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。常見的 shell 指令,例如 lscdrm,都是原生實作的。
  • 熟悉:Bun Shell 是類似 bash 的 shell,支援重新導向、管道、環境變數等。
  • Glob:原生支援 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 指令會列印至標準輸出。若要靜音輸出,請呼叫 .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 會將標準輸出和標準錯誤輸出作為 Buffer 傳回。

import { $ } from "bun";

const { stdout, stderr } = await $`echo "Hello World!"`.quiet();

console.log(stdout); // Buffer(6) [ 72, 101, 108, 108, 111, 32 ]
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(); // equivilent 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 算子重新導向

  • < 重新導向標準輸入
  • >1> 重新導向標準輸出
  • 2> 重新導向標準錯誤輸出
  • &> 重新導向標準輸出和標準錯誤輸出
  • >>1>> 重新導向標準輸出,附加至目的地,而不是覆寫
  • 2>> 重新導向標準錯誤輸出,附加至目的地,而不是覆寫
  • &>> 重新導向標準輸出和標準錯誤輸出,附加至目的地,而不是覆寫
  • 1>&2 將標準輸出重新導向至標準錯誤輸出(所有寫入標準輸出的內容將改為寫入標準錯誤輸出)
  • 2>&1 將標準錯誤輸出重新導向至標準輸出(所有寫入標準錯誤輸出的內容將改為寫入標準輸出)

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

環境變數

環境變數可以像在 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:移動檔案和目錄(缺少跨裝置支援)

尚未實作,但已規劃

  • 請參閱 https://github.com/oven-sh/bun/issues/9716 以取得完整清單。

公用程式

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

為此,只需使用 bun 執行具有 .sh 副檔名的檔案即可。

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 是使用 Zig 在 Bun 中實作的小型程式語言。它包含手寫的詞法分析器、剖析器和直譯器。與 bash、zsh 和其他 shell 不同,Bun Shell 會並行執行作業。

鳴謝

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