commander vs yargs ― Node.js CLIフレームワーク徹底比較
1. 結論
シンプルで軽量なCLIツールを素早く作りたいなら commander、複雑なサブコマンド体系やインタラクティブなヘルプを備えた本格的なCLIを構築したいなら yargs を選ぶのがおすすめです。どちらも成熟したパッケージであり、プロダクション利用に十分耐えうる品質を持っています。プロジェクトの規模と求める機能の複雑さで判断してください。
2. 比較表
| 項目 | commander | yargs |
|---|---|---|
| npm 週間DL数 | 約 1.8 億 | 約 7,500 万 |
| GitHub Stars | 約 27k | 約 11k |
| バンドルサイズ (unpacked) | 約 190 KB(依存ゼロ) | 約 300 KB+(複数の依存あり) |
| 依存パッケージ数 | 0 | 約 7〜10 |
| TypeScript 対応 | 同梱型定義ファイル | 同梱型定義ファイル |
| サブコマンド | ✅ ネイティブ対応 | ✅ .command() で対応 |
| 自動ヘルプ生成 | ✅ | ✅(より詳細) |
| 自動バリデーション | 最小限(手動補完が必要) | ✅ 豊富(型・必須・排他など) |
| 補完 (Shell Completion) | プラグインで対応 | ✅ 組み込み対応 |
| ミドルウェア | ❌ | ✅ .middleware() |
| Strict モード | ❌ | ✅ .strict() |
| 学習コスト | 🟢 低い | 🟡 やや高い |
| API スタイル | メソッドチェーン(宣言的) | メソッドチェーン(ビルダー的) |
| ライセンス | MIT | MIT |
3. それぞれの強み
commander の強み
- ゼロ依存 —
node_modulesを汚さず、インストールが高速です。CI/CD パイプラインでも有利に働きます。 - 直感的な API — POSIX 規約に忠実で、
-p, --port <number>のような記法をそのまま文字列で定義できます。学習コストが非常に低く、README を読むだけで使い始められます。 - 圧倒的な採用実績 — webpack CLI、create-react-app、Vue CLI など、メジャーなツールが内部で採用しています。週間ダウンロード数は npm 全体でもトップクラスです。
- 軽量設計思想 — 「必要十分」を追求した設計で、余計な抽象化がありません。小〜中規模の CLI に最適です。
yargs の強み
- 強力なバリデーション — 型チェック(
string,number,boolean)、必須チェック(.demandOption())、排他オプション(.conflicts())、依存オプション(.implies())など、宣言的にルールを定義できます。 - ミドルウェア機構 — コマンド実行前に共通処理(認証チェック、設定ファイル読み込みなど)を挟めます。Express のミドルウェアに近い感覚です。
- シェル補完の組み込みサポート —
.completion()を呼ぶだけで bash/zsh の補完スクリプトを自動生成できます。 - 高度なサブコマンド管理 — コマンドごとにビルダー関数とハンドラーを分離でき、大規模 CLI の構造化に向いています。
- Strict モード — 未定義のオプションが渡された場合にエラーを出す
.strict()が組み込まれており、ユーザーのタイプミスを即座に検出できます。
4. コード例で比較
以下では 「ファイルを指定ディレクトリにコピーする CLI」 を両パッケージで実装します。
要件
mycli copy --src ./a.txt --dest ./backup/ --verbose
commander 版
// src/cli-commander.ts
import { Command } from "commander";
import { copyFileSync, mkdirSync, existsSync } from "fs";
import { basename, join } from "path";
const program = new Command();
program
.name("mycli")
.description("ファイル操作ユーティリティ")
.version("1.0.0");
program
.command("copy")
.description("ファイルを指定ディレクトリにコピーします")
.requiredOption("-s, --src <path>", "コピー元ファイルパス")
.requiredOption("-d, --dest <dir>", "コピー先ディレクトリ")
.option("-v, --verbose", "詳細ログを出力する", false)
.action((options) => {
const { src, dest, verbose } = options as {
src: string;
dest: string;
verbose: boolean;
};
// コピー先ディレクトリがなければ作成
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const target = join(dest, basename(src));
copyFileSync(src, target);
if (verbose) {
console.log(`✅ コピー完了: ${src} → ${target}`);
}
});
program.parse();
yargs 版
// src/cli-yargs.ts
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { copyFileSync, mkdirSync, existsSync } from "fs";
import { basename, join } from "path";
yargs(hideBin(process.argv))
.scriptName("mycli")
.usage("$0 <command> [options]")
.command(
"copy",
"ファイルを指定ディレクトリにコピーします",
(yargs) =>
yargs
.option("src", {
alias: "s",
type: "string",
demandOption: true,
describe: "コピー元ファイルパス",
})
.option("dest", {
alias: "d",
type: "string",
demandOption: true,
describe: "コピー先ディレクトリ",
})
.option("verbose", {
alias: "v",
type: "boolean",
default: false,
describe: "詳細ログを出力する",
})
// src が指定されたら dest も必須、という依存関係を宣言
.implies("src", "dest"),
(argv) => {
const { src, dest, verbose } = argv;
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const target = join(dest, basename(src));
copyFileSync(src, target);
if (verbose) {
console.log(`✅ コピー完了: ${src} → ${target}`);
}
}
)
.strict() // 未定義オプションをエラーにする
.demandCommand(1, "コマンドを1つ指定してください")
.help()
.parse();
コード比較のポイント
| 観点 | commander | yargs |
|---|---|---|
| オプション定義 | -s, --src <path> の文字列1行で完結 | オブジェクトで type, demandOption 等を明示 |
| 型安全性 | action の引数を手動キャスト | ビルダーから型推論が効く |
| バリデーション | requiredOption 程度 | strict(), implies(), conflicts() 等が豊富 |
| コード量 | やや少ない | やや多いが宣言的で堅牢 |
5. どちらを選ぶべきか ― ユースケース別ガイド
commander を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| 小〜中規模の社内ツール | ゼロ依存で導入が手軽。学習コストが低く、チームへの展開が容易です |
| npm パッケージとして配布する CLI | 依存ツリーが浅いため、利用者の node_modules を肥大化させません |
| プロトタイプ / スクリプト的な CLI | 最小限のコードで動くものが作れます |
| 既存プロジェクトへの後付け CLI | 軽量なので既存の依存と衝突するリスクが低いです |
yargs を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| サブコマンドが多い大規模 CLI | ミドルウェア・ビルダーパターンで構造化しやすいです |
| ユーザー入力のバリデーションを厳密にしたい | strict(), conflicts(), implies(), check() で宣言的に制約を定義できます |
| シェル補完を提供したい | .completion() で bash/zsh 補完スクリプトを自動生成できます |
| 設定ファイル + 環境変数 + CLI 引数を統合したい | yargs は .config() や .env() で多層的な設定ソースをマージできます |
第三の選択肢も検討する場合
近年は citty(unjs 製・軽量)や clipanion(Yarn Berry 内部で使用)なども注目されています。ただし、エコシステムの成熟度・情報量の観点では commander / yargs が依然として安定した選択です。
6. まとめ
シンプルさ・軽量さ重視 → commander
堅牢性・拡張性重視 → yargs
commander は「UNIX 哲学的にシンプルなことをシンプルに」を体現したパッケージです。依存ゼロという潔さは、ライブラリ作者やツールチェーンの一部として CLI を組み込む場面で大きなアドバンテージになります。
yargs は「CLI フレームワーク」と呼ぶにふさわしい機能の厚みを持っています。バリデーション、ミドルウェア、シェル補完、設定ファイル統合など、エンタープライズ級の CLI を構築する際に真価を発揮します。
どちらも長年にわたりメンテナンスが継続されており、TypeScript 対応も万全です。まずはプロジェクトの規模感と「どこまで CLI の入力を厳密に制御したいか」を基準に選択するのが、最も後悔の少ない判断になるでしょう。