diff --git a/lib/command.js b/lib/command.js index 590a271dd..1c39da626 100644 --- a/lib/command.js +++ b/lib/command.js @@ -7,7 +7,7 @@ const process = require('process'); const { Argument, humanReadableArgName } = require('./argument.js'); const { CommanderError } = require('./error.js'); const { Help } = require('./help.js'); -const { Option, splitOptionFlags, DualOptions } = require('./option.js'); +const { Option, DualOptions } = require('./option.js'); const { suggestSimilar } = require('./suggestSimilar'); // @ts-check @@ -67,16 +67,13 @@ class Command extends EventEmitter { }; this._hidden = false; - this._hasHelpOption = true; - this._helpFlags = '-h, --help'; - this._helpDescription = 'display help for command'; - this._helpShortFlag = '-h'; - this._helpLongFlag = '--help'; this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false + + this._helpConfiguration = {}; this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; - this._helpConfiguration = {}; + this.helpOption('-h, --help', 'display help for command'); } /** @@ -89,15 +86,6 @@ class Command extends EventEmitter { */ copyInheritedSettings(sourceCommand) { this._outputConfiguration = sourceCommand._outputConfiguration; - this._hasHelpOption = sourceCommand._hasHelpOption; - this._helpFlags = sourceCommand._helpFlags; - this._helpDescription = sourceCommand._helpDescription; - this._helpShortFlag = sourceCommand._helpShortFlag; - this._helpLongFlag = sourceCommand._helpLongFlag; - this._helpCommandName = sourceCommand._helpCommandName; - this._helpCommandnameAndArgs = sourceCommand._helpCommandnameAndArgs; - this._helpCommandDescription = sourceCommand._helpCommandDescription; - this._helpConfiguration = sourceCommand._helpConfiguration; this._exitCallback = sourceCommand._exitCallback; this._storeOptionsAsProperties = sourceCommand._storeOptionsAsProperties; this._combineFlagAndOptionalValue = sourceCommand._combineFlagAndOptionalValue; @@ -106,6 +94,16 @@ class Command extends EventEmitter { this._showHelpAfterError = sourceCommand._showHelpAfterError; this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError; + this._helpConfiguration = sourceCommand._helpConfiguration; + this._helpCommandName = sourceCommand._helpCommandName; + this._helpCommandnameAndArgs = sourceCommand._helpCommandnameAndArgs; + this._helpCommandDescription = sourceCommand._helpCommandDescription; + if (sourceCommand._hasHelpOption) { + this.helpOption(sourceCommand._helpFlags, sourceCommand._helpDescription); + } else { + this._hasHelpOption = false; + } + return this; } @@ -1101,7 +1099,9 @@ Expecting one of '${allowedValues.join("', '")}'`); } // Fallback to parsing the help flag to invoke the help. - return this._dispatchSubcommand(subcommandName, [], [this._helpLongFlag]); + return this._dispatchSubcommand(subcommandName, [], [ + this._helpOption.long || this._helpOption.short + ]); } /** @@ -1257,7 +1257,14 @@ Expecting one of '${allowedValues.join("', '")}'`); */ _parseCommand(operands, unknown) { - const parsed = this.parseOptions(unknown); + const { displayHelp, ...parsed } = this.parseOptions(unknown); + if (displayHelp) { + // Could simply call .help() if not for the legacy 'commander.helpDisplayed' error code. + this.outputHelp(); + // (Do not have all displayed text available so only passing placeholder.) + this._exit(0, 'commander.helpDisplayed', '(outputHelp)'); + } + this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env this._parseOptionsImplied(); operands = operands.concat(parsed.operands); @@ -1271,7 +1278,6 @@ Expecting one of '${allowedValues.join("', '")}'`); return this._dispatchHelpCommand(operands[1]); } if (this._defaultCommandName) { - outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command return this._dispatchSubcommand(this._defaultCommandName, operands, unknown); } if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { @@ -1279,13 +1285,12 @@ Expecting one of '${allowedValues.join("', '")}'`); this.help({ error: true }); } - outputHelpIfRequested(this, parsed.unknown); this._checkForMissingMandatoryOptions(); this._checkForConflictingOptions(); // We do not always call this check to avoid masking a "better" error, like unknown command. const checkForUnknownOptions = () => { - if (parsed.unknown.length > 0) { + if (unknown.length > 0) { this.unknownOption(parsed.unknown[0]); } }; @@ -1419,32 +1424,34 @@ Expecting one of '${allowedValues.join("', '")}'`); /** * Parse options from `argv` removing known options, - * and return argv split into operands and unknown arguments. + * and return argv split into operands and unknown arguments, + * as well as a boolean indicating whether a help flag had been found before entering a subcommand. * - * Examples: - * - * argv => operands, unknown - * --known kkk op => [op], [] - * op --known kkk => [op], [] - * sub --unknown uuu op => [sub], [--unknown uuu op] - * sub -- --unknown uuu op => [sub --unknown uuu op], [] + * argv => operands, unknown, displayHelp + * --known kkk op => [op], [], false + * op --known kkk => [op], [], false + * sub --unknown uuu op => [sub], [--unknown uuu op], false + * sub -- --unknown uuu op => [sub --unknown uuu op], [], false + * --help => [], [], true + * sub --help => [], ['--help'], false * * @param {String[]} argv - * @return {{operands: String[], unknown: String[]}} + * @return {{operands: String[], unknown: String[], displayHelp: boolean}} */ parseOptions(argv) { const operands = []; // operands, not options or values const unknown = []; // first unknown option and remaining unknown args + let displayHelp = false; // whether a help flag had been found before entering a subcommand + let dest = operands; const args = argv.slice(); - function maybeOption(arg) { - return arg.length > 1 && arg[0] === '-'; - } - // parse options let activeVariadicOption = null; + let onlyKnownOptionsSoFar = true; + let reprocessedBySubcommand = false; + let onlyConsumeHelpOption = false; while (args.length) { const arg = args.shift(); @@ -1455,98 +1462,126 @@ Expecting one of '${allowedValues.join("', '")}'`); break; } - if (activeVariadicOption && !maybeOption(arg)) { + const isArgFlag = isFlag(arg); + + if (activeVariadicOption && !isArgFlag) { this.emit(`option:${activeVariadicOption.name()}`, arg); continue; } activeVariadicOption = null; - if (maybeOption(arg)) { - const option = this._findOption(arg); - // recognised option, call listener to assign value with possible custom processing - if (option) { - if (option.required) { - const value = args.shift(); - if (value === undefined) this.optionMissingArgument(option); - this.emit(`option:${option.name()}`, value); - } else if (option.optional) { - let value = null; - // historical behaviour is optional value is following arg unless an option - if (args.length > 0 && !maybeOption(args[0])) { - value = args.shift(); + if (isArgFlag) { + const isHelpOption = this._hasHelpOption && this._helpOption.is(arg); + // Options added via .option(), .addOption() and .version() have precedence over help option. + const option = this._findOption(arg) ?? (isHelpOption && this._helpOption); + if (option === this._helpOption) { + // Help option is always positional, consider unknown if reprocessed by subcommand. + displayHelp = !reprocessedBySubcommand; + if (displayHelp) continue; + } else if (!onlyConsumeHelpOption) { + if (option) { + // recognised option, call listener to assign value with possible custom processing + if (option.required) { + const value = args.shift(); + if (value === undefined) this.optionMissingArgument(option); + this.emit(`option:${option.name()}`, value); + } else if (option.optional) { + let value = null; + // historical behaviour is optional value is following arg unless an option + if (args.length > 0 && !isFlag(args[0])) { + value = args.shift(); + } + this.emit(`option:${option.name()}`, value); + } else { // boolean flag + this.emit(`option:${option.name()}`); } - this.emit(`option:${option.name()}`, value); - } else { // boolean flag - this.emit(`option:${option.name()}`); + activeVariadicOption = option.variadic ? option : null; + continue; } - activeVariadicOption = option.variadic ? option : null; - continue; - } - } - // Look for combo options following single dash, eat first one if known. - if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { - const option = this._findOption(`-${arg[1]}`); - if (option) { - if (option.required || (option.optional && this._combineFlagAndOptionalValue)) { - // option with value following in same argument - this.emit(`option:${option.name()}`, arg.slice(2)); - } else { - // boolean option, emit and put back remainder of arg for further processing - this.emit(`option:${option.name()}`); - args.unshift(`-${arg.slice(2)}`); + // Look for combo options following single dash, eat first one if known. + if (arg.length > 2 && arg[1] !== '-') { + const option = this._findOption(`-${arg[1]}`); + if (option) { + if (option.required || (option.optional && this._combineFlagAndOptionalValue)) { + // option with value following in same argument + this.emit(`option:${option.name()}`, arg.slice(2)); + } else { + // boolean option, emit and put back remainder of arg for further processing + this.emit(`option:${option.name()}`); + args.unshift(`-${arg.slice(2)}`); + } + continue; + } + } + + // Look for known long flag with value, like --foo=bar + if (/^--[^=]+=/.test(arg)) { + const index = arg.indexOf('='); + const option = this._findOption(arg.slice(0, index)); + if (option && (option.required || option.optional)) { + this.emit(`option:${option.name()}`, arg.slice(index + 1)); + continue; + } } - continue; } } - // Look for known long flag with value, like --foo=bar - if (/^--[^=]+=/.test(arg)) { - const index = arg.indexOf('='); - const option = this._findOption(arg.slice(0, index)); - if (option && (option.required || option.optional)) { - this.emit(`option:${option.name()}`, arg.slice(index + 1)); - continue; + // Not a recognised option by this command. Might be + // - a subcommand, + // - the help option encountered after a subcommand (considered unknown), + // - any other option unknown to this command, + // - or a command-argument. + + if (onlyKnownOptionsSoFar && !reprocessedBySubcommand) { + reprocessedBySubcommand = true; // reset to false later if not entering subcommand + const stopAtSubcommand = ( + this._enablePositionalOptions || this._passThroughOptions + ); + if (!isArgFlag && this._findCommand(arg)) { + if (stopAtSubcommand) { + operands.push(arg); + unknown.push(...args); + break; + } + } else if (!isArgFlag && + arg === this._helpCommandName && + this._hasImplicitHelpCommand() + ) { + if (stopAtSubcommand) { + operands.push(arg, ...args); + break; + } + } else if (this._defaultCommandName) { + if (stopAtSubcommand) { + unknown.push(arg, ...args); + break; + } + } else { + reprocessedBySubcommand = false; } } - - // Not a recognised option by this command. - // Might be a command-argument, or subcommand option, or unknown option, or help command or option. + onlyKnownOptionsSoFar = false; // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands. - if (maybeOption(arg)) { + if (isArgFlag) { dest = unknown; } - // If using positionalOptions, stop processing our options at subcommand. - if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { - if (this._findCommand(arg)) { - operands.push(arg); - if (args.length > 0) unknown.push(...args); - break; - } else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) { - operands.push(arg); - if (args.length > 0) operands.push(...args); - break; - } else if (this._defaultCommandName) { - unknown.push(arg); - if (args.length > 0) unknown.push(...args); + // If using passThroughOptions, stop processing options (except help) at first command-argument. + if (this._passThroughOptions) { + onlyConsumeHelpOption = true; + if (!this._hasHelpOption || !isArgFlag) { + dest.push(arg, ...args); break; } } - // If using passThroughOptions, stop processing options at first command-argument. - if (this._passThroughOptions) { - dest.push(arg); - if (args.length > 0) dest.push(...args); - break; - } - // add arg dest.push(arg); } - return { operands, unknown }; + return { operands, unknown, displayHelp }; } /** @@ -2034,7 +2069,9 @@ Expecting one of '${allowedValues.join("', '")}'`); } context.write(helpInformation); - this.emit(this._helpLongFlag); // deprecated + if (this._helpOption.long) { + this.emit(this._helpOption.long); // deprecated + } this.emit('afterHelp', context); getCommandAndParents(this).forEach(command => command.emit('afterAllHelp', context)); } @@ -2050,17 +2087,14 @@ Expecting one of '${allowedValues.join("', '")}'`); */ helpOption(flags, description) { - if (typeof flags === 'boolean') { - this._hasHelpOption = flags; + if (flags !== undefined && typeof flags !== 'string') { + this._hasHelpOption = !!flags; return this; } - this._helpFlags = flags || this._helpFlags; - this._helpDescription = description || this._helpDescription; - - const helpFlags = splitOptionFlags(this._helpFlags); - this._helpShortFlag = helpFlags.shortFlag; - this._helpLongFlag = helpFlags.longFlag; - + this._hasHelpOption = true; + this._helpFlags = flags = flags || this._helpFlags; + this._helpDescription = description = description || this._helpDescription; + this._helpOption = this.createOption(flags, description); return this; } @@ -2115,23 +2149,6 @@ Expecting one of '${allowedValues.join("', '")}'`); } } -/** - * Output help information if help flags specified - * - * @param {Command} cmd - command to output help for - * @param {Array} args - array of options to search for help flags - * @api private - */ - -function outputHelpIfRequested(cmd, args) { - const helpOption = cmd._hasHelpOption && args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag); - if (helpOption) { - cmd.outputHelp(); - // (Do not have all displayed text available so only passing placeholder.) - cmd._exit(0, 'commander.helpDisplayed', '(outputHelp)'); - } -} - /** * Scan arguments and increment port number for inspect calls (to avoid conflicts when spawning new command). * @@ -2193,4 +2210,14 @@ function getCommandAndParents(startCommand) { return result; } +/** + * @param {string} arg + * @returns {boolean} + * @api private + */ + +function isFlag(arg) { + return arg.length > 1 && arg[0] === '-'; +} + exports.Command = Command; diff --git a/lib/help.js b/lib/help.js index 14e0fb9f3..a112b59fe 100644 --- a/lib/help.js +++ b/lib/help.js @@ -71,14 +71,14 @@ class Help { visibleOptions(cmd) { const visibleOptions = cmd.options.filter((option) => !option.hidden); // Implicit help - const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag); - const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag); + const showShortHelpFlag = cmd._hasHelpOption && cmd._helpOption.short && !cmd._findOption(cmd._helpOption.short); + const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpOption.long); if (showShortHelpFlag || showLongHelpFlag) { let helpOption; if (!showShortHelpFlag) { - helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription); + helpOption = cmd.createOption(cmd._helpOption.long, cmd._helpDescription); } else if (!showLongHelpFlag) { - helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription); + helpOption = cmd.createOption(cmd._helpOption.short, cmd._helpDescription); } else { helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription); } diff --git a/lib/option.js b/lib/option.js index d61fc5f2f..613dd0748 100644 --- a/lib/option.js +++ b/lib/option.js @@ -220,13 +220,13 @@ class Option { /** * Check if `arg` matches the short or long flag. * - * @param {string} arg + * @param {string | undefined} arg * @return {boolean} * @api private */ is(arg) { - return this.short === arg || this.long === arg; + return arg !== undefined && (this.short === arg || this.long === arg); } /** diff --git a/tests/command.addHelpText.test.js b/tests/command.addHelpText.test.js index e0bac2895..7103b0861 100644 --- a/tests/command.addHelpText.test.js +++ b/tests/command.addHelpText.test.js @@ -181,7 +181,7 @@ describe('context checks with full parse', () => { }); test('when help requested then context.error is false', () => { - let context = {}; + let context; const program = new commander.Command(); program .exitOverride() @@ -193,7 +193,7 @@ describe('context checks with full parse', () => { }); test('when help for error then context.error is true', () => { - let context = {}; + let context; const program = new commander.Command(); program .exitOverride() @@ -206,7 +206,7 @@ describe('context checks with full parse', () => { }); test('when help on program then context.command is program', () => { - let context = {}; + let context; const program = new commander.Command(); program .exitOverride() @@ -218,7 +218,7 @@ describe('context checks with full parse', () => { }); test('when help on subcommand and "before" subcommand then context.command is subcommand', () => { - let context = {}; + let context; const program = new commander.Command(); program .exitOverride(); @@ -231,7 +231,7 @@ describe('context checks with full parse', () => { }); test('when help on subcommand and "beforeAll" on program then context.command is subcommand', () => { - let context = {}; + let context; const program = new commander.Command(); program .exitOverride() diff --git a/tests/command.copySettings.test.js b/tests/command.copySettings.test.js index 79722d78b..8b258f434 100644 --- a/tests/command.copySettings.test.js +++ b/tests/command.copySettings.test.js @@ -44,10 +44,11 @@ describe('copyInheritedSettings property tests', () => { source.helpOption('-Z, --zz', 'ddd'); cmd.copyInheritedSettings(source); + expect(cmd._hasHelpOption).toBeTruthy(); expect(cmd._helpFlags).toBe('-Z, --zz'); expect(cmd._helpDescription).toBe('ddd'); - expect(cmd._helpShortFlag).toBe('-Z'); - expect(cmd._helpLongFlag).toBe('--zz'); + expect(cmd._helpOption.short).toBe('-Z'); + expect(cmd._helpOption.long).toBe('--zz'); }); test('when copyInheritedSettings then copies addHelpCommand(name, description)', () => { diff --git a/tests/command.helpOption.test.js b/tests/command.helpOption.test.js index 00e068c94..8f074c6f1 100644 --- a/tests/command.helpOption.test.js +++ b/tests/command.helpOption.test.js @@ -131,3 +131,121 @@ describe('helpOption', () => { }).toThrow("error: unknown command 'UNKNOWN'"); }); }); + +describe('obscured help flags', () => { + test('when obscured default help short flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .option('-h'); + expect(() => { + program.parse(['-h'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when obscured default help long flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .option('--help'); + expect(() => { + program.parse(['--help'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when both default help flags obscured and short flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .option('-h, --help'); + expect(() => { + program.parse(['-h'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when both default help flags obscured and long flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .option('-h, --help'); + expect(() => { + program.parse(['--help'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when obscured custom help short flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .helpOption('-c, --custom-help') + .option('-c'); + expect(() => { + program.parse(['-c'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when obscured custom help long flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .helpOption('-c, --custom-help') + .option('--custom-help'); + expect(() => { + program.parse(['--custom-help'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when both custom help flags obscured and short flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .helpOption('-c, --custom-help') + .option('-c, --custom-help'); + expect(() => { + program.parse(['-c'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); + + test('when both custom help flags obscured and long flag parsed then outputHelp() not called', () => { + const program = new commander.Command(); + program.outputHelp = jest.fn().mockImplementation( + program.outputHelp.bind(program) + ); + program + .exitOverride() + .helpOption('-c, --custom-help') + .option('-c, --custom-help'); + expect(() => { + program.parse(['--custom-help'], { from: 'user' }); + }).not.toThrow(); + expect(program.outputHelp).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/command.parseOptions.test.js b/tests/command.parseOptions.test.js index f4706e41c..f71aa25d9 100644 --- a/tests/command.parseOptions.test.js +++ b/tests/command.parseOptions.test.js @@ -61,97 +61,97 @@ describe('parseOptions', () => { test('when empty args then empty results', () => { const program = createProgram(); const result = program.parseOptions([]); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); }); test('when only operands then results has all operands', () => { const program = createProgram(); const result = program.parseOptions('one two three'.split(' ')); - expect(result).toEqual({ operands: ['one', 'two', 'three'], unknown: [] }); + expect(result).toEqual({ operands: ['one', 'two', 'three'], unknown: [], displayHelp: false }); }); test('when subcommand and operand then results has subcommand and operand', () => { const program = createProgram(); const result = program.parseOptions('sub one'.split(' ')); - expect(result).toEqual({ operands: ['sub', 'one'], unknown: [] }); + expect(result).toEqual({ operands: ['sub', 'one'], unknown: [], displayHelp: false }); }); test('when program has flag then option removed', () => { const program = createProgram(); const result = program.parseOptions('--global-flag'.split(' ')); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); }); test('when program has option with value then option removed', () => { const program = createProgram(); const result = program.parseOptions('--global-value foo'.split(' ')); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); }); test('when program has flag before operand then option removed', () => { const program = createProgram(); const result = program.parseOptions('--global-flag arg'.split(' ')); - expect(result).toEqual({ operands: ['arg'], unknown: [] }); + expect(result).toEqual({ operands: ['arg'], unknown: [], displayHelp: false }); }); test('when program has flag after operand then option removed', () => { const program = createProgram(); const result = program.parseOptions('arg --global-flag'.split(' ')); - expect(result).toEqual({ operands: ['arg'], unknown: [] }); + expect(result).toEqual({ operands: ['arg'], unknown: [], displayHelp: false }); }); test('when program has flag after subcommand then option removed', () => { const program = createProgram(); const result = program.parseOptions('sub --global-flag'.split(' ')); - expect(result).toEqual({ operands: ['sub'], unknown: [] }); + expect(result).toEqual({ operands: ['sub'], unknown: [], displayHelp: false }); }); test('when program has unknown option then option returned in unknown', () => { const program = createProgram(); const result = program.parseOptions('--unknown'.split(' ')); - expect(result).toEqual({ operands: [], unknown: ['--unknown'] }); + expect(result).toEqual({ operands: [], unknown: ['--unknown'], displayHelp: false }); }); test('when program has unknown option before operands then all unknown in same order', () => { const program = createProgram(); const result = program.parseOptions('--unknown arg'.split(' ')); - expect(result).toEqual({ operands: [], unknown: ['--unknown', 'arg'] }); + expect(result).toEqual({ operands: [], unknown: ['--unknown', 'arg'], displayHelp: false }); }); test('when program has unknown option after operand then option returned in unknown', () => { const program = createProgram(); const result = program.parseOptions('arg --unknown'.split(' ')); - expect(result).toEqual({ operands: ['arg'], unknown: ['--unknown'] }); + expect(result).toEqual({ operands: ['arg'], unknown: ['--unknown'], displayHelp: false }); }); test('when program has flag after unknown option then flag removed', () => { const program = createProgram(); const result = program.parseOptions('--unknown --global-flag'.split(' ')); - expect(result).toEqual({ operands: [], unknown: ['--unknown'] }); + expect(result).toEqual({ operands: [], unknown: ['--unknown'], displayHelp: false }); }); test('when subcommand has flag then flag returned as unknown', () => { const program = createProgram(); const result = program.parseOptions('sub --sub-flag'.split(' ')); - expect(result).toEqual({ operands: ['sub'], unknown: ['--sub-flag'] }); + expect(result).toEqual({ operands: ['sub'], unknown: ['--sub-flag'], displayHelp: false }); }); test('when program has literal before known flag then option returned as operand', () => { const program = createProgram(); const result = program.parseOptions('-- --global-flag'.split(' ')); - expect(result).toEqual({ operands: ['--global-flag'], unknown: [] }); + expect(result).toEqual({ operands: ['--global-flag'], unknown: [], displayHelp: false }); }); test('when program has literal before unknown option then option returned as operand', () => { const program = createProgram(); const result = program.parseOptions('-- --unknown uuu'.split(' ')); - expect(result).toEqual({ operands: ['--unknown', 'uuu'], unknown: [] }); + expect(result).toEqual({ operands: ['--unknown', 'uuu'], unknown: [], displayHelp: false }); }); test('when program has literal after unknown option then literal preserved too', () => { const program = createProgram(); const result = program.parseOptions('--unknown1 -- --unknown2'.split(' ')); - expect(result).toEqual({ operands: [], unknown: ['--unknown1', '--', '--unknown2'] }); + expect(result).toEqual({ operands: [], unknown: ['--unknown1', '--', '--unknown2'], displayHelp: false }); }); }); @@ -236,56 +236,56 @@ describe('Utility Conventions', () => { test('when program has combo known boolean short flags then arg removed', () => { const program = createProgram(); const result = program.parseOptions(['-ab']); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); expect(program.opts()).toEqual({ aaa: true, bbb: true }); }); test('when program has combo unknown short flags then arg preserved', () => { const program = createProgram(); const result = program.parseOptions(['-pq']); - expect(result).toEqual({ operands: [], unknown: ['-pq'] }); + expect(result).toEqual({ operands: [], unknown: ['-pq'], displayHelp: false }); expect(program.opts()).toEqual({ }); }); test('when program has combo known short option and required value then arg removed', () => { const program = createProgram(); const result = program.parseOptions(['-cvalue']); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); expect(program.opts()).toEqual({ ccc: 'value' }); }); test('when program has combo known short option and optional value then arg removed', () => { const program = createProgram(); const result = program.parseOptions(['-dvalue']); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); expect(program.opts()).toEqual({ ddd: 'value' }); }); test('when program has known combo short boolean flags and required value then arg removed', () => { const program = createProgram(); const result = program.parseOptions(['-abcvalue']); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); expect(program.opts()).toEqual({ aaa: true, bbb: true, ccc: 'value' }); }); test('when program has known combo short boolean flags and optional value then arg removed', () => { const program = createProgram(); const result = program.parseOptions(['-abdvalue']); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); expect(program.opts()).toEqual({ aaa: true, bbb: true, ddd: 'value' }); }); test('when program has known long flag=value then arg removed', () => { const program = createProgram(); const result = program.parseOptions(['--ccc=value']); - expect(result).toEqual({ operands: [], unknown: [] }); + expect(result).toEqual({ operands: [], unknown: [], displayHelp: false }); expect(program.opts()).toEqual({ ccc: 'value' }); }); test('when program has unknown long flag=value then arg preserved', () => { const program = createProgram(); const result = program.parseOptions(['--rrr=value']); - expect(result).toEqual({ operands: [], unknown: ['--rrr=value'] }); + expect(result).toEqual({ operands: [], unknown: ['--rrr=value'], displayHelp: false }); expect(program.opts()).toEqual({ }); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 695c3bd25..93291a7d0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -694,13 +694,16 @@ export class Command { /** * Parse options from `argv` removing known options, - * and return argv split into operands and unknown arguments. + * and return argv split into operands and unknown arguments, + * as well as a boolean indicating whether a help flag had been found before entering a subcommand. * - * argv => operands, unknown - * --known kkk op => [op], [] - * op --known kkk => [op], [] - * sub --unknown uuu op => [sub], [--unknown uuu op] - * sub -- --unknown uuu op => [sub --unknown uuu op], [] + * argv => operands, unknown, displayHelp + * --known kkk op => [op], [], false + * op --known kkk => [op], [], false + * sub --unknown uuu op => [sub], [--unknown uuu op], false + * sub -- --unknown uuu op => [sub --unknown uuu op], [], false + * --help => [], [], true + * sub --help => [], ['--help'], false */ parseOptions(argv: string[]): ParseOptionsResult; @@ -880,6 +883,7 @@ export interface ExecutableCommandOptions extends CommandOptions { export interface ParseOptionsResult { operands: string[]; unknown: string[]; + displayHelp: boolean; } export function createCommand(name?: string): Command; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 734036fad..3661143f8 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -215,7 +215,9 @@ expectType>(program.parseAsync(['--option'], { from: expectType>(program.parseAsync(['node', 'script.js'] as const)); // parseOptions (and ParseOptionsResult) -expectType<{ operands: string[]; unknown: string[] }>(program.parseOptions(['node', 'script.js', 'hello'])); +expectType<{ + operands: string[]; unknown: string[]; displayHelp: boolean; +}>(program.parseOptions(['node', 'script.js', 'hello'])); // opts const opts = program.opts();