Commander.js
軽量で人気のあるNode.js CLIライブラリ。直感的で流暢なAPIで知られています。
GitHub概要
スター27,482
ウォッチ232
フォーク1,725
作成日:2011年8月14日
言語:JavaScript
ライセンス:MIT License
トピックス
なし
スター履歴
データ取得日時: 2025/7/25 02:05
フレームワーク
Commander.js
概要
Commander.jsは、Node.jsのコマンドライン界面を構築するための完全なソリューションです。「node.js command-line interfaces made easy」をコンセプトに、コマンドライン引数の解析、使用方法のエラー表示、ヘルプシステムの実装を簡潔で直感的なAPIで提供します。軽量でありながら豊富な機能を持ち、多くのnpmパッケージのCLIインターフェースで使用されている信頼性の高いライブラリです。
なぜNode.jsで人気なのか:
- シンプルで直感的なAPI: 流暢なAPIデザインにより、コマンドとオプションを簡単に定義
- 軽量でありながら高機能: 最小限の依存関係で豊富な機能を提供
- 広範囲な採用実績: 多くの有名なnpmパッケージで採用される実績
- 優れたTypeScriptサポート: 型安全性と開発者体験を重視
詳細説明
歴史と発展
Commander.jsは、TJ Holowaychukによって開発が開始されたNode.js CLIライブラリの老舗です。シンプルさと使いやすさを重視した設計により、多くの開発者に愛用され、Node.jsエコシステムの重要な部分を担っています。長年にわたる継続的な開発により、現在もアクティブにメンテナンスされ、最新のJavaScript/TypeScript機能に対応しています。
エコシステムでの位置づけ
Node.jsエコシステムにおいて、Commander.jsは以下の理由で特別な地位を占めています:
- CLIライブラリの定番: yargsと並んでNode.js CLIライブラリの2大巨頭
- 幅広い採用実績: Vue CLI、Create React App、Mocha、Webpackなど多数の著名プロジェクトで使用
- シンプルなAPIデザイン: 学習コストが低く、迅速な開発が可能
- 安定性と信頼性: 長年の実績により、プロダクション環境での安定性を実証
最新動向(2024-2025年)
v14.x シリーズ(2025年5月リリース)
- オプション・コマンドグループ機能:
.helpGroup()
による整理されたヘルプ表示 - 負の数値サポート: オプション引数や コマンド引数での負の数値の直接指定
- Node.js v20以上が必須: 最新のNode.js機能を活用
- 設定出力の改善:
.configureOutput()
の副作用を修正
v13.x シリーズ(2024年12月リリース)
- 複数の
.parse()
呼び出しサポート: デフォルト設定での複数解析対応 - 状態管理機能:
.saveStateBeforeParse()
と.restoreStateBeforeParse()
- ヘルプスタイリング: 色付きヘルプと整形機能の強化
- デュアル長オプションフラグ:
--ws, --workspace
のような複数長オプション対応 - 厳格なオプション解析: より厳密なオプションフラグ検証
v12.x シリーズ(2024年2月リリース)
- ヘルプ設定の拡張:
.addHelpOption()
と.helpCommand()
の追加 - Node.jsフラグ自動検出:
node --eval
等の特殊フラグの自動認識 - Node.js v18以上が必須: モダンなNode.js機能をフル活用
Node.js組み込み引数パーサーとの関係(2024年注目ポイント)
- Node.js 18.3以降での組み込みパーサー: Node.js自体に引数パーサーが内蔵されたが、Commander.jsは以下の優位性を維持
- 高度な機能: サブコマンド、複雑なオプション処理、カスタマイズ可能なヘルプ生成
- 成熟したエコシステム: 豊富なプラグインとライブラリとの統合
- 後方互換性: 既存プロジェクトでの継続使用に最適
- 開発者体験: より直感的で表現力豊かなAPI設計
主な特徴
コア機能
- 流暢なAPI設計: メソッドチェーンによる直感的なコマンド定義
- 自動ヘルプ生成: 定義に基づく包括的なヘルプメッセージ
- サブコマンドサポート: 階層的なコマンド構造をサポート
- オプション解析: 短いオプション・長いオプション・値付きオプションの完全サポート
- 引数バリデーション: 必須引数・オプション引数・可変長引数の管理
高度な機能
- カスタムオプション処理: 型変換・バリデーション・値の蓄積
- ネガブルオプション:
--no-
接頭詞による否定オプション - 環境変数統合: 環境変数からのオプション値読み込み
- 設定ファイル対応: 外部設定との統合機能
- パススルーオプション: 他のプログラムへのオプション転送
開発者体験
- TypeScript完全サポート: 型定義による型安全な開発
- 実行可能サブコマンド: 独立したスクリプトファイルとの連携
- 豊富なイベント: オプション・コマンド・エラーイベントのハンドリング
- カスタマイズ可能なヘルプ: ヘルプテキストの詳細カスタマイズ
エコシステム統合(2024-2025年の実装例)
- Chalkとの統合: CLIの出力をカラフルにするためのperfect match
- Inquirerとの連携: インタラクティブなプロンプト機能の追加
- Oraによる進行表示: ローディングアニメーションとの統合
- npmでの配布: ライブラリがCLIツールの公開と配布を簡素化
最新機能とアップデート
v14.x の主要機能
- ヘルプグループ機能: 関連するオプションやコマンドをグループ化
- 負の数値の直接サポート:
-3.14
のような負の数値を直接指定 - 設定の副作用修正:
.configureOutput()
が設定を適切にコピー
v13.x の改善点
- 厳格なエラー処理: 過剰な引数やサポートされていないオプションフラグのエラー
- ヘルプスタイリング: 色付きヘルプとボックス整形機能
- 状態管理: 解析前後の状態保存・復元機能
- デュアル長オプション: 2つの長オプション名の併用サポート
v12.x の新機能
- 重複検証: オプションフラグやコマンド名の重複チェック
- 実行可能サブコマンドの改善: より堅牢なプロセス管理
- 設定API拡張: より柔軟なヘルプとコマンド設定
メリット・デメリット
メリット
機能面
- シンプルで学習しやすいAPI: 直感的なメソッドチェーン
- 豊富な機能: 基本から高度な機能まで幅広くカバー
- 軽量: 最小限の依存関係とコンパクトなサイズ
- 高い拡張性: カスタム処理や統合機能
品質面
- 長期間の実績: 安定性と信頼性の高さ
- 活発なメンテナンス: 継続的な改善と最新対応
- 優れたドキュメント: 詳細で分かりやすい説明
- 型安全性: TypeScriptでの完全な型サポート
デメリット
複雑性の側面
- 高度な機能の複雑さ: 複雑なCLIでは設定が煩雑になる場合
- API設計の制約: 特定のパターンに従う必要性
パフォーマンス面
- 起動時間: 多機能ゆえの若干のオーバーヘッド
- メモリ使用量: シンプルな用途には過剰な場合
主要リンク
公式リソース
ドキュメント
書き方の例
基本的な使用例
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('string-util')
.description('CLI to some JavaScript string utilities')
.version('0.8.0');
program.command('split')
.description('Split a string into substrings and display as an array')
.argument('<string>', 'string to split')
.option('--first', 'display just the first substring')
.option('-s, --separator <char>', 'separator character', ',')
.action((str, options) => {
const limit = options.first ? 1 : undefined;
console.log(str.split(options.separator, limit));
});
program.parse();
# 実行例
node string-util.js split --separator=/ a/b/c
# 出力: [ 'a', 'b', 'c' ]
node string-util.js split --first --separator=/ a/b/c
# 出力: [ 'a' ]
オプションの定義と処理
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.option('-d, --debug', 'output extra debugging')
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza');
program.parse(process.argv);
const options = program.opts();
if (options.debug) console.log(options);
console.log('pizza details:');
if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);
# 実行例
pizza-options -d -s -p vegetarian
# 出力:
# { debug: true, small: true, pizzaType: 'vegetarian' }
# pizza details:
# - small pizza size
# - vegetarian
TypeScriptでの使用例
#!/usr/bin/env node
import { Command } from 'commander';
interface Options {
port?: string;
debug?: boolean;
env?: string;
}
const program = new Command();
program
.name('myserver')
.description('サーバー管理ツール')
.version('1.0.0')
.option('-p, --port <number>', 'サーバーポート番号', '3000')
.option('-d, --debug', 'デバッグモードを有効化')
.option('-e, --env <environment>', '実行環境', 'development')
.action((options: Options) => {
console.log(`サーバーを起動中...`);
console.log(`ポート: ${options.port}`);
console.log(`環境: ${options.env}`);
if (options.debug) {
console.log('デバッグモード: ON');
}
startServer(options);
});
program.parse();
function startServer(options: Options) {
// サーバー起動ロジック
console.log(`Server started on port ${options.port}`);
}
コマンド引数の処理
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('file-manager')
.description('ファイル管理ツール')
.version('1.0.0');
program
.command('copy')
.description('ファイルをコピー')
.argument('<source>', 'コピー元ファイル')
.argument('<destination>', 'コピー先ファイル')
.option('-f, --force', '強制上書き')
.action((source, destination, options) => {
console.log(`${source} を ${destination} にコピー中...`);
if (options.force) {
console.log('強制上書きモード');
}
copyFile(source, destination, options.force);
});
program
.command('delete')
.description('ファイルを削除')
.argument('<files...>', '削除するファイル(複数指定可)')
.option('-r, --recursive', '再帰的削除')
.action((files, options) => {
files.forEach((file) => {
console.log(`削除中: ${file}`);
if (options.recursive) {
console.log(' 再帰的削除モード');
}
deleteFile(file, options.recursive);
});
});
program.parse();
function copyFile(source, dest, force) {
// ファイルコピーロジック
console.log(`Copied ${source} to ${dest}`);
}
function deleteFile(file, recursive) {
// ファイル削除ロジック
console.log(`Deleted ${file}`);
}
可変長オプション
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.option('-n, --number <numbers...>', 'specify numbers')
.option('-l, --letter [letters...]', 'specify letters');
program.parse();
console.log('Options: ', program.opts());
console.log('Remaining arguments: ', program.args);
# 実行例
collect -n 1 2 3 --letter a b c
# 出力:
# Options: { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] }
# Remaining arguments: []
collect --letter=A -n80 operand
# 出力:
# Options: { number: [ '80' ], letter: [ 'A' ] }
# Remaining arguments: [ 'operand' ]
カスタムオプション処理
#!/usr/bin/env node
const { Command, InvalidArgumentError } = require('commander');
const program = new Command();
function myParseInt(value, dummyPrevious) {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new InvalidArgumentError('Not a number.');
}
return parsedValue;
}
function increaseVerbosity(dummyValue, previous) {
return previous + 1;
}
function collect(value, previous) {
return previous.concat([value]);
}
function commaSeparatedList(value, dummyPrevious) {
return value.split(',');
}
program
.option('-f, --float <number>', 'float argument', parseFloat)
.option('-i, --integer <number>', 'integer argument', myParseInt)
.option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0)
.option('-c, --collect <value>', 'repeatable value', collect, [])
.option('-l, --list <items>', 'comma separated list', commaSeparatedList);
program.parse();
const options = program.opts();
if (options.float !== undefined) console.log(`float: ${options.float}`);
if (options.integer !== undefined) console.log(`integer: ${options.integer}`);
if (options.verbose > 0) console.log(`verbosity: ${options.verbose}`);
if (options.collect.length > 0) console.log(options.collect);
if (options.list !== undefined) console.log(options.list);
# 実行例
custom -f 1e2 --integer 2 -v -v -v -c a -c b -c c --list x,y,z
# 出力:
# float: 100
# integer: 2
# verbosity: 3
# [ 'a', 'b', 'c' ]
# [ 'x', 'y', 'z' ]
高度なオプション設定
#!/usr/bin/env node
const { Command, Option } = require('commander');
const program = new Command();
program
.addOption(new Option('-s, --secret').hideHelp())
.addOption(new Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
.addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']))
.addOption(new Option('-p, --port <number>', 'port number').env('PORT'))
.addOption(new Option('--donate [amount]', 'optional donation in dollars').preset('20').argParser(parseFloat))
.addOption(new Option('--disable-server', 'disables the server').conflicts('port'))
.addOption(new Option('--free-drink', 'small drink included free ').implies({ drink: 'small' }));
program.parse();
console.log('Options: ', program.opts());
# 実行例
extra --drink large --donate --free-drink
# 出力: Options: { timeout: 60, drink: 'small', donate: 20, freeDrink: true }
extra --disable-server --port 8000
# 出力: error: option '--disable-server' cannot be used with option '-p, --port <number>'
サブコマンドの実装
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('git-like')
.description('Git風のコマンドライン')
.version('1.0.0');
program
.command('clone')
.description('リポジトリをクローン')
.argument('<repository>', 'クローンするリポジトリURL')
.argument('[directory]', 'クローン先ディレクトリ')
.option('-b, --branch <name>', 'ブランチを指定', 'main')
.option('--depth <number>', 'shallow cloneの深度', parseInt)
.action((repository, directory, options) => {
console.log(`クローン中: ${repository}`);
if (directory) {
console.log(`クローン先: ${directory}`);
}
console.log(`ブランチ: ${options.branch}`);
if (options.depth) {
console.log(`深度: ${options.depth}`);
}
cloneRepository(repository, directory, options);
});
program
.command('status')
.description('作業ディレクトリの状態を表示')
.option('-s, --short', '短縮形式で表示')
.action((options) => {
console.log('Working tree status:');
if (options.short) {
console.log(' (short format)');
}
showStatus(options.short);
});
program
.command('commit')
.description('変更をコミット')
.option('-m, --message <message>', 'コミットメッセージ')
.option('-a, --all', 'すべての変更を含める')
.action((options) => {
if (!options.message) {
console.error('エラー: コミットメッセージが必要です');
process.exit(1);
}
console.log(`コミット中: "${options.message}"`);
if (options.all) {
console.log(' すべての変更を含める');
}
commitChanges(options.message, options.all);
});
program.parse();
function cloneRepository(repo, dir, options) {
// クローンロジック
console.log(`Repository ${repo} cloned successfully`);
}
function showStatus(short) {
// 状態表示ロジック
console.log('Status displayed');
}
function commitChanges(message, all) {
// コミットロジック
console.log(`Changes committed: ${message}`);
}
ネガブルオプションと設定
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.option('--no-sauce', 'Remove sauce')
.option('--cheese <flavour>', 'cheese flavour', 'mozzarella')
.option('--no-cheese', 'plain with no cheese')
.parse();
const options = program.opts();
const sauceStr = options.sauce ? 'sauce' : 'no sauce';
const cheeseStr = (options.cheese === false) ? 'no cheese' : `${options.cheese} cheese`;
console.log(`You ordered a pizza with ${sauceStr} and ${cheeseStr}`);
# 実行例
pizza-options
# 出力: You ordered a pizza with sauce and mozzarella cheese
pizza-options --cheese=blue
# 出力: You ordered a pizza with sauce and blue cheese
pizza-options --no-sauce --no-cheese
# 出力: You ordered a pizza with no sauce and no cheese
実行可能サブコマンド
// cli.js - メインファイル
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('myapp')
.description('アプリケーション管理ツール')
.version('1.0.0');
// 独立したファイルとして実行されるサブコマンド
program
.command('start <service>', 'サービスを開始', { executableFile: 'myapp-start' })
.command('stop [service]', 'サービスを停止', { executableFile: 'myapp-stop' });
program.parse();
// myapp-start.js - 独立したサブコマンドファイル
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('myapp start')
.description('サービス開始コマンド')
.argument('<service>', '開始するサービス名')
.option('-p, --port <number>', 'ポート番号', '3000')
.option('-d, --daemon', 'デーモンとして実行')
.action((service, options) => {
console.log(`${service} サービスを開始中...`);
console.log(`ポート: ${options.port}`);
if (options.daemon) {
console.log('デーモンモードで実行');
}
startService(service, options);
});
program.parse();
function startService(service, options) {
// サービス開始ロジック
console.log(`Service ${service} started on port ${options.port}`);
}
エラーハンドリングとイベント
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();
program
.name('event-demo')
.description('イベント処理のデモ')
.version('1.0.0');
// オプションイベントの監視
program.on('option:verbose', function () {
process.env.VERBOSE = this.opts().verbose;
console.log('Verbose mode enabled');
});
// コマンドイベントの監視
program.on('command:*', function (operands) {
console.error(`Unknown command: ${operands[0]}`);
console.error('See --help for a list of available commands.');
process.exitCode = 1;
});
program
.option('-v, --verbose', 'enable verbose output')
.command('test')
.description('テストコマンド')
.action(() => {
console.log('Test command executed');
if (process.env.VERBOSE) {
console.log('Verbose information...');
}
});
// カスタムエラーハンドリング
program.exitOverride((err) => {
if (err.code === 'commander.unknownOption') {
console.error('カスタムエラー: 不明なオプションです');
}
process.exit(err.exitCode);
});
program.parse();
モジュール化された設計
// commands/database.js - データベースコマンドモジュール
const { Command } = require('commander');
function makeDatabaseCommand() {
const db = new Command('database');
db
.alias('db')
.description('データベース操作コマンド');
db
.command('migrate')
.description('データベースマイグレーション実行')
.option('--dry-run', 'マイグレーションのテスト実行')
.action((options) => {
console.log('マイグレーション実行中...');
if (options.dryRun) {
console.log(' (テスト実行モード)');
}
runMigration(options.dryRun);
});
db
.command('seed')
.description('初期データの投入')
.option('-e, --env <environment>', '環境', 'development')
.action((options) => {
console.log(`${options.env}環境に初期データを投入中...`);
seedDatabase(options.env);
});
return db;
}
function runMigration(dryRun) {
// マイグレーションロジック
console.log('Migration completed');
}
function seedDatabase(env) {
// シードロジック
console.log(`Database seeded for ${env}`);
}
module.exports = { makeDatabaseCommand };
// main.js - メインファイル
#!/usr/bin/env node
const { Command } = require('commander');
const { makeDatabaseCommand } = require('./commands/database');
const program = new Command();
program
.name('myapp')
.description('アプリケーション管理CLI')
.version('1.0.0');
// モジュール化されたコマンドを追加
program.addCommand(makeDatabaseCommand());
program.parse();
設定ファイルとの統合
#!/usr/bin/env node
const { Command } = require('commander');
const fs = require('fs');
const path = require('path');
const program = new Command();
// 設定ファイル読み込み関数
function loadConfig(configPath) {
if (fs.existsSync(configPath)) {
try {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch (error) {
console.warn(`設定ファイルの読み込みに失敗: ${configPath}`);
return {};
}
}
return {};
}
program
.name('config-demo')
.description('設定ファイル統合デモ')
.version('1.0.0')
.option('-c, --config <path>', '設定ファイルのパス', './config.json')
.option('-o, --output <path>', '出力ディレクトリ', './output')
.option('-v, --verbose', '詳細出力')
.action((options) => {
// 設定ファイルの読み込み
const config = loadConfig(options.config);
// コマンドラインオプションと設定ファイルのマージ
const mergedOptions = {
output: options.output || config.output || './output',
verbose: options.verbose || config.verbose || false,
...config,
...options // コマンドラインオプションを優先
};
console.log('最終設定:', mergedOptions);
if (mergedOptions.verbose) {
console.log('詳細モードで実行中...');
}
processWithConfig(mergedOptions);
});
program.parse();
function processWithConfig(config) {
// 設定に基づく処理
console.log(`Processing with output: ${config.output}`);
}
// config.json - 設定ファイル例
{
"output": "./dist",
"verbose": true,
"optimization": {
"minify": true,
"sourceMaps": false
},
"plugins": ["babel", "typescript"]
}
テストとの統合
// cli.js - テスト可能なCLI
const { Command } = require('commander');
function createProgram() {
const program = new Command();
program
.name('testable-cli')
.description('テスト可能なCLI')
.version('1.0.0');
program
.command('process')
.description('データ処理')
.argument('<input>', '入力ファイル')
.option('-f, --format <type>', '出力形式', 'json')
.action((input, options) => {
const result = processData(input, options.format);
console.log(JSON.stringify(result, null, 2));
return result; // テスト用に結果を返す
});
return program;
}
function processData(input, format) {
// データ処理ロジック
return {
input,
format,
processed: true,
timestamp: new Date().toISOString()
};
}
// メイン実行
if (require.main === module) {
const program = createProgram();
program.parse();
}
// テスト用エクスポート
module.exports = { createProgram, processData };
// cli.test.js - テストファイル例
const { createProgram, processData } = require('./cli');
describe('CLI Tests', () => {
test('should process data correctly', () => {
const result = processData('test.txt', 'json');
expect(result.input).toBe('test.txt');
expect(result.format).toBe('json');
expect(result.processed).toBe(true);
});
test('should parse arguments correctly', async () => {
const program = createProgram();
// プログラムをテストする方法
program.exitOverride(); // exitを無効化
try {
await program.parseAsync(['node', 'cli.js', 'process', 'input.txt', '--format', 'xml']);
} catch (err) {
// 正常終了の場合のみ
if (err.code !== 'commander.executeSubCommand') {
throw err;
}
}
});
});