command.js (70947B)
1 const EventEmitter = require('events').EventEmitter; 2 const childProcess = require('child_process'); 3 const path = require('path'); 4 const fs = require('fs'); 5 const process = require('process'); 6 7 const { Argument, humanReadableArgName } = require('./argument.js'); 8 const { CommanderError } = require('./error.js'); 9 const { Help } = require('./help.js'); 10 const { Option, splitOptionFlags, DualOptions } = require('./option.js'); 11 const { suggestSimilar } = require('./suggestSimilar'); 12 13 class Command extends EventEmitter { 14 /** 15 * Initialize a new `Command`. 16 * 17 * @param {string} [name] 18 */ 19 20 constructor(name) { 21 super(); 22 /** @type {Command[]} */ 23 this.commands = []; 24 /** @type {Option[]} */ 25 this.options = []; 26 this.parent = null; 27 this._allowUnknownOption = false; 28 this._allowExcessArguments = true; 29 /** @type {Argument[]} */ 30 this.registeredArguments = []; 31 this._args = this.registeredArguments; // deprecated old name 32 /** @type {string[]} */ 33 this.args = []; // cli args with options removed 34 this.rawArgs = []; 35 this.processedArgs = []; // like .args but after custom processing and collecting variadic 36 this._scriptPath = null; 37 this._name = name || ''; 38 this._optionValues = {}; 39 this._optionValueSources = {}; // default, env, cli etc 40 this._storeOptionsAsProperties = false; 41 this._actionHandler = null; 42 this._executableHandler = false; 43 this._executableFile = null; // custom name for executable 44 this._executableDir = null; // custom search directory for subcommands 45 this._defaultCommandName = null; 46 this._exitCallback = null; 47 this._aliases = []; 48 this._combineFlagAndOptionalValue = true; 49 this._description = ''; 50 this._summary = ''; 51 this._argsDescription = undefined; // legacy 52 this._enablePositionalOptions = false; 53 this._passThroughOptions = false; 54 this._lifeCycleHooks = {}; // a hash of arrays 55 /** @type {boolean | string} */ 56 this._showHelpAfterError = false; 57 this._showSuggestionAfterError = true; 58 59 // see .configureOutput() for docs 60 this._outputConfiguration = { 61 writeOut: (str) => process.stdout.write(str), 62 writeErr: (str) => process.stderr.write(str), 63 getOutHelpWidth: () => process.stdout.isTTY ? process.stdout.columns : undefined, 64 getErrHelpWidth: () => process.stderr.isTTY ? process.stderr.columns : undefined, 65 outputError: (str, write) => write(str) 66 }; 67 68 this._hidden = false; 69 this._hasHelpOption = true; 70 this._helpFlags = '-h, --help'; 71 this._helpDescription = 'display help for command'; 72 this._helpShortFlag = '-h'; 73 this._helpLongFlag = '--help'; 74 this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false 75 this._helpCommandName = 'help'; 76 this._helpCommandnameAndArgs = 'help [command]'; 77 this._helpCommandDescription = 'display help for command'; 78 this._helpConfiguration = {}; 79 } 80 81 /** 82 * Copy settings that are useful to have in common across root command and subcommands. 83 * 84 * (Used internally when adding a command using `.command()` so subcommands inherit parent settings.) 85 * 86 * @param {Command} sourceCommand 87 * @return {Command} `this` command for chaining 88 */ 89 copyInheritedSettings(sourceCommand) { 90 this._outputConfiguration = sourceCommand._outputConfiguration; 91 this._hasHelpOption = sourceCommand._hasHelpOption; 92 this._helpFlags = sourceCommand._helpFlags; 93 this._helpDescription = sourceCommand._helpDescription; 94 this._helpShortFlag = sourceCommand._helpShortFlag; 95 this._helpLongFlag = sourceCommand._helpLongFlag; 96 this._helpCommandName = sourceCommand._helpCommandName; 97 this._helpCommandnameAndArgs = sourceCommand._helpCommandnameAndArgs; 98 this._helpCommandDescription = sourceCommand._helpCommandDescription; 99 this._helpConfiguration = sourceCommand._helpConfiguration; 100 this._exitCallback = sourceCommand._exitCallback; 101 this._storeOptionsAsProperties = sourceCommand._storeOptionsAsProperties; 102 this._combineFlagAndOptionalValue = sourceCommand._combineFlagAndOptionalValue; 103 this._allowExcessArguments = sourceCommand._allowExcessArguments; 104 this._enablePositionalOptions = sourceCommand._enablePositionalOptions; 105 this._showHelpAfterError = sourceCommand._showHelpAfterError; 106 this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError; 107 108 return this; 109 } 110 111 /** 112 * @returns {Command[]} 113 * @api private 114 */ 115 116 _getCommandAndAncestors() { 117 const result = []; 118 for (let command = this; command; command = command.parent) { 119 result.push(command); 120 } 121 return result; 122 } 123 124 /** 125 * Define a command. 126 * 127 * There are two styles of command: pay attention to where to put the description. 128 * 129 * @example 130 * // Command implemented using action handler (description is supplied separately to `.command`) 131 * program 132 * .command('clone <source> [destination]') 133 * .description('clone a repository into a newly created directory') 134 * .action((source, destination) => { 135 * console.log('clone command called'); 136 * }); 137 * 138 * // Command implemented using separate executable file (description is second parameter to `.command`) 139 * program 140 * .command('start <service>', 'start named service') 141 * .command('stop [service]', 'stop named service, or all if no name supplied'); 142 * 143 * @param {string} nameAndArgs - command name and arguments, args are `<required>` or `[optional]` and last may also be `variadic...` 144 * @param {Object|string} [actionOptsOrExecDesc] - configuration options (for action), or description (for executable) 145 * @param {Object} [execOpts] - configuration options (for executable) 146 * @return {Command} returns new command for action handler, or `this` for executable command 147 */ 148 149 command(nameAndArgs, actionOptsOrExecDesc, execOpts) { 150 let desc = actionOptsOrExecDesc; 151 let opts = execOpts; 152 if (typeof desc === 'object' && desc !== null) { 153 opts = desc; 154 desc = null; 155 } 156 opts = opts || {}; 157 const [, name, args] = nameAndArgs.match(/([^ ]+) *(.*)/); 158 159 const cmd = this.createCommand(name); 160 if (desc) { 161 cmd.description(desc); 162 cmd._executableHandler = true; 163 } 164 if (opts.isDefault) this._defaultCommandName = cmd._name; 165 cmd._hidden = !!(opts.noHelp || opts.hidden); // noHelp is deprecated old name for hidden 166 cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor 167 if (args) cmd.arguments(args); 168 this.commands.push(cmd); 169 cmd.parent = this; 170 cmd.copyInheritedSettings(this); 171 172 if (desc) return this; 173 return cmd; 174 } 175 176 /** 177 * Factory routine to create a new unattached command. 178 * 179 * See .command() for creating an attached subcommand, which uses this routine to 180 * create the command. You can override createCommand to customise subcommands. 181 * 182 * @param {string} [name] 183 * @return {Command} new command 184 */ 185 186 createCommand(name) { 187 return new Command(name); 188 } 189 190 /** 191 * You can customise the help with a subclass of Help by overriding createHelp, 192 * or by overriding Help properties using configureHelp(). 193 * 194 * @return {Help} 195 */ 196 197 createHelp() { 198 return Object.assign(new Help(), this.configureHelp()); 199 } 200 201 /** 202 * You can customise the help by overriding Help properties using configureHelp(), 203 * or with a subclass of Help by overriding createHelp(). 204 * 205 * @param {Object} [configuration] - configuration options 206 * @return {Command|Object} `this` command for chaining, or stored configuration 207 */ 208 209 configureHelp(configuration) { 210 if (configuration === undefined) return this._helpConfiguration; 211 212 this._helpConfiguration = configuration; 213 return this; 214 } 215 216 /** 217 * The default output goes to stdout and stderr. You can customise this for special 218 * applications. You can also customise the display of errors by overriding outputError. 219 * 220 * The configuration properties are all functions: 221 * 222 * // functions to change where being written, stdout and stderr 223 * writeOut(str) 224 * writeErr(str) 225 * // matching functions to specify width for wrapping help 226 * getOutHelpWidth() 227 * getErrHelpWidth() 228 * // functions based on what is being written out 229 * outputError(str, write) // used for displaying errors, and not used for displaying help 230 * 231 * @param {Object} [configuration] - configuration options 232 * @return {Command|Object} `this` command for chaining, or stored configuration 233 */ 234 235 configureOutput(configuration) { 236 if (configuration === undefined) return this._outputConfiguration; 237 238 Object.assign(this._outputConfiguration, configuration); 239 return this; 240 } 241 242 /** 243 * Display the help or a custom message after an error occurs. 244 * 245 * @param {boolean|string} [displayHelp] 246 * @return {Command} `this` command for chaining 247 */ 248 showHelpAfterError(displayHelp = true) { 249 if (typeof displayHelp !== 'string') displayHelp = !!displayHelp; 250 this._showHelpAfterError = displayHelp; 251 return this; 252 } 253 254 /** 255 * Display suggestion of similar commands for unknown commands, or options for unknown options. 256 * 257 * @param {boolean} [displaySuggestion] 258 * @return {Command} `this` command for chaining 259 */ 260 showSuggestionAfterError(displaySuggestion = true) { 261 this._showSuggestionAfterError = !!displaySuggestion; 262 return this; 263 } 264 265 /** 266 * Add a prepared subcommand. 267 * 268 * See .command() for creating an attached subcommand which inherits settings from its parent. 269 * 270 * @param {Command} cmd - new subcommand 271 * @param {Object} [opts] - configuration options 272 * @return {Command} `this` command for chaining 273 */ 274 275 addCommand(cmd, opts) { 276 if (!cmd._name) { 277 throw new Error(`Command passed to .addCommand() must have a name 278 - specify the name in Command constructor or using .name()`); 279 } 280 281 opts = opts || {}; 282 if (opts.isDefault) this._defaultCommandName = cmd._name; 283 if (opts.noHelp || opts.hidden) cmd._hidden = true; // modifying passed command due to existing implementation 284 285 this.commands.push(cmd); 286 cmd.parent = this; 287 return this; 288 } 289 290 /** 291 * Factory routine to create a new unattached argument. 292 * 293 * See .argument() for creating an attached argument, which uses this routine to 294 * create the argument. You can override createArgument to return a custom argument. 295 * 296 * @param {string} name 297 * @param {string} [description] 298 * @return {Argument} new argument 299 */ 300 301 createArgument(name, description) { 302 return new Argument(name, description); 303 } 304 305 /** 306 * Define argument syntax for command. 307 * 308 * The default is that the argument is required, and you can explicitly 309 * indicate this with <> around the name. Put [] around the name for an optional argument. 310 * 311 * @example 312 * program.argument('<input-file>'); 313 * program.argument('[output-file]'); 314 * 315 * @param {string} name 316 * @param {string} [description] 317 * @param {Function|*} [fn] - custom argument processing function 318 * @param {*} [defaultValue] 319 * @return {Command} `this` command for chaining 320 */ 321 argument(name, description, fn, defaultValue) { 322 const argument = this.createArgument(name, description); 323 if (typeof fn === 'function') { 324 argument.default(defaultValue).argParser(fn); 325 } else { 326 argument.default(fn); 327 } 328 this.addArgument(argument); 329 return this; 330 } 331 332 /** 333 * Define argument syntax for command, adding multiple at once (without descriptions). 334 * 335 * See also .argument(). 336 * 337 * @example 338 * program.arguments('<cmd> [env]'); 339 * 340 * @param {string} names 341 * @return {Command} `this` command for chaining 342 */ 343 344 arguments(names) { 345 names.trim().split(/ +/).forEach((detail) => { 346 this.argument(detail); 347 }); 348 return this; 349 } 350 351 /** 352 * Define argument syntax for command, adding a prepared argument. 353 * 354 * @param {Argument} argument 355 * @return {Command} `this` command for chaining 356 */ 357 addArgument(argument) { 358 const previousArgument = this.registeredArguments.slice(-1)[0]; 359 if (previousArgument && previousArgument.variadic) { 360 throw new Error(`only the last argument can be variadic '${previousArgument.name()}'`); 361 } 362 if (argument.required && argument.defaultValue !== undefined && argument.parseArg === undefined) { 363 throw new Error(`a default value for a required argument is never used: '${argument.name()}'`); 364 } 365 this.registeredArguments.push(argument); 366 return this; 367 } 368 369 /** 370 * Override default decision whether to add implicit help command. 371 * 372 * addHelpCommand() // force on 373 * addHelpCommand(false); // force off 374 * addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom details 375 * 376 * @return {Command} `this` command for chaining 377 */ 378 379 addHelpCommand(enableOrNameAndArgs, description) { 380 if (enableOrNameAndArgs === false) { 381 this._addImplicitHelpCommand = false; 382 } else { 383 this._addImplicitHelpCommand = true; 384 if (typeof enableOrNameAndArgs === 'string') { 385 this._helpCommandName = enableOrNameAndArgs.split(' ')[0]; 386 this._helpCommandnameAndArgs = enableOrNameAndArgs; 387 } 388 this._helpCommandDescription = description || this._helpCommandDescription; 389 } 390 return this; 391 } 392 393 /** 394 * @return {boolean} 395 * @api private 396 */ 397 398 _hasImplicitHelpCommand() { 399 if (this._addImplicitHelpCommand === undefined) { 400 return this.commands.length && !this._actionHandler && !this._findCommand('help'); 401 } 402 return this._addImplicitHelpCommand; 403 } 404 405 /** 406 * Add hook for life cycle event. 407 * 408 * @param {string} event 409 * @param {Function} listener 410 * @return {Command} `this` command for chaining 411 */ 412 413 hook(event, listener) { 414 const allowedValues = ['preSubcommand', 'preAction', 'postAction']; 415 if (!allowedValues.includes(event)) { 416 throw new Error(`Unexpected value for event passed to hook : '${event}'. 417 Expecting one of '${allowedValues.join("', '")}'`); 418 } 419 if (this._lifeCycleHooks[event]) { 420 this._lifeCycleHooks[event].push(listener); 421 } else { 422 this._lifeCycleHooks[event] = [listener]; 423 } 424 return this; 425 } 426 427 /** 428 * Register callback to use as replacement for calling process.exit. 429 * 430 * @param {Function} [fn] optional callback which will be passed a CommanderError, defaults to throwing 431 * @return {Command} `this` command for chaining 432 */ 433 434 exitOverride(fn) { 435 if (fn) { 436 this._exitCallback = fn; 437 } else { 438 this._exitCallback = (err) => { 439 if (err.code !== 'commander.executeSubCommandAsync') { 440 throw err; 441 } else { 442 // Async callback from spawn events, not useful to throw. 443 } 444 }; 445 } 446 return this; 447 } 448 449 /** 450 * Call process.exit, and _exitCallback if defined. 451 * 452 * @param {number} exitCode exit code for using with process.exit 453 * @param {string} code an id string representing the error 454 * @param {string} message human-readable description of the error 455 * @return never 456 * @api private 457 */ 458 459 _exit(exitCode, code, message) { 460 if (this._exitCallback) { 461 this._exitCallback(new CommanderError(exitCode, code, message)); 462 // Expecting this line is not reached. 463 } 464 process.exit(exitCode); 465 } 466 467 /** 468 * Register callback `fn` for the command. 469 * 470 * @example 471 * program 472 * .command('serve') 473 * .description('start service') 474 * .action(function() { 475 * // do work here 476 * }); 477 * 478 * @param {Function} fn 479 * @return {Command} `this` command for chaining 480 */ 481 482 action(fn) { 483 const listener = (args) => { 484 // The .action callback takes an extra parameter which is the command or options. 485 const expectedArgsCount = this.registeredArguments.length; 486 const actionArgs = args.slice(0, expectedArgsCount); 487 if (this._storeOptionsAsProperties) { 488 actionArgs[expectedArgsCount] = this; // backwards compatible "options" 489 } else { 490 actionArgs[expectedArgsCount] = this.opts(); 491 } 492 actionArgs.push(this); 493 494 return fn.apply(this, actionArgs); 495 }; 496 this._actionHandler = listener; 497 return this; 498 } 499 500 /** 501 * Factory routine to create a new unattached option. 502 * 503 * See .option() for creating an attached option, which uses this routine to 504 * create the option. You can override createOption to return a custom option. 505 * 506 * @param {string} flags 507 * @param {string} [description] 508 * @return {Option} new option 509 */ 510 511 createOption(flags, description) { 512 return new Option(flags, description); 513 } 514 515 /** 516 * Wrap parseArgs to catch 'commander.invalidArgument'. 517 * 518 * @param {Option | Argument} target 519 * @param {string} value 520 * @param {*} previous 521 * @param {string} invalidArgumentMessage 522 * @api private 523 */ 524 525 _callParseArg(target, value, previous, invalidArgumentMessage) { 526 try { 527 return target.parseArg(value, previous); 528 } catch (err) { 529 if (err.code === 'commander.invalidArgument') { 530 const message = `${invalidArgumentMessage} ${err.message}`; 531 this.error(message, { exitCode: err.exitCode, code: err.code }); 532 } 533 throw err; 534 } 535 } 536 537 /** 538 * Add an option. 539 * 540 * @param {Option} option 541 * @return {Command} `this` command for chaining 542 */ 543 addOption(option) { 544 const oname = option.name(); 545 const name = option.attributeName(); 546 547 // store default value 548 if (option.negate) { 549 // --no-foo is special and defaults foo to true, unless a --foo option is already defined 550 const positiveLongFlag = option.long.replace(/^--no-/, '--'); 551 if (!this._findOption(positiveLongFlag)) { 552 this.setOptionValueWithSource(name, option.defaultValue === undefined ? true : option.defaultValue, 'default'); 553 } 554 } else if (option.defaultValue !== undefined) { 555 this.setOptionValueWithSource(name, option.defaultValue, 'default'); 556 } 557 558 // register the option 559 this.options.push(option); 560 561 // handler for cli and env supplied values 562 const handleOptionValue = (val, invalidValueMessage, valueSource) => { 563 // val is null for optional option used without an optional-argument. 564 // val is undefined for boolean and negated option. 565 if (val == null && option.presetArg !== undefined) { 566 val = option.presetArg; 567 } 568 569 // custom processing 570 const oldValue = this.getOptionValue(name); 571 if (val !== null && option.parseArg) { 572 val = this._callParseArg(option, val, oldValue, invalidValueMessage); 573 } else if (val !== null && option.variadic) { 574 val = option._concatValue(val, oldValue); 575 } 576 577 // Fill-in appropriate missing values. Long winded but easy to follow. 578 if (val == null) { 579 if (option.negate) { 580 val = false; 581 } else if (option.isBoolean() || option.optional) { 582 val = true; 583 } else { 584 val = ''; // not normal, parseArg might have failed or be a mock function for testing 585 } 586 } 587 this.setOptionValueWithSource(name, val, valueSource); 588 }; 589 590 this.on('option:' + oname, (val) => { 591 const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`; 592 handleOptionValue(val, invalidValueMessage, 'cli'); 593 }); 594 595 if (option.envVar) { 596 this.on('optionEnv:' + oname, (val) => { 597 const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`; 598 handleOptionValue(val, invalidValueMessage, 'env'); 599 }); 600 } 601 602 return this; 603 } 604 605 /** 606 * Internal implementation shared by .option() and .requiredOption() 607 * 608 * @api private 609 */ 610 _optionEx(config, flags, description, fn, defaultValue) { 611 if (typeof flags === 'object' && flags instanceof Option) { 612 throw new Error('To add an Option object use addOption() instead of option() or requiredOption()'); 613 } 614 const option = this.createOption(flags, description); 615 option.makeOptionMandatory(!!config.mandatory); 616 if (typeof fn === 'function') { 617 option.default(defaultValue).argParser(fn); 618 } else if (fn instanceof RegExp) { 619 // deprecated 620 const regex = fn; 621 fn = (val, def) => { 622 const m = regex.exec(val); 623 return m ? m[0] : def; 624 }; 625 option.default(defaultValue).argParser(fn); 626 } else { 627 option.default(fn); 628 } 629 630 return this.addOption(option); 631 } 632 633 /** 634 * Define option with `flags`, `description`, and optional argument parsing function or `defaultValue` or both. 635 * 636 * The `flags` string contains the short and/or long flags, separated by comma, a pipe or space. A required 637 * option-argument is indicated by `<>` and an optional option-argument by `[]`. 638 * 639 * See the README for more details, and see also addOption() and requiredOption(). 640 * 641 * @example 642 * program 643 * .option('-p, --pepper', 'add pepper') 644 * .option('-p, --pizza-type <TYPE>', 'type of pizza') // required option-argument 645 * .option('-c, --cheese [CHEESE]', 'add extra cheese', 'mozzarella') // optional option-argument with default 646 * .option('-t, --tip <VALUE>', 'add tip to purchase cost', parseFloat) // custom parse function 647 * 648 * @param {string} flags 649 * @param {string} [description] 650 * @param {Function|*} [parseArg] - custom option processing function or default value 651 * @param {*} [defaultValue] 652 * @return {Command} `this` command for chaining 653 */ 654 655 option(flags, description, parseArg, defaultValue) { 656 return this._optionEx({}, flags, description, parseArg, defaultValue); 657 } 658 659 /** 660 * Add a required option which must have a value after parsing. This usually means 661 * the option must be specified on the command line. (Otherwise the same as .option().) 662 * 663 * The `flags` string contains the short and/or long flags, separated by comma, a pipe or space. 664 * 665 * @param {string} flags 666 * @param {string} [description] 667 * @param {Function|*} [parseArg] - custom option processing function or default value 668 * @param {*} [defaultValue] 669 * @return {Command} `this` command for chaining 670 */ 671 672 requiredOption(flags, description, parseArg, defaultValue) { 673 return this._optionEx({ mandatory: true }, flags, description, parseArg, defaultValue); 674 } 675 676 /** 677 * Alter parsing of short flags with optional values. 678 * 679 * @example 680 * // for `.option('-f,--flag [value]'): 681 * program.combineFlagAndOptionalValue(true); // `-f80` is treated like `--flag=80`, this is the default behaviour 682 * program.combineFlagAndOptionalValue(false) // `-fb` is treated like `-f -b` 683 * 684 * @param {Boolean} [combine=true] - if `true` or omitted, an optional value can be specified directly after the flag. 685 */ 686 combineFlagAndOptionalValue(combine = true) { 687 this._combineFlagAndOptionalValue = !!combine; 688 return this; 689 } 690 691 /** 692 * Allow unknown options on the command line. 693 * 694 * @param {Boolean} [allowUnknown=true] - if `true` or omitted, no error will be thrown 695 * for unknown options. 696 */ 697 allowUnknownOption(allowUnknown = true) { 698 this._allowUnknownOption = !!allowUnknown; 699 return this; 700 } 701 702 /** 703 * Allow excess command-arguments on the command line. Pass false to make excess arguments an error. 704 * 705 * @param {Boolean} [allowExcess=true] - if `true` or omitted, no error will be thrown 706 * for excess arguments. 707 */ 708 allowExcessArguments(allowExcess = true) { 709 this._allowExcessArguments = !!allowExcess; 710 return this; 711 } 712 713 /** 714 * Enable positional options. Positional means global options are specified before subcommands which lets 715 * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions. 716 * The default behaviour is non-positional and global options may appear anywhere on the command line. 717 * 718 * @param {Boolean} [positional=true] 719 */ 720 enablePositionalOptions(positional = true) { 721 this._enablePositionalOptions = !!positional; 722 return this; 723 } 724 725 /** 726 * Pass through options that come after command-arguments rather than treat them as command-options, 727 * so actual command-options come before command-arguments. Turning this on for a subcommand requires 728 * positional options to have been enabled on the program (parent commands). 729 * The default behaviour is non-positional and options may appear before or after command-arguments. 730 * 731 * @param {Boolean} [passThrough=true] 732 * for unknown options. 733 */ 734 passThroughOptions(passThrough = true) { 735 this._passThroughOptions = !!passThrough; 736 if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) { 737 throw new Error('passThroughOptions can not be used without turning on enablePositionalOptions for parent command(s)'); 738 } 739 return this; 740 } 741 742 /** 743 * Whether to store option values as properties on command object, 744 * or store separately (specify false). In both cases the option values can be accessed using .opts(). 745 * 746 * @param {boolean} [storeAsProperties=true] 747 * @return {Command} `this` command for chaining 748 */ 749 750 storeOptionsAsProperties(storeAsProperties = true) { 751 if (this.options.length) { 752 throw new Error('call .storeOptionsAsProperties() before adding options'); 753 } 754 // if (Object.keys(this._optionValues).length) { 755 // throw new Error('call .storeOptionsAsProperties() before setting option values'); 756 // } 757 this._storeOptionsAsProperties = !!storeAsProperties; 758 return this; 759 } 760 761 /** 762 * Retrieve option value. 763 * 764 * @param {string} key 765 * @return {Object} value 766 */ 767 768 getOptionValue(key) { 769 if (this._storeOptionsAsProperties) { 770 return this[key]; 771 } 772 return this._optionValues[key]; 773 } 774 775 /** 776 * Store option value. 777 * 778 * @param {string} key 779 * @param {Object} value 780 * @return {Command} `this` command for chaining 781 */ 782 783 setOptionValue(key, value) { 784 return this.setOptionValueWithSource(key, value, undefined); 785 } 786 787 /** 788 * Store option value and where the value came from. 789 * 790 * @param {string} key 791 * @param {Object} value 792 * @param {string} source - expected values are default/config/env/cli/implied 793 * @return {Command} `this` command for chaining 794 */ 795 796 setOptionValueWithSource(key, value, source) { 797 if (this._storeOptionsAsProperties) { 798 this[key] = value; 799 } else { 800 this._optionValues[key] = value; 801 } 802 this._optionValueSources[key] = source; 803 return this; 804 } 805 806 /** 807 * Get source of option value. 808 * Expected values are default | config | env | cli | implied 809 * 810 * @param {string} key 811 * @return {string} 812 */ 813 814 getOptionValueSource(key) { 815 return this._optionValueSources[key]; 816 } 817 818 /** 819 * Get source of option value. See also .optsWithGlobals(). 820 * Expected values are default | config | env | cli | implied 821 * 822 * @param {string} key 823 * @return {string} 824 */ 825 826 getOptionValueSourceWithGlobals(key) { 827 // global overwrites local, like optsWithGlobals 828 let source; 829 this._getCommandAndAncestors().forEach((cmd) => { 830 if (cmd.getOptionValueSource(key) !== undefined) { 831 source = cmd.getOptionValueSource(key); 832 } 833 }); 834 return source; 835 } 836 837 /** 838 * Get user arguments from implied or explicit arguments. 839 * Side-effects: set _scriptPath if args included script. Used for default program name, and subcommand searches. 840 * 841 * @api private 842 */ 843 844 _prepareUserArgs(argv, parseOptions) { 845 if (argv !== undefined && !Array.isArray(argv)) { 846 throw new Error('first parameter to parse must be array or undefined'); 847 } 848 parseOptions = parseOptions || {}; 849 850 // Default to using process.argv 851 if (argv === undefined) { 852 argv = process.argv; 853 // @ts-ignore: unknown property 854 if (process.versions && process.versions.electron) { 855 parseOptions.from = 'electron'; 856 } 857 } 858 this.rawArgs = argv.slice(); 859 860 // make it a little easier for callers by supporting various argv conventions 861 let userArgs; 862 switch (parseOptions.from) { 863 case undefined: 864 case 'node': 865 this._scriptPath = argv[1]; 866 userArgs = argv.slice(2); 867 break; 868 case 'electron': 869 // @ts-ignore: unknown property 870 if (process.defaultApp) { 871 this._scriptPath = argv[1]; 872 userArgs = argv.slice(2); 873 } else { 874 userArgs = argv.slice(1); 875 } 876 break; 877 case 'user': 878 userArgs = argv.slice(0); 879 break; 880 default: 881 throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`); 882 } 883 884 // Find default name for program from arguments. 885 if (!this._name && this._scriptPath) this.nameFromFilename(this._scriptPath); 886 this._name = this._name || 'program'; 887 888 return userArgs; 889 } 890 891 /** 892 * Parse `argv`, setting options and invoking commands when defined. 893 * 894 * The default expectation is that the arguments are from node and have the application as argv[0] 895 * and the script being run in argv[1], with user parameters after that. 896 * 897 * @example 898 * program.parse(process.argv); 899 * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions 900 * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] 901 * 902 * @param {string[]} [argv] - optional, defaults to process.argv 903 * @param {Object} [parseOptions] - optionally specify style of options with from: node/user/electron 904 * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron' 905 * @return {Command} `this` command for chaining 906 */ 907 908 parse(argv, parseOptions) { 909 const userArgs = this._prepareUserArgs(argv, parseOptions); 910 this._parseCommand([], userArgs); 911 912 return this; 913 } 914 915 /** 916 * Parse `argv`, setting options and invoking commands when defined. 917 * 918 * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. 919 * 920 * The default expectation is that the arguments are from node and have the application as argv[0] 921 * and the script being run in argv[1], with user parameters after that. 922 * 923 * @example 924 * await program.parseAsync(process.argv); 925 * await program.parseAsync(); // implicitly use process.argv and auto-detect node vs electron conventions 926 * await program.parseAsync(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] 927 * 928 * @param {string[]} [argv] 929 * @param {Object} [parseOptions] 930 * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' 931 * @return {Promise} 932 */ 933 934 async parseAsync(argv, parseOptions) { 935 const userArgs = this._prepareUserArgs(argv, parseOptions); 936 await this._parseCommand([], userArgs); 937 938 return this; 939 } 940 941 /** 942 * Execute a sub-command executable. 943 * 944 * @api private 945 */ 946 947 _executeSubCommand(subcommand, args) { 948 args = args.slice(); 949 let launchWithNode = false; // Use node for source targets so do not need to get permissions correct, and on Windows. 950 const sourceExt = ['.js', '.ts', '.tsx', '.mjs', '.cjs']; 951 952 function findFile(baseDir, baseName) { 953 // Look for specified file 954 const localBin = path.resolve(baseDir, baseName); 955 if (fs.existsSync(localBin)) return localBin; 956 957 // Stop looking if candidate already has an expected extension. 958 if (sourceExt.includes(path.extname(baseName))) return undefined; 959 960 // Try all the extensions. 961 const foundExt = sourceExt.find(ext => fs.existsSync(`${localBin}${ext}`)); 962 if (foundExt) return `${localBin}${foundExt}`; 963 964 return undefined; 965 } 966 967 // Not checking for help first. Unlikely to have mandatory and executable, and can't robustly test for help flags in external command. 968 this._checkForMissingMandatoryOptions(); 969 this._checkForConflictingOptions(); 970 971 // executableFile and executableDir might be full path, or just a name 972 let executableFile = subcommand._executableFile || `${this._name}-${subcommand._name}`; 973 let executableDir = this._executableDir || ''; 974 if (this._scriptPath) { 975 let resolvedScriptPath; // resolve possible symlink for installed npm binary 976 try { 977 resolvedScriptPath = fs.realpathSync(this._scriptPath); 978 } catch (err) { 979 resolvedScriptPath = this._scriptPath; 980 } 981 executableDir = path.resolve(path.dirname(resolvedScriptPath), executableDir); 982 } 983 984 // Look for a local file in preference to a command in PATH. 985 if (executableDir) { 986 let localFile = findFile(executableDir, executableFile); 987 988 // Legacy search using prefix of script name instead of command name 989 if (!localFile && !subcommand._executableFile && this._scriptPath) { 990 const legacyName = path.basename(this._scriptPath, path.extname(this._scriptPath)); 991 if (legacyName !== this._name) { 992 localFile = findFile(executableDir, `${legacyName}-${subcommand._name}`); 993 } 994 } 995 executableFile = localFile || executableFile; 996 } 997 998 launchWithNode = sourceExt.includes(path.extname(executableFile)); 999 1000 let proc; 1001 if (process.platform !== 'win32') { 1002 if (launchWithNode) { 1003 args.unshift(executableFile); 1004 // add executable arguments to spawn 1005 args = incrementNodeInspectorPort(process.execArgv).concat(args); 1006 1007 proc = childProcess.spawn(process.argv[0], args, { stdio: 'inherit' }); 1008 } else { 1009 proc = childProcess.spawn(executableFile, args, { stdio: 'inherit' }); 1010 } 1011 } else { 1012 args.unshift(executableFile); 1013 // add executable arguments to spawn 1014 args = incrementNodeInspectorPort(process.execArgv).concat(args); 1015 proc = childProcess.spawn(process.execPath, args, { stdio: 'inherit' }); 1016 } 1017 1018 if (!proc.killed) { // testing mainly to avoid leak warnings during unit tests with mocked spawn 1019 const signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP']; 1020 signals.forEach((signal) => { 1021 // @ts-ignore 1022 process.on(signal, () => { 1023 if (proc.killed === false && proc.exitCode === null) { 1024 proc.kill(signal); 1025 } 1026 }); 1027 }); 1028 } 1029 1030 // By default terminate process when spawned process terminates. 1031 // Suppressing the exit if exitCallback defined is a bit messy and of limited use, but does allow process to stay running! 1032 const exitCallback = this._exitCallback; 1033 if (!exitCallback) { 1034 proc.on('close', process.exit.bind(process)); 1035 } else { 1036 proc.on('close', () => { 1037 exitCallback(new CommanderError(process.exitCode || 0, 'commander.executeSubCommandAsync', '(close)')); 1038 }); 1039 } 1040 proc.on('error', (err) => { 1041 // @ts-ignore 1042 if (err.code === 'ENOENT') { 1043 const executableDirMessage = executableDir 1044 ? `searched for local subcommand relative to directory '${executableDir}'` 1045 : 'no directory for search for local subcommand, use .executableDir() to supply a custom directory'; 1046 const executableMissing = `'${executableFile}' does not exist 1047 - if '${subcommand._name}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead 1048 - if the default executable name is not suitable, use the executableFile option to supply a custom name or path 1049 - ${executableDirMessage}`; 1050 throw new Error(executableMissing); 1051 // @ts-ignore 1052 } else if (err.code === 'EACCES') { 1053 throw new Error(`'${executableFile}' not executable`); 1054 } 1055 if (!exitCallback) { 1056 process.exit(1); 1057 } else { 1058 const wrappedError = new CommanderError(1, 'commander.executeSubCommandAsync', '(error)'); 1059 wrappedError.nestedError = err; 1060 exitCallback(wrappedError); 1061 } 1062 }); 1063 1064 // Store the reference to the child process 1065 this.runningCommand = proc; 1066 } 1067 1068 /** 1069 * @api private 1070 */ 1071 1072 _dispatchSubcommand(commandName, operands, unknown) { 1073 const subCommand = this._findCommand(commandName); 1074 if (!subCommand) this.help({ error: true }); 1075 1076 let promiseChain; 1077 promiseChain = this._chainOrCallSubCommandHook(promiseChain, subCommand, 'preSubcommand'); 1078 promiseChain = this._chainOrCall(promiseChain, () => { 1079 if (subCommand._executableHandler) { 1080 this._executeSubCommand(subCommand, operands.concat(unknown)); 1081 } else { 1082 return subCommand._parseCommand(operands, unknown); 1083 } 1084 }); 1085 return promiseChain; 1086 } 1087 1088 /** 1089 * Invoke help directly if possible, or dispatch if necessary. 1090 * e.g. help foo 1091 * 1092 * @api private 1093 */ 1094 1095 _dispatchHelpCommand(subcommandName) { 1096 if (!subcommandName) { 1097 this.help(); 1098 } 1099 const subCommand = this._findCommand(subcommandName); 1100 if (subCommand && !subCommand._executableHandler) { 1101 subCommand.help(); 1102 } 1103 1104 // Fallback to parsing the help flag to invoke the help. 1105 return this._dispatchSubcommand(subcommandName, [], [ 1106 this._helpLongFlag || this._helpShortFlag 1107 ]); 1108 } 1109 1110 /** 1111 * Check this.args against expected this.registeredArguments. 1112 * 1113 * @api private 1114 */ 1115 1116 _checkNumberOfArguments() { 1117 // too few 1118 this.registeredArguments.forEach((arg, i) => { 1119 if (arg.required && this.args[i] == null) { 1120 this.missingArgument(arg.name()); 1121 } 1122 }); 1123 // too many 1124 if (this.registeredArguments.length > 0 && this.registeredArguments[this.registeredArguments.length - 1].variadic) { 1125 return; 1126 } 1127 if (this.args.length > this.registeredArguments.length) { 1128 this._excessArguments(this.args); 1129 } 1130 } 1131 1132 /** 1133 * Process this.args using this.registeredArguments and save as this.processedArgs! 1134 * 1135 * @api private 1136 */ 1137 1138 _processArguments() { 1139 const myParseArg = (argument, value, previous) => { 1140 // Extra processing for nice error message on parsing failure. 1141 let parsedValue = value; 1142 if (value !== null && argument.parseArg) { 1143 const invalidValueMessage = `error: command-argument value '${value}' is invalid for argument '${argument.name()}'.`; 1144 parsedValue = this._callParseArg(argument, value, previous, invalidValueMessage); 1145 } 1146 return parsedValue; 1147 }; 1148 1149 this._checkNumberOfArguments(); 1150 1151 const processedArgs = []; 1152 this.registeredArguments.forEach((declaredArg, index) => { 1153 let value = declaredArg.defaultValue; 1154 if (declaredArg.variadic) { 1155 // Collect together remaining arguments for passing together as an array. 1156 if (index < this.args.length) { 1157 value = this.args.slice(index); 1158 if (declaredArg.parseArg) { 1159 value = value.reduce((processed, v) => { 1160 return myParseArg(declaredArg, v, processed); 1161 }, declaredArg.defaultValue); 1162 } 1163 } else if (value === undefined) { 1164 value = []; 1165 } 1166 } else if (index < this.args.length) { 1167 value = this.args[index]; 1168 if (declaredArg.parseArg) { 1169 value = myParseArg(declaredArg, value, declaredArg.defaultValue); 1170 } 1171 } 1172 processedArgs[index] = value; 1173 }); 1174 this.processedArgs = processedArgs; 1175 } 1176 1177 /** 1178 * Once we have a promise we chain, but call synchronously until then. 1179 * 1180 * @param {Promise|undefined} promise 1181 * @param {Function} fn 1182 * @return {Promise|undefined} 1183 * @api private 1184 */ 1185 1186 _chainOrCall(promise, fn) { 1187 // thenable 1188 if (promise && promise.then && typeof promise.then === 'function') { 1189 // already have a promise, chain callback 1190 return promise.then(() => fn()); 1191 } 1192 // callback might return a promise 1193 return fn(); 1194 } 1195 1196 /** 1197 * 1198 * @param {Promise|undefined} promise 1199 * @param {string} event 1200 * @return {Promise|undefined} 1201 * @api private 1202 */ 1203 1204 _chainOrCallHooks(promise, event) { 1205 let result = promise; 1206 const hooks = []; 1207 this._getCommandAndAncestors() 1208 .reverse() 1209 .filter(cmd => cmd._lifeCycleHooks[event] !== undefined) 1210 .forEach(hookedCommand => { 1211 hookedCommand._lifeCycleHooks[event].forEach((callback) => { 1212 hooks.push({ hookedCommand, callback }); 1213 }); 1214 }); 1215 if (event === 'postAction') { 1216 hooks.reverse(); 1217 } 1218 1219 hooks.forEach((hookDetail) => { 1220 result = this._chainOrCall(result, () => { 1221 return hookDetail.callback(hookDetail.hookedCommand, this); 1222 }); 1223 }); 1224 return result; 1225 } 1226 1227 /** 1228 * 1229 * @param {Promise|undefined} promise 1230 * @param {Command} subCommand 1231 * @param {string} event 1232 * @return {Promise|undefined} 1233 * @api private 1234 */ 1235 1236 _chainOrCallSubCommandHook(promise, subCommand, event) { 1237 let result = promise; 1238 if (this._lifeCycleHooks[event] !== undefined) { 1239 this._lifeCycleHooks[event].forEach((hook) => { 1240 result = this._chainOrCall(result, () => { 1241 return hook(this, subCommand); 1242 }); 1243 }); 1244 } 1245 return result; 1246 } 1247 1248 /** 1249 * Process arguments in context of this command. 1250 * Returns action result, in case it is a promise. 1251 * 1252 * @api private 1253 */ 1254 1255 _parseCommand(operands, unknown) { 1256 const parsed = this.parseOptions(unknown); 1257 this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env 1258 this._parseOptionsImplied(); 1259 operands = operands.concat(parsed.operands); 1260 unknown = parsed.unknown; 1261 this.args = operands.concat(unknown); 1262 1263 if (operands && this._findCommand(operands[0])) { 1264 return this._dispatchSubcommand(operands[0], operands.slice(1), unknown); 1265 } 1266 if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { 1267 return this._dispatchHelpCommand(operands[1]); 1268 } 1269 if (this._defaultCommandName) { 1270 outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command 1271 return this._dispatchSubcommand(this._defaultCommandName, operands, unknown); 1272 } 1273 if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { 1274 // probably missing subcommand and no handler, user needs help (and exit) 1275 this.help({ error: true }); 1276 } 1277 1278 outputHelpIfRequested(this, parsed.unknown); 1279 this._checkForMissingMandatoryOptions(); 1280 this._checkForConflictingOptions(); 1281 1282 // We do not always call this check to avoid masking a "better" error, like unknown command. 1283 const checkForUnknownOptions = () => { 1284 if (parsed.unknown.length > 0) { 1285 this.unknownOption(parsed.unknown[0]); 1286 } 1287 }; 1288 1289 const commandEvent = `command:${this.name()}`; 1290 if (this._actionHandler) { 1291 checkForUnknownOptions(); 1292 this._processArguments(); 1293 1294 let promiseChain; 1295 promiseChain = this._chainOrCallHooks(promiseChain, 'preAction'); 1296 promiseChain = this._chainOrCall(promiseChain, () => this._actionHandler(this.processedArgs)); 1297 if (this.parent) { 1298 promiseChain = this._chainOrCall(promiseChain, () => { 1299 this.parent.emit(commandEvent, operands, unknown); // legacy 1300 }); 1301 } 1302 promiseChain = this._chainOrCallHooks(promiseChain, 'postAction'); 1303 return promiseChain; 1304 } 1305 if (this.parent && this.parent.listenerCount(commandEvent)) { 1306 checkForUnknownOptions(); 1307 this._processArguments(); 1308 this.parent.emit(commandEvent, operands, unknown); // legacy 1309 } else if (operands.length) { 1310 if (this._findCommand('*')) { // legacy default command 1311 return this._dispatchSubcommand('*', operands, unknown); 1312 } 1313 if (this.listenerCount('command:*')) { 1314 // skip option check, emit event for possible misspelling suggestion 1315 this.emit('command:*', operands, unknown); 1316 } else if (this.commands.length) { 1317 this.unknownCommand(); 1318 } else { 1319 checkForUnknownOptions(); 1320 this._processArguments(); 1321 } 1322 } else if (this.commands.length) { 1323 checkForUnknownOptions(); 1324 // This command has subcommands and nothing hooked up at this level, so display help (and exit). 1325 this.help({ error: true }); 1326 } else { 1327 checkForUnknownOptions(); 1328 this._processArguments(); 1329 // fall through for caller to handle after calling .parse() 1330 } 1331 } 1332 1333 /** 1334 * Find matching command. 1335 * 1336 * @api private 1337 */ 1338 _findCommand(name) { 1339 if (!name) return undefined; 1340 return this.commands.find(cmd => cmd._name === name || cmd._aliases.includes(name)); 1341 } 1342 1343 /** 1344 * Return an option matching `arg` if any. 1345 * 1346 * @param {string} arg 1347 * @return {Option} 1348 * @api private 1349 */ 1350 1351 _findOption(arg) { 1352 return this.options.find(option => option.is(arg)); 1353 } 1354 1355 /** 1356 * Display an error message if a mandatory option does not have a value. 1357 * Called after checking for help flags in leaf subcommand. 1358 * 1359 * @api private 1360 */ 1361 1362 _checkForMissingMandatoryOptions() { 1363 // Walk up hierarchy so can call in subcommand after checking for displaying help. 1364 this._getCommandAndAncestors().forEach((cmd) => { 1365 cmd.options.forEach((anOption) => { 1366 if (anOption.mandatory && (cmd.getOptionValue(anOption.attributeName()) === undefined)) { 1367 cmd.missingMandatoryOptionValue(anOption); 1368 } 1369 }); 1370 }); 1371 } 1372 1373 /** 1374 * Display an error message if conflicting options are used together in this. 1375 * 1376 * @api private 1377 */ 1378 _checkForConflictingLocalOptions() { 1379 const definedNonDefaultOptions = this.options.filter( 1380 (option) => { 1381 const optionKey = option.attributeName(); 1382 if (this.getOptionValue(optionKey) === undefined) { 1383 return false; 1384 } 1385 return this.getOptionValueSource(optionKey) !== 'default'; 1386 } 1387 ); 1388 1389 const optionsWithConflicting = definedNonDefaultOptions.filter( 1390 (option) => option.conflictsWith.length > 0 1391 ); 1392 1393 optionsWithConflicting.forEach((option) => { 1394 const conflictingAndDefined = definedNonDefaultOptions.find((defined) => 1395 option.conflictsWith.includes(defined.attributeName()) 1396 ); 1397 if (conflictingAndDefined) { 1398 this._conflictingOption(option, conflictingAndDefined); 1399 } 1400 }); 1401 } 1402 1403 /** 1404 * Display an error message if conflicting options are used together. 1405 * Called after checking for help flags in leaf subcommand. 1406 * 1407 * @api private 1408 */ 1409 _checkForConflictingOptions() { 1410 // Walk up hierarchy so can call in subcommand after checking for displaying help. 1411 this._getCommandAndAncestors().forEach((cmd) => { 1412 cmd._checkForConflictingLocalOptions(); 1413 }); 1414 } 1415 1416 /** 1417 * Parse options from `argv` removing known options, 1418 * and return argv split into operands and unknown arguments. 1419 * 1420 * Examples: 1421 * 1422 * argv => operands, unknown 1423 * --known kkk op => [op], [] 1424 * op --known kkk => [op], [] 1425 * sub --unknown uuu op => [sub], [--unknown uuu op] 1426 * sub -- --unknown uuu op => [sub --unknown uuu op], [] 1427 * 1428 * @param {String[]} argv 1429 * @return {{operands: String[], unknown: String[]}} 1430 */ 1431 1432 parseOptions(argv) { 1433 const operands = []; // operands, not options or values 1434 const unknown = []; // first unknown option and remaining unknown args 1435 let dest = operands; 1436 const args = argv.slice(); 1437 1438 function maybeOption(arg) { 1439 return arg.length > 1 && arg[0] === '-'; 1440 } 1441 1442 // parse options 1443 let activeVariadicOption = null; 1444 while (args.length) { 1445 const arg = args.shift(); 1446 1447 // literal 1448 if (arg === '--') { 1449 if (dest === unknown) dest.push(arg); 1450 dest.push(...args); 1451 break; 1452 } 1453 1454 if (activeVariadicOption && !maybeOption(arg)) { 1455 this.emit(`option:${activeVariadicOption.name()}`, arg); 1456 continue; 1457 } 1458 activeVariadicOption = null; 1459 1460 if (maybeOption(arg)) { 1461 const option = this._findOption(arg); 1462 // recognised option, call listener to assign value with possible custom processing 1463 if (option) { 1464 if (option.required) { 1465 const value = args.shift(); 1466 if (value === undefined) this.optionMissingArgument(option); 1467 this.emit(`option:${option.name()}`, value); 1468 } else if (option.optional) { 1469 let value = null; 1470 // historical behaviour is optional value is following arg unless an option 1471 if (args.length > 0 && !maybeOption(args[0])) { 1472 value = args.shift(); 1473 } 1474 this.emit(`option:${option.name()}`, value); 1475 } else { // boolean flag 1476 this.emit(`option:${option.name()}`); 1477 } 1478 activeVariadicOption = option.variadic ? option : null; 1479 continue; 1480 } 1481 } 1482 1483 // Look for combo options following single dash, eat first one if known. 1484 if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { 1485 const option = this._findOption(`-${arg[1]}`); 1486 if (option) { 1487 if (option.required || (option.optional && this._combineFlagAndOptionalValue)) { 1488 // option with value following in same argument 1489 this.emit(`option:${option.name()}`, arg.slice(2)); 1490 } else { 1491 // boolean option, emit and put back remainder of arg for further processing 1492 this.emit(`option:${option.name()}`); 1493 args.unshift(`-${arg.slice(2)}`); 1494 } 1495 continue; 1496 } 1497 } 1498 1499 // Look for known long flag with value, like --foo=bar 1500 if (/^--[^=]+=/.test(arg)) { 1501 const index = arg.indexOf('='); 1502 const option = this._findOption(arg.slice(0, index)); 1503 if (option && (option.required || option.optional)) { 1504 this.emit(`option:${option.name()}`, arg.slice(index + 1)); 1505 continue; 1506 } 1507 } 1508 1509 // Not a recognised option by this command. 1510 // Might be a command-argument, or subcommand option, or unknown option, or help command or option. 1511 1512 // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands. 1513 if (maybeOption(arg)) { 1514 dest = unknown; 1515 } 1516 1517 // If using positionalOptions, stop processing our options at subcommand. 1518 if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { 1519 if (this._findCommand(arg)) { 1520 operands.push(arg); 1521 if (args.length > 0) unknown.push(...args); 1522 break; 1523 } else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) { 1524 operands.push(arg); 1525 if (args.length > 0) operands.push(...args); 1526 break; 1527 } else if (this._defaultCommandName) { 1528 unknown.push(arg); 1529 if (args.length > 0) unknown.push(...args); 1530 break; 1531 } 1532 } 1533 1534 // If using passThroughOptions, stop processing options at first command-argument. 1535 if (this._passThroughOptions) { 1536 dest.push(arg); 1537 if (args.length > 0) dest.push(...args); 1538 break; 1539 } 1540 1541 // add arg 1542 dest.push(arg); 1543 } 1544 1545 return { operands, unknown }; 1546 } 1547 1548 /** 1549 * Return an object containing local option values as key-value pairs. 1550 * 1551 * @return {Object} 1552 */ 1553 opts() { 1554 if (this._storeOptionsAsProperties) { 1555 // Preserve original behaviour so backwards compatible when still using properties 1556 const result = {}; 1557 const len = this.options.length; 1558 1559 for (let i = 0; i < len; i++) { 1560 const key = this.options[i].attributeName(); 1561 result[key] = key === this._versionOptionName ? this._version : this[key]; 1562 } 1563 return result; 1564 } 1565 1566 return this._optionValues; 1567 } 1568 1569 /** 1570 * Return an object containing merged local and global option values as key-value pairs. 1571 * 1572 * @return {Object} 1573 */ 1574 optsWithGlobals() { 1575 // globals overwrite locals 1576 return this._getCommandAndAncestors().reduce( 1577 (combinedOptions, cmd) => Object.assign(combinedOptions, cmd.opts()), 1578 {} 1579 ); 1580 } 1581 1582 /** 1583 * Display error message and exit (or call exitOverride). 1584 * 1585 * @param {string} message 1586 * @param {Object} [errorOptions] 1587 * @param {string} [errorOptions.code] - an id string representing the error 1588 * @param {number} [errorOptions.exitCode] - used with process.exit 1589 */ 1590 error(message, errorOptions) { 1591 // output handling 1592 this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr); 1593 if (typeof this._showHelpAfterError === 'string') { 1594 this._outputConfiguration.writeErr(`${this._showHelpAfterError}\n`); 1595 } else if (this._showHelpAfterError) { 1596 this._outputConfiguration.writeErr('\n'); 1597 this.outputHelp({ error: true }); 1598 } 1599 1600 // exit handling 1601 const config = errorOptions || {}; 1602 const exitCode = config.exitCode || 1; 1603 const code = config.code || 'commander.error'; 1604 this._exit(exitCode, code, message); 1605 } 1606 1607 /** 1608 * Apply any option related environment variables, if option does 1609 * not have a value from cli or client code. 1610 * 1611 * @api private 1612 */ 1613 _parseOptionsEnv() { 1614 this.options.forEach((option) => { 1615 if (option.envVar && option.envVar in process.env) { 1616 const optionKey = option.attributeName(); 1617 // Priority check. Do not overwrite cli or options from unknown source (client-code). 1618 if (this.getOptionValue(optionKey) === undefined || ['default', 'config', 'env'].includes(this.getOptionValueSource(optionKey))) { 1619 if (option.required || option.optional) { // option can take a value 1620 // keep very simple, optional always takes value 1621 this.emit(`optionEnv:${option.name()}`, process.env[option.envVar]); 1622 } else { // boolean 1623 // keep very simple, only care that envVar defined and not the value 1624 this.emit(`optionEnv:${option.name()}`); 1625 } 1626 } 1627 } 1628 }); 1629 } 1630 1631 /** 1632 * Apply any implied option values, if option is undefined or default value. 1633 * 1634 * @api private 1635 */ 1636 _parseOptionsImplied() { 1637 const dualHelper = new DualOptions(this.options); 1638 const hasCustomOptionValue = (optionKey) => { 1639 return this.getOptionValue(optionKey) !== undefined && !['default', 'implied'].includes(this.getOptionValueSource(optionKey)); 1640 }; 1641 this.options 1642 .filter(option => (option.implied !== undefined) && 1643 hasCustomOptionValue(option.attributeName()) && 1644 dualHelper.valueFromOption(this.getOptionValue(option.attributeName()), option)) 1645 .forEach((option) => { 1646 Object.keys(option.implied) 1647 .filter(impliedKey => !hasCustomOptionValue(impliedKey)) 1648 .forEach(impliedKey => { 1649 this.setOptionValueWithSource(impliedKey, option.implied[impliedKey], 'implied'); 1650 }); 1651 }); 1652 } 1653 1654 /** 1655 * Argument `name` is missing. 1656 * 1657 * @param {string} name 1658 * @api private 1659 */ 1660 1661 missingArgument(name) { 1662 const message = `error: missing required argument '${name}'`; 1663 this.error(message, { code: 'commander.missingArgument' }); 1664 } 1665 1666 /** 1667 * `Option` is missing an argument. 1668 * 1669 * @param {Option} option 1670 * @api private 1671 */ 1672 1673 optionMissingArgument(option) { 1674 const message = `error: option '${option.flags}' argument missing`; 1675 this.error(message, { code: 'commander.optionMissingArgument' }); 1676 } 1677 1678 /** 1679 * `Option` does not have a value, and is a mandatory option. 1680 * 1681 * @param {Option} option 1682 * @api private 1683 */ 1684 1685 missingMandatoryOptionValue(option) { 1686 const message = `error: required option '${option.flags}' not specified`; 1687 this.error(message, { code: 'commander.missingMandatoryOptionValue' }); 1688 } 1689 1690 /** 1691 * `Option` conflicts with another option. 1692 * 1693 * @param {Option} option 1694 * @param {Option} conflictingOption 1695 * @api private 1696 */ 1697 _conflictingOption(option, conflictingOption) { 1698 // The calling code does not know whether a negated option is the source of the 1699 // value, so do some work to take an educated guess. 1700 const findBestOptionFromValue = (option) => { 1701 const optionKey = option.attributeName(); 1702 const optionValue = this.getOptionValue(optionKey); 1703 const negativeOption = this.options.find(target => target.negate && optionKey === target.attributeName()); 1704 const positiveOption = this.options.find(target => !target.negate && optionKey === target.attributeName()); 1705 if (negativeOption && ( 1706 (negativeOption.presetArg === undefined && optionValue === false) || 1707 (negativeOption.presetArg !== undefined && optionValue === negativeOption.presetArg) 1708 )) { 1709 return negativeOption; 1710 } 1711 return positiveOption || option; 1712 }; 1713 1714 const getErrorMessage = (option) => { 1715 const bestOption = findBestOptionFromValue(option); 1716 const optionKey = bestOption.attributeName(); 1717 const source = this.getOptionValueSource(optionKey); 1718 if (source === 'env') { 1719 return `environment variable '${bestOption.envVar}'`; 1720 } 1721 return `option '${bestOption.flags}'`; 1722 }; 1723 1724 const message = `error: ${getErrorMessage(option)} cannot be used with ${getErrorMessage(conflictingOption)}`; 1725 this.error(message, { code: 'commander.conflictingOption' }); 1726 } 1727 1728 /** 1729 * Unknown option `flag`. 1730 * 1731 * @param {string} flag 1732 * @api private 1733 */ 1734 1735 unknownOption(flag) { 1736 if (this._allowUnknownOption) return; 1737 let suggestion = ''; 1738 1739 if (flag.startsWith('--') && this._showSuggestionAfterError) { 1740 // Looping to pick up the global options too 1741 let candidateFlags = []; 1742 let command = this; 1743 do { 1744 const moreFlags = command.createHelp().visibleOptions(command) 1745 .filter(option => option.long) 1746 .map(option => option.long); 1747 candidateFlags = candidateFlags.concat(moreFlags); 1748 command = command.parent; 1749 } while (command && !command._enablePositionalOptions); 1750 suggestion = suggestSimilar(flag, candidateFlags); 1751 } 1752 1753 const message = `error: unknown option '${flag}'${suggestion}`; 1754 this.error(message, { code: 'commander.unknownOption' }); 1755 } 1756 1757 /** 1758 * Excess arguments, more than expected. 1759 * 1760 * @param {string[]} receivedArgs 1761 * @api private 1762 */ 1763 1764 _excessArguments(receivedArgs) { 1765 if (this._allowExcessArguments) return; 1766 1767 const expected = this.registeredArguments.length; 1768 const s = (expected === 1) ? '' : 's'; 1769 const forSubcommand = this.parent ? ` for '${this.name()}'` : ''; 1770 const message = `error: too many arguments${forSubcommand}. Expected ${expected} argument${s} but got ${receivedArgs.length}.`; 1771 this.error(message, { code: 'commander.excessArguments' }); 1772 } 1773 1774 /** 1775 * Unknown command. 1776 * 1777 * @api private 1778 */ 1779 1780 unknownCommand() { 1781 const unknownName = this.args[0]; 1782 let suggestion = ''; 1783 1784 if (this._showSuggestionAfterError) { 1785 const candidateNames = []; 1786 this.createHelp().visibleCommands(this).forEach((command) => { 1787 candidateNames.push(command.name()); 1788 // just visible alias 1789 if (command.alias()) candidateNames.push(command.alias()); 1790 }); 1791 suggestion = suggestSimilar(unknownName, candidateNames); 1792 } 1793 1794 const message = `error: unknown command '${unknownName}'${suggestion}`; 1795 this.error(message, { code: 'commander.unknownCommand' }); 1796 } 1797 1798 /** 1799 * Get or set the program version. 1800 * 1801 * This method auto-registers the "-V, --version" option which will print the version number. 1802 * 1803 * You can optionally supply the flags and description to override the defaults. 1804 * 1805 * @param {string} [str] 1806 * @param {string} [flags] 1807 * @param {string} [description] 1808 * @return {this | string | undefined} `this` command for chaining, or version string if no arguments 1809 */ 1810 1811 version(str, flags, description) { 1812 if (str === undefined) return this._version; 1813 this._version = str; 1814 flags = flags || '-V, --version'; 1815 description = description || 'output the version number'; 1816 const versionOption = this.createOption(flags, description); 1817 this._versionOptionName = versionOption.attributeName(); // [sic] not defined in constructor, partly legacy, partly only needed at root 1818 this.options.push(versionOption); 1819 this.on('option:' + versionOption.name(), () => { 1820 this._outputConfiguration.writeOut(`${str}\n`); 1821 this._exit(0, 'commander.version', str); 1822 }); 1823 return this; 1824 } 1825 1826 /** 1827 * Set the description. 1828 * 1829 * @param {string} [str] 1830 * @param {Object} [argsDescription] 1831 * @return {string|Command} 1832 */ 1833 description(str, argsDescription) { 1834 if (str === undefined && argsDescription === undefined) return this._description; 1835 this._description = str; 1836 if (argsDescription) { 1837 this._argsDescription = argsDescription; 1838 } 1839 return this; 1840 } 1841 1842 /** 1843 * Set the summary. Used when listed as subcommand of parent. 1844 * 1845 * @param {string} [str] 1846 * @return {string|Command} 1847 */ 1848 summary(str) { 1849 if (str === undefined) return this._summary; 1850 this._summary = str; 1851 return this; 1852 } 1853 1854 /** 1855 * Set an alias for the command. 1856 * 1857 * You may call more than once to add multiple aliases. Only the first alias is shown in the auto-generated help. 1858 * 1859 * @param {string} [alias] 1860 * @return {string|Command} 1861 */ 1862 1863 alias(alias) { 1864 if (alias === undefined) return this._aliases[0]; // just return first, for backwards compatibility 1865 1866 /** @type {Command} */ 1867 let command = this; 1868 if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) { 1869 // assume adding alias for last added executable subcommand, rather than this 1870 command = this.commands[this.commands.length - 1]; 1871 } 1872 1873 if (alias === command._name) throw new Error('Command alias can\'t be the same as its name'); 1874 1875 command._aliases.push(alias); 1876 return this; 1877 } 1878 1879 /** 1880 * Set aliases for the command. 1881 * 1882 * Only the first alias is shown in the auto-generated help. 1883 * 1884 * @param {string[]} [aliases] 1885 * @return {string[]|Command} 1886 */ 1887 1888 aliases(aliases) { 1889 // Getter for the array of aliases is the main reason for having aliases() in addition to alias(). 1890 if (aliases === undefined) return this._aliases; 1891 1892 aliases.forEach((alias) => this.alias(alias)); 1893 return this; 1894 } 1895 1896 /** 1897 * Set / get the command usage `str`. 1898 * 1899 * @param {string} [str] 1900 * @return {String|Command} 1901 */ 1902 1903 usage(str) { 1904 if (str === undefined) { 1905 if (this._usage) return this._usage; 1906 1907 const args = this.registeredArguments.map((arg) => { 1908 return humanReadableArgName(arg); 1909 }); 1910 return [].concat( 1911 (this.options.length || this._hasHelpOption ? '[options]' : []), 1912 (this.commands.length ? '[command]' : []), 1913 (this.registeredArguments.length ? args : []) 1914 ).join(' '); 1915 } 1916 1917 this._usage = str; 1918 return this; 1919 } 1920 1921 /** 1922 * Get or set the name of the command. 1923 * 1924 * @param {string} [str] 1925 * @return {string|Command} 1926 */ 1927 1928 name(str) { 1929 if (str === undefined) return this._name; 1930 this._name = str; 1931 return this; 1932 } 1933 1934 /** 1935 * Set the name of the command from script filename, such as process.argv[1], 1936 * or require.main.filename, or __filename. 1937 * 1938 * (Used internally and public although not documented in README.) 1939 * 1940 * @example 1941 * program.nameFromFilename(require.main.filename); 1942 * 1943 * @param {string} filename 1944 * @return {Command} 1945 */ 1946 1947 nameFromFilename(filename) { 1948 this._name = path.basename(filename, path.extname(filename)); 1949 1950 return this; 1951 } 1952 1953 /** 1954 * Get or set the directory for searching for executable subcommands of this command. 1955 * 1956 * @example 1957 * program.executableDir(__dirname); 1958 * // or 1959 * program.executableDir('subcommands'); 1960 * 1961 * @param {string} [path] 1962 * @return {string|null|Command} 1963 */ 1964 1965 executableDir(path) { 1966 if (path === undefined) return this._executableDir; 1967 this._executableDir = path; 1968 return this; 1969 } 1970 1971 /** 1972 * Return program help documentation. 1973 * 1974 * @param {{ error: boolean }} [contextOptions] - pass {error:true} to wrap for stderr instead of stdout 1975 * @return {string} 1976 */ 1977 1978 helpInformation(contextOptions) { 1979 const helper = this.createHelp(); 1980 if (helper.helpWidth === undefined) { 1981 helper.helpWidth = (contextOptions && contextOptions.error) ? this._outputConfiguration.getErrHelpWidth() : this._outputConfiguration.getOutHelpWidth(); 1982 } 1983 return helper.formatHelp(this, helper); 1984 } 1985 1986 /** 1987 * @api private 1988 */ 1989 1990 _getHelpContext(contextOptions) { 1991 contextOptions = contextOptions || {}; 1992 const context = { error: !!contextOptions.error }; 1993 let write; 1994 if (context.error) { 1995 write = (arg) => this._outputConfiguration.writeErr(arg); 1996 } else { 1997 write = (arg) => this._outputConfiguration.writeOut(arg); 1998 } 1999 context.write = contextOptions.write || write; 2000 context.command = this; 2001 return context; 2002 } 2003 2004 /** 2005 * Output help information for this command. 2006 * 2007 * Outputs built-in help, and custom text added using `.addHelpText()`. 2008 * 2009 * @param {{ error: boolean } | Function} [contextOptions] - pass {error:true} to write to stderr instead of stdout 2010 */ 2011 2012 outputHelp(contextOptions) { 2013 let deprecatedCallback; 2014 if (typeof contextOptions === 'function') { 2015 deprecatedCallback = contextOptions; 2016 contextOptions = undefined; 2017 } 2018 const context = this._getHelpContext(contextOptions); 2019 2020 this._getCommandAndAncestors().reverse().forEach(command => command.emit('beforeAllHelp', context)); 2021 this.emit('beforeHelp', context); 2022 2023 let helpInformation = this.helpInformation(context); 2024 if (deprecatedCallback) { 2025 helpInformation = deprecatedCallback(helpInformation); 2026 if (typeof helpInformation !== 'string' && !Buffer.isBuffer(helpInformation)) { 2027 throw new Error('outputHelp callback must return a string or a Buffer'); 2028 } 2029 } 2030 context.write(helpInformation); 2031 2032 if (this._helpLongFlag) { 2033 this.emit(this._helpLongFlag); // deprecated 2034 } 2035 this.emit('afterHelp', context); 2036 this._getCommandAndAncestors().forEach(command => command.emit('afterAllHelp', context)); 2037 } 2038 2039 /** 2040 * You can pass in flags and a description to override the help 2041 * flags and help description for your command. Pass in false to 2042 * disable the built-in help option. 2043 * 2044 * @param {string | boolean} [flags] 2045 * @param {string} [description] 2046 * @return {Command} `this` command for chaining 2047 */ 2048 2049 helpOption(flags, description) { 2050 if (typeof flags === 'boolean') { 2051 this._hasHelpOption = flags; 2052 return this; 2053 } 2054 this._helpFlags = flags || this._helpFlags; 2055 this._helpDescription = description || this._helpDescription; 2056 2057 const helpFlags = splitOptionFlags(this._helpFlags); 2058 this._helpShortFlag = helpFlags.shortFlag; 2059 this._helpLongFlag = helpFlags.longFlag; 2060 2061 return this; 2062 } 2063 2064 /** 2065 * Output help information and exit. 2066 * 2067 * Outputs built-in help, and custom text added using `.addHelpText()`. 2068 * 2069 * @param {{ error: boolean }} [contextOptions] - pass {error:true} to write to stderr instead of stdout 2070 */ 2071 2072 help(contextOptions) { 2073 this.outputHelp(contextOptions); 2074 let exitCode = process.exitCode || 0; 2075 if (exitCode === 0 && contextOptions && typeof contextOptions !== 'function' && contextOptions.error) { 2076 exitCode = 1; 2077 } 2078 // message: do not have all displayed text available so only passing placeholder. 2079 this._exit(exitCode, 'commander.help', '(outputHelp)'); 2080 } 2081 2082 /** 2083 * Add additional text to be displayed with the built-in help. 2084 * 2085 * Position is 'before' or 'after' to affect just this command, 2086 * and 'beforeAll' or 'afterAll' to affect this command and all its subcommands. 2087 * 2088 * @param {string} position - before or after built-in help 2089 * @param {string | Function} text - string to add, or a function returning a string 2090 * @return {Command} `this` command for chaining 2091 */ 2092 addHelpText(position, text) { 2093 const allowedValues = ['beforeAll', 'before', 'after', 'afterAll']; 2094 if (!allowedValues.includes(position)) { 2095 throw new Error(`Unexpected value for position to addHelpText. 2096 Expecting one of '${allowedValues.join("', '")}'`); 2097 } 2098 const helpEvent = `${position}Help`; 2099 this.on(helpEvent, (context) => { 2100 let helpStr; 2101 if (typeof text === 'function') { 2102 helpStr = text({ error: context.error, command: context.command }); 2103 } else { 2104 helpStr = text; 2105 } 2106 // Ignore falsy value when nothing to output. 2107 if (helpStr) { 2108 context.write(`${helpStr}\n`); 2109 } 2110 }); 2111 return this; 2112 } 2113 } 2114 2115 /** 2116 * Output help information if help flags specified 2117 * 2118 * @param {Command} cmd - command to output help for 2119 * @param {Array} args - array of options to search for help flags 2120 * @api private 2121 */ 2122 2123 function outputHelpIfRequested(cmd, args) { 2124 const helpOption = cmd._hasHelpOption && args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag); 2125 if (helpOption) { 2126 cmd.outputHelp(); 2127 // (Do not have all displayed text available so only passing placeholder.) 2128 cmd._exit(0, 'commander.helpDisplayed', '(outputHelp)'); 2129 } 2130 } 2131 2132 /** 2133 * Scan arguments and increment port number for inspect calls (to avoid conflicts when spawning new command). 2134 * 2135 * @param {string[]} args - array of arguments from node.execArgv 2136 * @returns {string[]} 2137 * @api private 2138 */ 2139 2140 function incrementNodeInspectorPort(args) { 2141 // Testing for these options: 2142 // --inspect[=[host:]port] 2143 // --inspect-brk[=[host:]port] 2144 // --inspect-port=[host:]port 2145 return args.map((arg) => { 2146 if (!arg.startsWith('--inspect')) { 2147 return arg; 2148 } 2149 let debugOption; 2150 let debugHost = '127.0.0.1'; 2151 let debugPort = '9229'; 2152 let match; 2153 if ((match = arg.match(/^(--inspect(-brk)?)$/)) !== null) { 2154 // e.g. --inspect 2155 debugOption = match[1]; 2156 } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+)$/)) !== null) { 2157 debugOption = match[1]; 2158 if (/^\d+$/.test(match[3])) { 2159 // e.g. --inspect=1234 2160 debugPort = match[3]; 2161 } else { 2162 // e.g. --inspect=localhost 2163 debugHost = match[3]; 2164 } 2165 } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/)) !== null) { 2166 // e.g. --inspect=localhost:1234 2167 debugOption = match[1]; 2168 debugHost = match[3]; 2169 debugPort = match[4]; 2170 } 2171 2172 if (debugOption && debugPort !== '0') { 2173 return `${debugOption}=${debugHost}:${parseInt(debugPort) + 1}`; 2174 } 2175 return arg; 2176 }); 2177 } 2178 2179 exports.Command = Command;