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 命令會列印到 stdout。要靜音輸出,請呼叫 .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 形式傳回 stdout 和 stderr。

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

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(); // equivalent 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 運算符進行重新導向

  • < 重新導向 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 物件,請使用 > 運算符

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

範例:重新導向 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

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

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

import { $ } from "bun";

// Prints out the hash of the current commit
await $`echo Hash of current commit: $(git rev-parse HEAD)`;

這是命令輸出的文字插入,可用於宣告 shell 變數等。

import { $ } from "bun";

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

注意:由於 Bun 內部在輸入模板字串上使用了特殊的 raw 屬性,因此使用反引號語法進行命令替換將無法運作

import { $ } from "bun";

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

而不是印出

hi

上述程式碼將印出

echo hi

我們建議改為堅持使用 $(...) 語法。

環境變數

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

尚未實作,但已計畫

工具程式

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

若要這樣做,只需在具有 .sh 副檔名的檔案上使用 bun 執行腳本即可。

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

致謝

此 API 的很大一部分靈感來自 zxdaxbnx。感謝這些專案的作者。