Commander.js

A lightweight and popular Node.js CLI library. Known for its straightforward and fluent API.

javascriptnodejsclicommand-linetypescript

GitHub Overview

tj/commander.js

node.js command-line interfaces made easy

Stars27,482
Watchers232
Forks1,725
Created:August 14, 2011
Language:JavaScript
License:MIT License

Topics

None

Star History

tj/commander.js Star History
Data as of: 7/25/2025, 02:05 AM

Framework

Commander.js

Overview

Commander.js is the complete solution for building Node.js command-line interfaces. With the concept of "node.js command-line interfaces made easy," it provides command-line argument parsing, usage error display, and help system implementation through a concise and intuitive API. Despite being lightweight, it offers rich functionality and is a trusted library used in the CLI interfaces of many npm packages.

Why is it popular in Node.js:

  • Simple and intuitive API: Fluent API design makes it easy to define commands and options
  • Lightweight yet feature-rich: Provides extensive functionality with minimal dependencies
  • Extensive adoption track record: Proven track record of adoption in many famous npm packages
  • Excellent TypeScript support: Emphasizes type safety and developer experience

Detailed Description

History and Development

Commander.js is a veteran Node.js CLI library originally developed by TJ Holowaychuk. With a design philosophy emphasizing simplicity and ease of use, it has been beloved by many developers and plays an important role in the Node.js ecosystem. Through continuous development over many years, it remains actively maintained and supports the latest JavaScript/TypeScript features.

Position in the Ecosystem

Commander.js holds a special position in the Node.js ecosystem for the following reasons:

  • Standard CLI library: One of the two major CLI libraries alongside yargs in Node.js
  • Wide adoption track record: Used in many prominent projects including Vue CLI, Create React App, Mocha, and Webpack
  • Simple API design: Low learning curve enables rapid development
  • Stability and reliability: Long-standing track record proves stability in production environments

Latest Trends (2024-2025)

v14.x Series (Released May 2025)

  • Option and Command Group functionality: Organized help display using .helpGroup()
  • Negative number support: Direct specification of negative numbers in option and command arguments
  • Node.js v20+ required: Leverages the latest Node.js features
  • Configuration output improvements: Fixed side effects in .configureOutput()

v13.x Series (Released December 2024)

  • Multiple .parse() call support: Support for multiple parsing with default settings
  • State management functionality: .saveStateBeforeParse() and .restoreStateBeforeParse()
  • Help styling: Enhanced colored help and formatting features
  • Dual long option flags: Support for multiple long options like --ws, --workspace
  • Strict option parsing: More rigorous option flag validation

v12.x Series (Released February 2024)

  • Extended help configuration: Added .addHelpOption() and .helpCommand()
  • Node.js flag auto-detection: Automatic recognition of special flags like node --eval
  • Node.js v18+ required: Full utilization of modern Node.js features

Relationship with Node.js Built-in Argument Parser (2024 Key Point)

  • Built-in parser since Node.js 18.3: Node.js now includes a built-in argument parser, but Commander.js maintains these advantages:
  • Advanced features: Subcommands, complex option processing, customizable help generation
  • Mature ecosystem: Rich integration with plugins and libraries
  • Backward compatibility: Perfect for continued use in existing projects
  • Developer experience: More intuitive and expressive API design

Key Features

Core Functionality

  • Fluent API design: Intuitive command definition through method chaining
  • Automatic help generation: Comprehensive help messages based on definitions
  • Subcommand support: Support for hierarchical command structures
  • Option parsing: Complete support for short options, long options, and valued options
  • Argument validation: Management of required arguments, optional arguments, and variadic arguments

Advanced Features

  • Custom option processing: Type conversion, validation, and value accumulation
  • Negatable options: Negation options with --no- prefix
  • Environment variable integration: Reading option values from environment variables
  • Configuration file support: Integration functionality with external configurations
  • Pass-through options: Option forwarding to other programs

Developer Experience

  • Complete TypeScript support: Type-safe development with type definitions
  • Executable subcommands: Integration with independent script files
  • Rich events: Handling of option, command, and error events
  • Customizable help: Detailed customization of help text

Ecosystem Integration (2024-2025 Implementation Examples)

  • Chalk integration: Perfect match for making CLI output colorful
  • Inquirer integration: Adding interactive prompt functionality
  • Ora for progress display: Integration with loading animations
  • npm distribution: Libraries simplify publishing and distributing CLI tools

Latest Features and Updates

v14.x Major Features

  • Help group functionality: Grouping related options and commands
  • Direct negative number support: Direct specification of negative numbers like -3.14
  • Configuration side effect fixes: .configureOutput() properly copies configurations

v13.x Improvements

  • Strict error handling: Errors for excess arguments and unsupported option flags
  • Help styling: Colored help and box formatting features
  • State management: State save and restore functionality before and after parsing
  • Dual long options: Support for using two long option names together

v12.x New Features

  • Duplicate validation: Duplicate checking for option flags and command names
  • Executable subcommand improvements: More robust process management
  • Configuration API extensions: More flexible help and command configuration

Pros and Cons

Pros

Functionality

  • Simple and easy-to-learn API: Intuitive method chaining
  • Rich functionality: Wide coverage from basic to advanced features
  • Lightweight: Minimal dependencies and compact size
  • High extensibility: Custom processing and integration capabilities

Quality

  • Long-term track record: High stability and reliability
  • Active maintenance: Continuous improvement and latest support
  • Excellent documentation: Detailed and easy-to-understand explanations
  • Type safety: Complete type support in TypeScript

Cons

Complexity Aspects

  • Complexity of advanced features: Configuration can become complex for sophisticated CLIs
  • API design constraints: Need to follow specific patterns

Performance Aspects

  • Startup time: Slight overhead due to rich functionality
  • Memory usage: May be excessive for simple use cases

Key Links

Official Resources

Documentation

Usage Examples

Basic Usage Example

#!/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();
# Usage example
node string-util.js split --separator=/ a/b/c
# Output: [ 'a', 'b', 'c' ]

node string-util.js split --first --separator=/ a/b/c
# Output: [ 'a' ]

Option Definition and Processing

#!/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}`);
# Usage example
pizza-options -d -s -p vegetarian
# Output:
# { debug: true, small: true, pizzaType: 'vegetarian' }
# pizza details:
# - small pizza size
# - vegetarian

TypeScript Usage Example

#!/usr/bin/env node
import { Command } from 'commander';

interface Options {
  port?: string;
  debug?: boolean;
  env?: string;
}

const program = new Command();

program
  .name('myserver')
  .description('Server management tool')
  .version('1.0.0')
  .option('-p, --port <number>', 'server port number', '3000')
  .option('-d, --debug', 'enable debug mode')
  .option('-e, --env <environment>', 'execution environment', 'development')
  .action((options: Options) => {
    console.log(`Starting server...`);
    console.log(`Port: ${options.port}`);
    console.log(`Environment: ${options.env}`);
    if (options.debug) {
      console.log('Debug mode: ON');
    }
    startServer(options);
  });

program.parse();

function startServer(options: Options) {
  // Server startup logic
  console.log(`Server started on port ${options.port}`);
}

Command Argument Processing

#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();

program
  .name('file-manager')
  .description('File management tool')
  .version('1.0.0');

program
  .command('copy')
  .description('Copy files')
  .argument('<source>', 'source file')
  .argument('<destination>', 'destination file')
  .option('-f, --force', 'force overwrite')
  .action((source, destination, options) => {
    console.log(`Copying ${source} to ${destination}...`);
    if (options.force) {
      console.log('Force overwrite mode');
    }
    copyFile(source, destination, options.force);
  });

program
  .command('delete')
  .description('Delete files')
  .argument('<files...>', 'files to delete (multiple allowed)')
  .option('-r, --recursive', 'recursive deletion')
  .action((files, options) => {
    files.forEach((file) => {
      console.log(`Deleting: ${file}`);
      if (options.recursive) {
        console.log('  (recursive deletion mode)');
      }
      deleteFile(file, options.recursive);
    });
  });

program.parse();

function copyFile(source, dest, force) {
  // File copy logic
  console.log(`Copied ${source} to ${dest}`);
}

function deleteFile(file, recursive) {
  // File deletion logic
  console.log(`Deleted ${file}`);
}

Variadic Options

#!/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);
# Usage example
collect -n 1 2 3 --letter a b c
# Output:
# Options:  { number: [ '1', '2', '3' ], letter: [ 'a', 'b', 'c' ] }
# Remaining arguments:  []

collect --letter=A -n80 operand
# Output:
# Options:  { number: [ '80' ], letter: [ 'A' ] }
# Remaining arguments:  [ 'operand' ]

Custom Option Processing

#!/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);
# Usage example
custom -f 1e2 --integer 2 -v -v -v -c a -c b -c c --list x,y,z
# Output:
# float: 100
# integer: 2
# verbosity: 3
# [ 'a', 'b', 'c' ]
# [ 'x', 'y', 'z' ]

Advanced Option Configuration

#!/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());
# Usage example
extra --drink large --donate --free-drink
# Output: Options:  { timeout: 60, drink: 'small', donate: 20, freeDrink: true }

extra --disable-server --port 8000
# Output: error: option '--disable-server' cannot be used with option '-p, --port <number>'

Subcommand Implementation

#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();

program
  .name('git-like')
  .description('Git-like command line')
  .version('1.0.0');

program
  .command('clone')
  .description('Clone a repository')
  .argument('<repository>', 'repository URL to clone')
  .argument('[directory]', 'destination directory')
  .option('-b, --branch <name>', 'specify branch', 'main')
  .option('--depth <number>', 'shallow clone depth', parseInt)
  .action((repository, directory, options) => {
    console.log(`Cloning: ${repository}`);
    if (directory) {
      console.log(`Destination: ${directory}`);
    }
    console.log(`Branch: ${options.branch}`);
    if (options.depth) {
      console.log(`Depth: ${options.depth}`);
    }
    cloneRepository(repository, directory, options);
  });

program
  .command('status')
  .description('Show working directory status')
  .option('-s, --short', 'show in short format')
  .action((options) => {
    console.log('Working tree status:');
    if (options.short) {
      console.log('  (short format)');
    }
    showStatus(options.short);
  });

program
  .command('commit')
  .description('Commit changes')
  .option('-m, --message <message>', 'commit message')
  .option('-a, --all', 'include all changes')
  .action((options) => {
    if (!options.message) {
      console.error('Error: commit message required');
      process.exit(1);
    }
    console.log(`Committing: "${options.message}"`);
    if (options.all) {
      console.log('  including all changes');
    }
    commitChanges(options.message, options.all);
  });

program.parse();

function cloneRepository(repo, dir, options) {
  // Clone logic
  console.log(`Repository ${repo} cloned successfully`);
}

function showStatus(short) {
  // Status display logic
  console.log('Status displayed');
}

function commitChanges(message, all) {
  // Commit logic
  console.log(`Changes committed: ${message}`);
}

Negatable Options and Configuration

#!/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}`);
# Usage example
pizza-options
# Output: You ordered a pizza with sauce and mozzarella cheese

pizza-options --cheese=blue
# Output: You ordered a pizza with sauce and blue cheese

pizza-options --no-sauce --no-cheese
# Output: You ordered a pizza with no sauce and no cheese

Executable Subcommands

// cli.js - Main file
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();

program
  .name('myapp')
  .description('Application management tool')
  .version('1.0.0');

// Subcommands executed as independent files
program
  .command('start <service>', 'start service', { executableFile: 'myapp-start' })
  .command('stop [service]', 'stop service', { executableFile: 'myapp-stop' });

program.parse();
// myapp-start.js - Independent subcommand file
#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();

program
  .name('myapp start')
  .description('Service start command')
  .argument('<service>', 'service name to start')
  .option('-p, --port <number>', 'port number', '3000')
  .option('-d, --daemon', 'run as daemon')
  .action((service, options) => {
    console.log(`Starting ${service} service...`);
    console.log(`Port: ${options.port}`);
    if (options.daemon) {
      console.log('Running in daemon mode');
    }
    startService(service, options);
  });

program.parse();

function startService(service, options) {
  // Service start logic
  console.log(`Service ${service} started on port ${options.port}`);
}

Error Handling and Events

#!/usr/bin/env node
const { Command } = require('commander');
const program = new Command();

program
  .name('event-demo')
  .description('Event handling demo')
  .version('1.0.0');

// Monitor option events
program.on('option:verbose', function () {
  process.env.VERBOSE = this.opts().verbose;
  console.log('Verbose mode enabled');
});

// Monitor command events
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('Test command')
  .action(() => {
    console.log('Test command executed');
    if (process.env.VERBOSE) {
      console.log('Verbose information...');
    }
  });

// Custom error handling
program.exitOverride((err) => {
  if (err.code === 'commander.unknownOption') {
    console.error('Custom error: Unknown option');
  }
  process.exit(err.exitCode);
});

program.parse();

Modular Design

// commands/database.js - Database command module
const { Command } = require('commander');

function makeDatabaseCommand() {
  const db = new Command('database');
  
  db
    .alias('db')
    .description('Database operation commands');

  db
    .command('migrate')
    .description('Execute database migration')
    .option('--dry-run', 'test migration execution')
    .action((options) => {
      console.log('Executing migration...');
      if (options.dryRun) {
        console.log('  (test execution mode)');
      }
      runMigration(options.dryRun);
    });

  db
    .command('seed')
    .description('Insert initial data')
    .option('-e, --env <environment>', 'environment', 'development')
    .action((options) => {
      console.log(`Inserting initial data for ${options.env} environment...`);
      seedDatabase(options.env);
    });

  return db;
}

function runMigration(dryRun) {
  // Migration logic
  console.log('Migration completed');
}

function seedDatabase(env) {
  // Seed logic  
  console.log(`Database seeded for ${env}`);
}

module.exports = { makeDatabaseCommand };
// main.js - Main file
#!/usr/bin/env node
const { Command } = require('commander');
const { makeDatabaseCommand } = require('./commands/database');

const program = new Command();

program
  .name('myapp')
  .description('Application management CLI')
  .version('1.0.0');

// Add modular commands
program.addCommand(makeDatabaseCommand());

program.parse();

Configuration File Integration

#!/usr/bin/env node
const { Command } = require('commander');
const fs = require('fs');
const path = require('path');

const program = new Command();

// Configuration file loading function
function loadConfig(configPath) {
  if (fs.existsSync(configPath)) {
    try {
      return JSON.parse(fs.readFileSync(configPath, 'utf8'));
    } catch (error) {
      console.warn(`Failed to load config file: ${configPath}`);
      return {};
    }
  }
  return {};
}

program
  .name('config-demo')
  .description('Configuration file integration demo') 
  .version('1.0.0')
  .option('-c, --config <path>', 'configuration file path', './config.json')
  .option('-o, --output <path>', 'output directory', './output')
  .option('-v, --verbose', 'verbose output')
  .action((options) => {
    // Load configuration file
    const config = loadConfig(options.config);
    
    // Merge command line options with config file
    const mergedOptions = {
      output: options.output || config.output || './output',
      verbose: options.verbose || config.verbose || false,
      ...config,
      ...options // Prioritize command line options
    };

    console.log('Final configuration:', mergedOptions);
    
    if (mergedOptions.verbose) {
      console.log('Running in verbose mode...');
    }
    
    processWithConfig(mergedOptions);
  });

program.parse();

function processWithConfig(config) {
  // Processing based on configuration
  console.log(`Processing with output: ${config.output}`);
}
// config.json - Configuration file example
{
  "output": "./dist",
  "verbose": true,
  "optimization": {
    "minify": true,
    "sourceMaps": false
  },
  "plugins": ["babel", "typescript"]
}

Integration with Testing

// cli.js - Testable CLI
const { Command } = require('commander');

function createProgram() {
  const program = new Command();
  
  program
    .name('testable-cli')
    .description('Testable CLI')
    .version('1.0.0');

  program
    .command('process')
    .description('Data processing')
    .argument('<input>', 'input file')
    .option('-f, --format <type>', 'output format', 'json')
    .action((input, options) => {
      const result = processData(input, options.format);
      console.log(JSON.stringify(result, null, 2));
      return result; // Return result for testing
    });

  return program;
}

function processData(input, format) {
  // Data processing logic
  return {
    input,
    format,
    processed: true,
    timestamp: new Date().toISOString()
  };
}

// Main execution
if (require.main === module) {
  const program = createProgram();
  program.parse();
}

// Export for testing
module.exports = { createProgram, processData };
// cli.test.js - Test file example
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();
    
    // Method to test program
    program.exitOverride(); // Disable exit
    
    try {
      await program.parseAsync(['node', 'cli.js', 'process', 'input.txt', '--format', 'xml']);
    } catch (err) {
      // Only for normal termination
      if (err.code !== 'commander.executeSubCommand') {
        throw err;
      }
    }
  });
});