はじめに
process.argvをパースするコードを自分で書いたことがある方なら、あの面倒さを覚えているはずです。オプションの短縮形、デフォルト値、必須チェック、ヘルプメッセージの自動生成……。CLIツールが育つほど、引数まわりのコードは肥大化し、いつの間にか本来のロジックより長くなっていることも珍しくありません。
そんな悩みに応えるのが、モダンなJavaScript CLIライブラリ「Gunshi」です。名前の由来は古代日本の「軍師」——戦略を立て、戦を指揮する役職から取られています。Gunshiはコマンドの定義から実行までを一手に引き受け、開発者は「何を実行するか」だけに集中できるようにしてくれます。
Gunshiとは
Gunshiは、作者kazupon氏(Vue I18nの開発者としても知られています)が手がける、宣言的でモダンなCLI構築ライブラリです。Node.js・Deno・Bunといった複数のJavaScriptランタイムに対応した統一APIを提供し、TypeScriptによる型安全な引数定義が可能です。
主な特徴
- 宣言的で型安全 - コマンドを
name・args・runを持つオブジェクトとして定義し、args-tokensベースの型安全な引数パースを実現 - マルチランタイム対応 - Node.js、Deno、Bunで同じAPIが使える
- コンポーザブルな設計 - サブコマンドの登録や遅延読み込み(lazy loading)に対応し、大規模なCLIでも起動時間を抑えられる
- プラグインシステム - グローバルオプションの追加やコンテキストの拡張、コマンド実行への割り込みができる仕組みを標準搭載
- 国際化対応 - ヘルプメッセージやエラーメッセージのロケール対応を組み込みで用意
インストール
# npm
npm install gunshi
# pnpm
pnpm add gunshi
# yarn
yarn add gunshi
# bun
bun add gunshi
# deno
deno add jsr:@gunshi/gunshi
Gunshiはenginesでnode >= 22を要求するため、Node.js環境で使う場合はバージョンに注意してください。
基本的な使い方
Gunshiの中心的なAPIはcli関数です。コマンドをname・description・args・runを持つオブジェクトとして定義し、cliに渡すだけで、引数パース・ヘルプ表示・バリデーションまで一括で処理してくれます。
import { cli } from 'gunshi'
const command = {
name: 'greet',
description: 'A greeting command',
args: {
name: {
type: 'string',
short: 'n',
description: 'Name to greet'
},
greeting: {
type: 'string',
short: 'g',
default: 'Hello',
description: 'Greeting to use'
},
times: {
type: 'number',
short: 't',
default: 1,
description: 'Number of repetitions'
}
},
run: ctx => {
const { name = 'World', greeting, times } = ctx.values
for (let i = 0; i < times; i++) {
console.log(`${greeting}, ${name}!`)
}
}
}
await cli(process.argv.slice(2), command, {
name: 'my-app',
version: '1.0.0',
description: 'My CLI application'
})
argsに定義したオプションは、実行時にctx.valuesとしてrun関数へ渡されます。typeで型を指定し、shortで短縮オプション(-nなど)、defaultでデフォルト値を設定できる点が、素のprocess.argvパースと比べて圧倒的に見通しをよくしてくれます。
このスクリプトをnode app.js --name Taro --times 3のように実行すれば、Hello, Taro!が3回出力されます。ヘルプ表示(--help)やバージョン表示(--version)もGunshiが自動的に用意してくれるため、追加の実装は不要です。
実践的なユースケース
型安全なコマンド定義(define)
単一のCLIならオブジェクトを直接cliに渡すだけで十分ですが、複数のサブコマンドを持つCLIでは、各コマンドを個別に定義してから組み合わせる設計が現実的です。Gunshiのdefine関数を使うと、TypeScriptの型推論を効かせながらコマンドを定義できます。
import { define } from 'gunshi'
const createCommand = define({
name: 'create',
description: 'Create a new resource',
args: {
name: { type: 'string', short: 'n', required: true }
},
run: ctx => {
console.log(`Creating resource: ${ctx.values.name}`)
}
})
defineで定義したコマンドはctx.valuesの型がargsの定義から自動的に推論されるため、required: trueを付けたオプションを参照する際も型エラーで気付けます。
サブコマンドの構成
複数の機能をまとめたCLI(gitやnpmのようにcreate・list・deleteといったサブコマンドを持つツール)を作る場合、Gunshiはcli関数のsubCommandsオプションでコマンドを束ねます。
await cli(process.argv.slice(2), mainCommand, {
name: 'my-app',
version: '1.0.0',
subCommands: {
create: createCommand,
list: listCommand
}
})
my-app create --name fooのように呼び出せば、createキーに対応するcreateCommandのrunが実行される仕組みです。サブコマンドが増えても、それぞれ独立したファイルで管理できるため、CLIが肥大化しても見通しを保てます。
プラグインによる機能拡張
Gunshiのプラグインシステムを使うと、グローバルオプションの追加やコンテキストの拡張、コマンド実行前後へのフックといった横断的な機能を、コマンド定義とは切り離して実装できます。
import { plugin } from 'gunshi/plugin'
export default plugin({
id: 'logger',
setup: ctx => {
ctx.addGlobalOption('debug', {
type: 'boolean',
description: 'デバッグモードを有効化'
})
},
extension: () => ({
logger: {
info: (msg) => console.log(`[INFO] ${msg}`)
}
})
})
作成したプラグインは、cliのpluginsオプションに渡すだけですべてのサブコマンドに適用されます。
await cli(process.argv.slice(2), mainCommand, {
name: 'my-app',
plugins: [loggerPlugin()]
})
--debugのようなグローバルオプションを個々のコマンドに書き足す必要がなくなり、ログ出力やデータベース接続のような共通機能を一箇所にまとめられます。プラグイン同士の依存関係も自動的に解決されるため、機能を積み上げていくほどGunshiの恩恵を感じられる設計です。
まとめ
Gunshiは、引数パースというCLI開発の地味だけれど避けて通れない部分を、宣言的な定義に置き換えてくれるライブラリです。defineによる型安全なコマンド定義、subCommandsによるサブコマンドの構成、そしてプラグインシステムによる横断的な機能拡張と、小さなスクリプトから本格的なCLIツールまで一貫した書き方で育てていけます。
まずは単一コマンドのcli呼び出しから試してみて、必要に応じてサブコマンドやプラグインへと拡張していくのがおすすめです。「軍師」の名にふさわしく、CLIの設計判断をGunshiに任せてみてはいかがでしょうか。
