Commander.js

軽量で人気のあるNode.js CLIライブラリ。直感的で流暢なAPIで知られています。

javascriptnodejsclicommand-linetypescript

GitHub概要

tj/commander.js

node.js command-line interfaces made easy

スター27,482
ウォッチ232
フォーク1,725
作成日:2011年8月14日
言語:JavaScript
ライセンス:MIT License

トピックス

なし

スター履歴

tj/commander.js Star History
データ取得日時: 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;
      }
    }
  });
});