|claim|login
RepoCritics — Review. Share. Archive. Every open-source repo.

tj/commander.js

Wiki: tj/commander.js

Source: https://github.com/tj/commander.js

Last synced 2026-07-16 · 1157 words · Edit wiki on GitHub →

tj/commander.js

> The most-installed Node.js argument parser — a small, dependency-free library for describing CLIs as a tree of commands, options, and arguments.

GitHub repo · npm: commander · License: MIT

Overview

Commander is a command-line interface library for Node.js, originally written by TJ Holowaychuk in 20111 and maintained since the late 2010s primarily by John Gee (shadowspawn) and other contributors. You declare a program's options, sub-commands, and positional arguments with a chained builder API; Commander parses process.argv, validates it, dispatches to an action handler, and generates --help output. As of 2026 it is one of the most-depended-upon packages on npm, pulled in transitively by a large share of the CLI tooling in the JavaScript ecosystem.

Its defining choice is scope restraint. Commander has zero runtime dependencies2 and deliberately does not do prompting, colored output, autocompletion, or config-file loading — those are left to other libraries. What it does cover is thorough: strict unknown-option rejection, negatable booleans, variadic and optional option-arguments, required options, custom argument coercion, sub-command trees, life-cycle hooks, and environment-variable fallbacks. The tension for adopters is between this and heavier frameworks (yargs, oclif): Commander gives you a parser and a help generator, not an application skeleton.

The library ships as ESM ("type": "module") with a CommonJS interop path and bundled TypeScript declarations3. Version 15 requires Node.js >= 22.122; older Node lines must pin to an earlier major.

Getting Started

npm install commander
// string-util.js — ESM
import { Command } from 'commander';
const program = new Command();

program
  .name('string-util')
  .description('CLI to some JavaScript string utilities')
  .version('1.0.0');

program.command('split')
  .description('Split a string into substrings')
  .argument('<string>', 'string to split')
  .option('-s, --separator <char>', 'separator character', ',')
  .option('--first', 'display just the first substring')
  .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 --oops a-b-c
error: unknown option '--oops'

Architecture / How It Works

The public surface is small — Command, Option, Argument, Help — but the model behind it is a tree of Command objects. .command() creates and links a child; sub-commands nest arbitrarily. Each node carries its own options, arguments, help configuration, and optional action handler.

Parsing is a hand-written argument scanner, not a grammar or schema. It walks argv left to right, classifying tokens as options (long, short, combined short like -ds, --opt=value), option-arguments, or operands. -- terminates option processing. Anything not consumed by a command's own options is passed down to a matched sub-command or left in program.args. Because the scanner is stateful and greedy, precedence rules matter: options with a required value consume the next token unconditionally, while optional-argument options ([value]) are non-greedy and stop at a leading dash — a distinction that produces most of the "why did my flag eat that argument" surprises.

Option names are normalized to camelCase (--template-engineopts().templateEngine). Since v7 the recommended access path is .opts() and action-handler parameters rather than reading options as properties on the command object; the legacy property-storage behavior still exists behind storeOptionsAsProperties() for older code4. The Option class exposes the less-common features — .choices(), .env(), .default(), .preset(), .conflicts(), .implies(), .argParser() — added via .addOption().

Sub-commands come in two forms: action handlers (a callback on the same process) and stand-alone executables (Commander spawns a separate file, e.g. myprog-install, as a child process, git-style). Life-cycle hooks (preAction, postAction, preSubcommand) let you run logic around dispatch. Help is generated by the Help class, which is subclassable for custom layouts.

Production Notes

  • The global program singleton is shared state. import { program } is convenient for one-file scripts, but the same instance persists across calls within a process. For anything unit-tested or embedded, construct a fresh new Command() per invocation instead — reusing the singleton leaks parsed option values between runs.
  • Commander calls process.exit() by default. On a parse error or on --help/--version, it writes output and exits the process. That is correct for a CLI but hostile to tests and to embedding Commander inside a larger app. Use .exitOverride() to make it throw a CommanderError instead, and .configureOutput() to redirect writes5.
  • The v7 upgrade was the big behavioral break. Command-arguments moved to being passed as explicit action-handler parameters, and reading options as properties on the command was demoted in favor of .opts(). Migrating a large v6-or-earlier CLI is mechanical but touches every action handler4.
  • Stand-alone executable sub-commands resist bundling. Because they are discovered and spawned as separate files at runtime, single-file bundlers (esbuild, pkg, ncc) and packaged binaries do not trace them. Prefer action-handler sub-commands if you bundle.
  • Optional-argument greediness is a recurring footgun. --flag [value] will not consume -x but --flag <value> will; negative numbers are a special case that is accepted as a value. Read the options-in-depth docs before relying on ambiguous forms.
  • Node baseline moves with majors. v15 requires Node >= 22.12; v12 required Node >= 18; v9 dropped Node 12. Pin the major to your runtime — upgrading Commander can silently raise your minimum Node.
  • No prompts, colors, or completion by design. If you need interactive input or shell completion, layer separate libraries; Commander will not grow them.

When to Use / When Not

Use when:

  • You want a small, stable, dependency-free parser with good --help generation.
  • Your CLI is a program with flags and a handful of sub-commands, driven by action handlers.
  • You value a long-lived, conservative API over a batteries-included framework.

Avoid when:

  • You are building a large multi-team CLI product that wants plugins, generators, and a testing harness — reach for a framework (oclif).
  • You want middleware, command grouping, and rich composition semantics out of the box (yargs).
  • You need only to read a flat --key value map with no help system — a minimal parser is lighter.

Alternatives

  • yargs/yargs — heavier, more features (middleware, command modules, built-in completion); use when you want more framework and don't mind the surface area.
  • oclif/oclif — opinionated CLI framework with plugins and generators; use for large, long-lived CLI products, not small scripts.
  • sindresorhus/meow — minimal ESM-first wrapper over a low-level parser; use for tiny single-purpose CLIs.
  • minimistjs/minimist — bare argv-to-object parser, no help or validation; use when you want to build your own layer on top.
  • cacjs/cac — small parser with a similar builder feel and built-in help; use when you want a lighter Commander-like API.

History

VersionDateNotes
1.x2011Initial release by TJ Holowaychuk1.
5.02020-03-12Modernized parsing; dropped older Node lines.
7.02021-01-13Command-arguments passed to action handlers; .opts() favored over properties4.
8.02021-06-23.argument() API, argument coercion refinements.
9.02022-01-28Dropped Node 12.
10.02023-01-14Node baseline raised; internal cleanups.
11.02023-05-27Help groups and configuration additions.
12.02024-02-03Requires Node >= 18.
13.02024-12-30Continued API refinement.
14.02025-05-16Help output and typing improvements.
15.02026-05-29Requires Node >= 22.12; ESM-first2.

References

  1. ^ Commander.js repository history; original author TJ Holowaychuk, first published 2011. https://github.com/tj/commander.js
  2. ^ package.json at v15.0.0 — zero dependencies, "type": "module", engines.node ">=22.12.0". https://github.com/tj/commander.js/blob/master/package.json
  3. ^ Commander README — TypeScript declarations bundled, CommonJS and ESM usage. https://github.com/tj/commander.js/blob/master/Readme.md
  4. ^ Commander CHANGELOG / v7 release notes — action-handler argument and options-access changes. https://github.com/tj/commander.js/blob/master/CHANGELOG.md
  5. ^ Commander README, "Override exit and output handling" — .exitOverride() and .configureOutput(). https://github.com/tj/commander.js/blob/master/Readme.md#override-exit-and-output-handling

Tags

javascript, typescript, nodejs, cli, argument-parser, command-line, library, zero-dependency, esm, developer-tools