Commander.js
A lightweight and popular Node.js CLI library. Known for its straightforward and fluent API.
GitHub Overview
tj/commander.js
node.js command-line interfaces made easy
Topics
Star History
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;
}
}
});
});