見出し画像

Bun shellを使って快適にスクリプトを書こう(1) 基本的な使い方

こんにちは、はしるとりです。
ナビタイムジャパンでSREを担当しています。

アプリケーションのCIやクラウドへのデプロイの処理を書く場合、シェルスクリプトで実装することが多いと思います。
手軽に扱えて便利な反面、エディタの補完が弱かったり型がなかったりと、間違えずに実装するのは結構たいへんです。
個人的には100行を超えてくるとメンテナンスが辛いと感じます。

この記事では、Bun shellの紹介と、AWSのリソースを操作する処理をBun shell + TypeScriptで書く手法について紹介します。
2回にわけて、今回は基本的な利用方法を説明し、次回はAWS CLIを用いた実用例を紹介するという構成でお届けします。

環境の都合でbashが適している場合や、すでにシェルスクリプトで運用されていて移植が難しいという場合にはこちらの記事を参考に保守性を高めていきましょう ->
大量なシェルスクリプトの開発・運用で悩んでいるあなたへ贈る話


環境

  • Bun 1.1.8

  • macOS Sonoma 14.5

Bun shellとは

2024/01/20に発表された機能で、 Bun 1.0.24 から使えるようになっています。

クロスプラットフォームに対応していて、macOS, Linux, WindowsのOSに依存せず、どこでも同じように動作します。
npm scriptsで環境ごとのコマンドの差分を吸収するために `rm -rf` を `rimraf` に、環境変数の設定を `cross-env` に置き換えて使っていたと思いますが、その必要もありません。
環境変数は `FOO=bar` で宣言できますし、`ls` `cd` `rm` といったよく使うコマンドはBun shellのbuiltinに実装されています(詳細は https://bun.sh/docs/runtime/shell#builtin-commands)

JavaScriptでシェルスクリプトを書けるといえば google/zx はご存知の方も多いかもしれません。
Bun shellもzxのように `$` とタグ付きテンプレートリテラルを使ってコマンドをラップします。

また、TypeScriptで記述するとzxの場合はts-nodeなどを挟んでトランスパイルする必要がありましたが、Bunはネイティブで実行できます。

基本的な使い方

Bunのインストールについては公式ドキュメントが詳しいので割愛します。

`bun init` でプロジェクトを作成します。

❯ mkdir bun-shell
❯ cd bun-shell
❯ bun init

`index.ts` を編集します。

import { $ } from "bun";

await $`echo "Hello Bun shell!"`;

これを実行すると標準出力に結果が出力されます。

bun run index.ts
Hello Bun shell!

出力結果をさまざまな形式で変数に格納する

コマンドの出力をそのまま文字列として変数に格納するには `.text()` をつけます。

import { $ } from "bun";

const out = await $`echo "Hello Bun shell!"`.text();

console.log(out);
// => Hello Bun shell!

出力結果を改行で区切って配列にするには、前述の `text()` の結果を `split` するほか、`lines()` メソッドを使うこともできます。

import { $ } from "bun";

for await (const file of $`ls *`.lines()) {
  console.log(`file: ${file}`);
}
// =>
// file: index.ts
// file: package.json

他にもJSONやBlobで受け取ることもできます。
詳しくはこちら

エラーハンドリング

デフォルトでは0以外のexit codeで終了するとerrorがthrowされます。
個人的にシェルスクリプトのエラーハンドリングを書くのが苦手なので、これは非常に助かります。

import { $ } from "bun";

try {
  await $`git notfound`.text();
} catch (err) {
  console.log(`Failed with code ${err.exitCode}`);
  console.log(`stdout: ${err.stdout.toString()}`);
  console.log(`stderr: ${err.stderr.toString()}`);
}
❯ bun run index.ts
Failed with code 1
stdout:
stderr: git: 'notfound' is not a git command. See 'git --help'.

`.nothrow()` をつけるとthrowされるのを抑制できて、代わりに `exitCode` でチェックすることができます。

import { $ } from "bun";

const { stdout, stderr, exitCode } = await $`git notfound`.nothrow().quiet();

if (exitCode !== 0) {
  console.log(`Non-zero exit code ${exitCode}`);
}

console.log(`stdout: ${stdout.toString()}`);
console.log(`stderr: ${stderr.toString()}`);
❯ bun run index.ts
Non-zero exit code 1
stdout:
stderr: git: 'notfound' is not a git command. See 'git --help'.

引数を取得する

`node:utils.parseArgs` と同等の関数が利用できます。
https://bun.sh/guides/process/argv

import { parseArgs } from "util";

const { values, positionals } = parseArgs({
  args: Bun.argv.slice(2),
  options: {
    bool: {
      type: "boolean",
      short: "b",
      default: false,
    },
    str: {
      type: "string",
      short: "s",
    },
  },
  strict: true,
  allowPositionals: true,
});

console.log(
  `bool=${values.bool}, str=${values.str}, args=${positionals}`,
);
❯ bun run args.ts
bool=false, str=undefined, args=

❯ bun run args.ts --bool -s "chiaotzu" hello world
bool=true, str=chiaotzu, args=hello,world

これをシェルスクリプトで書くと次のようになります。

bool=false
str=""

while getopts "bs:" opt; do
    case ${opt} in
        b )
            bool=true
            ;;
        s )
            s=$OPTARG
            ;;
    esac
done
shift $((OPTIND -1))

echo "bool=${bool}, str=${str}, args=${@}"
❯ sh args.sh -b -s "chiaotzu" hello world
bool=true, str=chiaotzu, args=hello world

ただしシェルにbuilt-inの `getopts` 、BSD版の `getopt` 、 GNU版の `getopt` の違いを理解することが必要となります。
GNU版の `getopt` ではロングオプションなどの高機能を提供しますが、どこでも同じように動作することを考慮すると `getopts` を使う方が安全です。

参考: シェルスクリプト オプション解析 徹底解説 (getopt / getopts) - Qiita

JSONやYAMLファイルをロードする

https://bun.sh/guides/read-file/json

`Bun.file()` でファイルを読み込むと `BunFile` インスタンスが生成されます。
`BunFile` には `.json()` メソッドがあり、これによって簡単にJSONをパースできます。

// params.json
// {"name": "my-service", "duration": 30}
const file = Bun.file('./params.json');
const param = await file.json();

console.log(param.name, param.duration)
// => my-service 30

YAMLについては自分で処理を書く必要はありますが、js-yaml などを使うと楽に書けます。

bun add js-yamlbun add -D @types/js-yaml
import { load } from "js-yaml";

// params.yaml
// name: my-service
// duration: 30
const file = Bun.file("./params.yaml");
const content = await file.text();
const param = load(content);

console.log(param.name, param.duration)
// => my-service 30

シェルスクリプトでは `jq` や `yq` を使うと思いますが、階層が深かったり配列を扱おうとしたりすると複雑化するので、JavaScriptの文法でアクセスできるのは非常に便利です。

おわりに

今回はBun shellの紹介と使い方を説明しました。
スクリプトを作成する上で便利な機能が提供されていることが伝わったでしょうか。
次回はAWS CLI/SDKを組み合わせた利用例を紹介したいと思います。

参考