Commander.js

repository·master·Indexed 12 days ago

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

A complete solution for building command-line interfaces in Node.js. Version 15.0.0 handles argument parsing, option management, subcommand routing, and automatic help generation. It supports synchronous and asynchronous action handlers via .parse() and .parseAsync(), variadic options, custom argument processing, and lifecycle hooks.

Tokens
27.4K
Snippets
108
Records
124
Agent score
92%

What's inside Commander.js

  1. Understand the Commander.js parsing lifecycle

    master

    Commander.js processes arguments through a hierarchical lifecycle. When a command is executed, it parses and removes the options it recognizes from the argument array, then passes the remaining arguments to the next subcommand. This continues until a leaf (final) command is reached.

    Lifecycle for a command (including the top-level program):

    1. parse options: Recognizes and removes options belonging to the current command from the arguments.
    2. parse env: Checks for and applies values from environment variables configured for the command.
    3. process implied: Sets any implied option values.
    4. Subcommand delegation: If the first remaining argument is a subcommand, the command triggers preSubcommand hooks and passes the remaining arguments to that subcommand to repeat the process.

    Lifecycle for the final (leaf) command:

    Once the traversal reaches the final command, the following steps occur in order:

    1. Validation: Checks for missing mandatory options, conflicting options, and unknown options.
    2. Argument processing: Processes any remaining arguments as command-arguments.
    3. Hooks & Execution:
      • Call preAction hooks.
      • Call the action handler.
      • Call postAction hooks.
  2. Understand Commander.js command line terminology

    master

    To use Commander.js effectively, it is important to distinguish between the different types of arguments used in a command line interface:

    • option: An argument starting with - (short form, e.g., -s) or -- (long form, e.g., --short). In some contexts, these are referred to as flags.
    • option-argument: A value provided specifically for an option (e.g., in --file <path>, <path> is the option-argument).
    • command: A specific action or program execution. Commands can have subcommands.
    • command-argument: An argument intended for the command itself, rather than an option. These are also known as positional arguments or operands.

    Example structure: my-utility command -o --option option-argument command-argument-1 command-argument-2

    my-utility command -o --option option-argument command-argument-1 command-argument-2
  3. Configure stand-alone executable subcommands

    master

    When a command is defined with a description as the second argument to .command(), Commander treats it as a stand-alone executable.

    How it works: Commander searches the directory of the entry script for a file named command-subcommand (e.g., pm install looks for pm-install). It supports common extensions like .js.

    Key Configuration:

    • executableFile: Specify a custom name or path for the executable.
    • executableDir(): Specify a custom search directory for subcommands.

    Important: In this mode, the executable handles its own options; they are not declared in the main program.

    program
      .name('pm')
      .command('install [package-names...]', 'install packages')
      .command('update', 'update packages', { executableFile: 'myUpdateSubCommand' })
      .command('list', 'list packages', { isDefault: true });
    
    program.parse(process.argv);
  4. Customize help text with stringify and style routines

    master

    The Help class uses two types of routines to generate output:

    1. Stringify routines: These take a Command, Option, or Argument object and return a string. Use these to change how terms (like subcommand usage) are represented.
    2. Style routines: These take a string and return a styled version (e.g., adding colors or bold text).

    Commander.js automatically handles color detection and respects environment variables like NO_COLOR, FORCE_COLOR, and CLIFORCE_COLOR. If you use custom styles, Commander will strip colors if the output destination does not support them using Command.configureOutput().stripColor().

    Example of styling titles using Node's util.styleText:

    import { styleText } from 'node:util';
    program.configureHelp({
       styleTitle: (str) => styleText('bold', str)
    });
  5. Common option types: Boolean and Value

    master

    Commander supports two primary option types:

    1. Boolean options: Defined without an argument placeholder. They are true if present and undefined if not.
    2. Value options: Defined with angle brackets <value> (e.g., --expect <value>). These are greedy and consume the next argument. They are undefined if not specified.

    Multiple boolean short options can be combined (e.g., -ds instead of -d -s).

    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 run with: pizza-options -d -s -p vegetarian
    // options is: { debug: true, small: true, pizzaType: 'vegetarian' }
  6. Handle parsing ambiguity with varying option-arguments

    master

    When a command has both command-arguments and options that take a varying number of arguments (e.g., [arg] or <arg...>), Commander prioritizes option-arguments. This can cause command-arguments to be incorrectly consumed as option-arguments.

    To resolve this ambiguity, you can use one of the following strategies:

    1. Use the -- delimiter: Instruct users to use -- to signal the end of options. Everything following -- will be treated as command-arguments.
    2. Change the usage pattern: Update your program.usage() to suggest putting options last (e.g., [technique] [options]), which prevents command-arguments from being confused with option-arguments.
    3. Use options instead of arguments: Convert command-arguments into named options (e.g., --technique <name>) to completely eliminate ambiguity.
    // Example of ambiguity
    program
      .name('cook')
      .argument('[technique]')
      .option('-i, --ingredient [ingredient]', 'add cheese or given ingredient')
      .action((technique, options) => {
        // If user runs: cook -i scrambled
        // 'scrambled' is consumed as the ingredient, leaving technique undefined
      });
    
    // Resolution 1: Using --
    // $ cook -i -- scrambled
    // technique: scrambled, ingredient: cheese
    
    // Resolution 2: Put options last in usage
    program.usage('[technique] [options]');
    // $ cook scrambled -i
    
    // Resolution 3: Use options for everything
    program
      .option('-t, --technique <technique>', 'cooking technique')
      .option('-i, --ingredient [ingredient]', 'add cheese or given ingredient');
  7. Organize help output into groups

    master

    By default, options are listed under Options: and commands under Commands:. You can create custom headings using two methods:

    1. High-level: Use .optionsGroup(heading) and .commandsGroup(heading) when adding options or commands.
    2. Low-level: Use .helpGroup(heading) on an individual Option or Command instance.
  8. Configure independent executable subcommands

    master

    When a .command() is provided with a description as the second argument, Commander treats it as an external executable.

    Key behaviors:

    • Search Pattern: Commander looks for a file named {programName}-{commandName} (e.g., pm-install) in the directory of the entry script.
    • Customization:
      • Use { executableFile: 'path/to/file' } in the command options to specify a custom path/name.
      • Use .executableDir('/path/to/dir') to change the search directory for subcommands.
    • Permissions: If the command is intended for global installation, ensure the executable has appropriate permissions (e.g., 755).
    program
      .command('update', 'update packages', { executableFile: 'myUpdateSubCommand' })
      .command('list', 'list packages', { isDefault: true });
    
    program.parse(process.argv);
  9. Declare the program variable

    master

    Commander provides two ways to initialize your CLI program:

    1. Using the global program object

    For simple scripts, you can import the pre-instantiated program object directly. This is the easiest way to get started.

    // CommonJS
    const { program } = require('commander');

    2. Creating a local Command instance

    For complex applications or when you need to perform unit testing, it is recommended to create your own Command instance. This provides better isolation.

    // CommonJS
    const { Command } = require('commander');
    const program = new Command();
    
    // ECMAScript Modules (.mjs)
    import { Command } from 'commander';
    const program = new Command();
    
    // TypeScript (.ts)
    import { Command } from 'commander';
    const program = new Command();
  10. Import Command correctly from 'commander'

    master

    Avoid importing from commander/esm.mjs. For both CommonJS and ESM, import directly from the main commander module.

    Also, avoid relying on the default import of a global Command object. Instead, explicitly import Command or the program instance.

    // Recommended (ESM or CJS)
    import { Command } from 'commander';
    // or
    const { Command } = require('commander');
    
    const program = new Command();
  11. Use extra-typings for TypeScript support

    master

    To get strong typing for the options returned by .opts() and the parameters passed to .action(), use the @commander-js/extra-typings package.

    import { Command } from '@commander-js/extra-typings';