From 01500af07bb4bf55b23f74099b4c9f85c916648b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 12 Jan 2020 22:38:58 +1300 Subject: [PATCH 01/37] First cut at parse rework - skip asterisk tests - other tests runnning - nested commands untested - lots of details to check --- index.js | 348 +++++++++++++---------------- tests/command.action.test.js | 8 +- tests/command.asterisk.test.js | 4 +- tests/command.exitOverride.test.js | 3 +- tests/command.help.test.js | 6 +- tests/command.usage.test.js | 16 +- tests/options.mandatory.test.js | 5 +- 7 files changed, 178 insertions(+), 212 deletions(-) diff --git a/index.js b/index.js index be0ffbcf8..c2b929fcd 100644 --- a/index.js +++ b/index.js @@ -5,8 +5,6 @@ var EventEmitter = require('events').EventEmitter; var spawn = require('child_process').spawn; var path = require('path'); -var dirname = path.dirname; -var basename = path.basename; var fs = require('fs'); /** @@ -132,6 +130,10 @@ function Command(name) { this._storeOptionsAsProperties = true; // backwards compatible by default this._passCommandToAction = true; // backwards compatible by default this._actionResults = []; + this._actionHandler = undefined; + this._executableHandler = false; + this._executableFile = undefined; // custom name for executable + this._defaultCommandName = undefined; this._helpFlags = '-h, --help'; this._helpDescription = 'output usage information'; @@ -179,9 +181,9 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts if (desc) { cmd.description(desc); - this.executables = true; + cmd._executableHandler = true; this._execs.add(cmd._name); - if (opts.isDefault) this.defaultExecutable = cmd._name; + if (opts.isDefault) this._defaultCommandName = cmd._name; } cmd._noHelp = !!opts.noHelp; cmd._helpFlags = this._helpFlags; @@ -194,7 +196,7 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts cmd._executableFile = opts.executableFile; // Custom name for executable file this.commands.push(cmd); - cmd.parseExpectedArgs(args); + cmd._parseExpectedArgs(args); cmd.parent = this; if (desc) return this; @@ -208,7 +210,7 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts */ Command.prototype.arguments = function(desc) { - return this.parseExpectedArgs(desc.split(/ +/)); + return this._parseExpectedArgs(desc.split(/ +/)); }; /** @@ -229,10 +231,10 @@ Command.prototype.addImplicitHelpCommand = function() { * * @param {Array} args * @return {Command} for chaining - * @api public + * @api private */ -Command.prototype.parseExpectedArgs = function(args) { +Command.prototype._parseExpectedArgs = function(args) { if (!args.length) return; var self = this; args.forEach(function(arg) { @@ -323,37 +325,8 @@ Command.prototype._exit = function(exitCode, code, message) { Command.prototype.action = function(fn) { var self = this; - var listener = function(args, unknown) { - // Parse any so-far unknown options - args = args || []; - unknown = unknown || []; - - var parsed = self.parseOptions(unknown); - - // Output help if necessary - outputHelpIfRequested(self, parsed.unknown); - self._checkForMissingMandatoryOptions(); - - // If there are still any unknown options, then we simply die. - if (parsed.unknown.length > 0) { - self.unknownOption(parsed.unknown[0]); - } - - args = args.concat(parsed.operands, parsed.unknown); - - self._args.forEach(function(expectedArg, i) { - if (expectedArg.required && args[i] == null) { - self.missingArgument(expectedArg.name); - } else if (expectedArg.variadic) { - if (i !== self._args.length - 1) { - self.variadicArgNotLast(expectedArg.name); - } - - args[i] = args.splice(i); - } - }); - - // The .action callback takes an extra parameter which is the command itself. + var listener = function(args) { + // The .action callback takes an extra parameter which is the command or options. var expectedArgsCount = self._args.length; var actionArgs = args.slice(0, expectedArgsCount); if (self._passCommandToAction) { @@ -374,14 +347,7 @@ Command.prototype.action = function(fn) { } rootCommand._actionResults.push(actionResult); }; - var parent = this.parent || this; - if (parent === this) { - parent.on('program-command', listener); - } else { - parent.on('command:' + this._name, listener); - } - - if (this._alias) parent.on('command:' + this._alias, listener); + this._actionHandler = listener; return this; }; @@ -425,7 +391,7 @@ Command.prototype._optionEx = function(config, flags, description, fn, defaultVa // when --no-foo we make sure default is true, unless a --foo option is already defined if (option.negate) { const positiveLongFlag = option.long.replace(/^--no-/, '--'); - defaultValue = self.optionFor(positiveLongFlag) ? self._getOptionValue(name) : true; + defaultValue = self._findOption(positiveLongFlag) ? self._getOptionValue(name) : true; } // preassign only if we have a default if (defaultValue !== undefined) { @@ -623,79 +589,32 @@ Command.prototype._getOptionValue = function(key) { Command.prototype.parse = function(argv) { // implicit help - if (this.executables) this.addImplicitHelpCommand(); + // if (this._executables) this.addImplicitHelpCommand(); ???? // store raw args this.rawArgs = argv; - // guess name - this._name = this._name || basename(argv[1], '.js'); + // Guess name, used in usage in help. Not trying very hard. See _executeSubCommand for trying hard by checking files. + this._name = this._name || path.basename(argv[1], path.extname(argv[1])); - // github-style sub-commands with no sub-command - if (this.executables && argv.length < 3 && !this.defaultExecutable) { - // this user needs help - argv.push(this._helpLongFlag); - } - - // process argv, leaving off first two args which are app and scriptname. - const parsed = this.parseOptions(argv.slice(2)); - const args = parsed.operands.concat(parsed.unknown); - this.args = args.slice(); + this._parseCommand([], argv.slice(2)); - var result = this.parseArgs(parsed.operands, parsed.unknown); + // const args = this.args; - if (args[0] === 'help' && args.length === 1) this.help(); + // if (args[0] === 'help' && args.length === 1) this.help(); - // Note for future: we could return early if we found an action handler in parseArgs, as none of following code needed? + // // --help + // if (args[0] === 'help') { + // args[0] = args[1]; + // args[1] = this._helpLongFlag; + // } else { + // // If calling through to executable subcommand we could check for help flags before failing, + // // but a somewhat unlikely case since program options not passed to executable subcommands. + // // Wait for reports to see if check needed and what usage pattern is. + // this._checkForMissingMandatoryOptions(); + // } - // --help - if (args[0] === 'help') { - args[0] = args[1]; - args[1] = this._helpLongFlag; - } else { - // If calling through to executable subcommand we could check for help flags before failing, - // but a somewhat unlikely case since program options not passed to executable subcommands. - // Wait for reports to see if check needed and what usage pattern is. - this._checkForMissingMandatoryOptions(); - } - - // executable sub-commands - // (Debugging note for future: args[0] is not right if an action has been called) - var name = result.args[0]; - var subCommand = null; - - // Look for subcommand - if (name) { - subCommand = this.commands.find(function(command) { - return command._name === name; - }); - } - - // Look for alias - if (!subCommand && name) { - subCommand = this.commands.find(function(command) { - return command.alias() === name; - }); - if (subCommand) { - name = subCommand._name; - args[0] = name; - } - } - - // Look for default subcommand - if (!subCommand && this.defaultExecutable) { - name = this.defaultExecutable; - args.unshift(name); - subCommand = this.commands.find(function(command) { - return command._name === name; - }); - } - - if (this._execs.has(name)) { - return this.executeSubCommand(argv, args, subCommand ? subCommand._executableFile : undefined); - } - - return result; + return this; }; /** @@ -707,6 +626,7 @@ Command.prototype.parse = function(argv) { * @return {Promise} * @api public */ + Command.prototype.parseAsync = function(argv) { this.parse(argv); return Promise.all(this._actionResults); @@ -715,59 +635,55 @@ Command.prototype.parseAsync = function(argv) { /** * Execute a sub-command executable. * - * @param {Array} argv - * @param {Array} args - * @param {Array} unknown - * @param {String} executableFile * @api private */ -Command.prototype.executeSubCommand = function(argv, args, executableFile) { - if (!args.length) this.help(); +Command.prototype._executeSubCommand = function(subcommand, args) { + args = args.slice(); + let launchWithNode = false; // Use node for source targets so do not need to get permissions correct, and on Windows. + const sourceExt = ['.js', '.ts', '.mjs']; + + // Want the entry script as the reference for command name and directory for searching for other files. + let scriptPath = this.rawArgs[1]; + if (!fs.existsSync(scriptPath) || fs.statSync(scriptPath).isDirectory()) { + // When launched using "node foo" will be missing file extension, or "node ." will be pointing at module instead of file. + // Try process.mainModule.filename as a fallback. + if (process.mainModule && process.mainModule.filename) { + scriptPath = process.mainModule.filename; + } + } - var isExplicitJS = false; // Whether to use node to launch "executable" + let baseDir; + try { + const resolvedLink = fs.realpathSync(scriptPath); + baseDir = path.dirname(resolvedLink); + } catch (e) { + baseDir = '.'; // dummy, probably not going to find executable! + } - // executable - var pm = argv[1]; // name of the subcommand, like `pm-install` - var bin = basename(pm, path.extname(pm)) + '-' + args[0]; - if (executableFile != null) { - bin = executableFile; - // Check for same extensions as we scan for below so get consistent launch behaviour. - var executableExt = path.extname(executableFile); - isExplicitJS = executableExt === '.js' || executableExt === '.ts' || executableExt === '.mjs'; + let bin = path.basename(scriptPath, path.extname(scriptPath)) + '-' + subcommand._name; + if (subcommand._executableFile) { + bin = subcommand._executableFile; } - // In case of globally installed, get the base dir where executable - // subcommand file should be located at - var baseDir; - - var resolvedLink = fs.realpathSync(pm); - - baseDir = dirname(resolvedLink); - - // prefer local `./` to bin in the $PATH - var localBin = path.join(baseDir, bin); - - // whether bin file is a js script with explicit `.js` or `.ts` extension - if (exists(localBin + '.js')) { - bin = localBin + '.js'; - isExplicitJS = true; - } else if (exists(localBin + '.ts')) { - bin = localBin + '.ts'; - isExplicitJS = true; - } else if (exists(localBin + '.mjs')) { - bin = localBin + '.mjs'; - isExplicitJS = true; - } else if (exists(localBin)) { + const localBin = path.join(baseDir, bin); + if (fs.existsSync(localBin)) { + // prefer local `./` to bin in the $PATH bin = localBin; + } else { + // Look for source files. + sourceExt.forEach((ext) => { + if (fs.existsSync(`${localBin}${ext}`)) { + bin = `${localBin}${ext}`; + } + }); } + launchWithNode = sourceExt.includes(path.extname(bin)); - args = args.slice(1); - - var proc; + let proc; if (process.platform !== 'win32') { - if (isExplicitJS) { + if (launchWithNode) { args.unshift(bin); // add executable arguments to spawn args = incrementNodeInspectorPort(process.execArgv).concat(args); @@ -783,7 +699,7 @@ Command.prototype.executeSubCommand = function(argv, args, executableFile) { proc = spawn(process.execPath, args, { stdio: 'inherit' }); } - var signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP']; + const signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP']; signals.forEach(function(signal) { process.on(signal, function() { if (proc.killed === false && proc.exitCode === null) { @@ -822,41 +738,84 @@ Command.prototype.executeSubCommand = function(argv, args, executableFile) { }; /** - * Parse command `args`. - * - * When listener(s) are available those - * callbacks are invoked, otherwise the "*" - * event is emitted and those actions are invoked. + * @api private + */ +Command.prototype._dispatchSubcommand = function(operands, unknown) { + let subCommand = this._findCommand(operands[0]); + if (subCommand) { + operands = operands.slice(1); + } else { + subCommand = this._findCommand(this._defaultCommandName); + } + if (!subCommand) { + // ???? + throw new Error('Failed to find subcommand'); + } + + if (subCommand._executableHandler) { + this._executeSubCommand(subCommand, operands.concat(unknown)); + } else { + subCommand._parseCommand(operands, unknown); + } +}; + +/** + * Process arguments in context of this command. * - * @param {Array} args - * @return {Command} for chaining * @api private */ -Command.prototype.parseArgs = function(operands, unknown) { - if (operands.length) { - const name = operands[0]; - if (this.listeners('command:' + name).length) { - this.emit('command:' + operands[0], operands.slice(1), unknown); - } else { - this.emit('program-command', operands, unknown); - this.emit('command:*', operands, unknown); - } +Command.prototype._parseCommand = function(operands, unknown) { + const parsed = this.parseOptions(unknown); + operands = operands.concat(parsed.operands); + unknown = parsed.unknown; + this.args = operands.concat(unknown); + + if (this._defaultCommandName || (operands && this._findCommand(operands[0]))) { + this._dispatchSubcommand(operands, unknown); } else { - outputHelpIfRequested(this, unknown); + if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { + // probaby missing subcommand and no handler, user needs help + this._helpAndError(); + } - // If there were no args and we have unknown options, - // then they are extraneous and we need to error. - if (unknown.length > 0 && !this.defaultExecutable) { - this.unknownOption(unknown[0]); + outputHelpIfRequested(this, parsed.unknown); + this._checkForMissingMandatoryOptions(); + if (parsed.unknown.length > 0) { + this.unknownOption(parsed.unknown[0]); } - // Call the program action handler, unless it has a (missing) required parameter and signature does not match. - if (this._args.filter(function(a) { return a.required; }).length === 0) { - this.emit('program-command'); + + if (this._actionHandler) { + const self = this; + const args = this.args.slice(); + this._args.forEach(function(arg, i) { + if (arg.required && args[i] == null) { + self.missingArgument(arg.name); + } else if (arg.variadic) { + if (i !== self._args.length - 1) { + self.variadicArgNotLast(arg.name); + } + + args[i] = args.splice(i); + } + }); + + this._actionHandler(args); + // this.emit('command:' + this.name(), operands, unknown); // still needed???? + } else { + this.emit('command:*', operands, unknown); } } +}; - return this; +/** + * Find matching command. + * + * @api private + */ +Command.prototype._findCommand = function(name) { + if (!name) return undefined; + return this.commands.find(cmd => cmd._name === name || cmd._alias === name); }; /** @@ -867,7 +826,7 @@ Command.prototype.parseArgs = function(operands, unknown) { * @api private */ -Command.prototype.optionFor = function(arg) { +Command.prototype._findOption = function(arg) { return this.options.find(option => option.is(arg)); }; @@ -927,7 +886,7 @@ Command.prototype.parseOptions = function(argv) { } if (maybeOption(arg)) { - const option = this.optionFor(arg); + const option = this._findOption(arg); // recognised option, call listener to assign value with possible custom processing if (option) { if (option.required) { @@ -950,7 +909,7 @@ Command.prototype.parseOptions = function(argv) { // Look for combo options following single dash, eat first one if known. if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { - const option = this.optionFor(`-${arg[1]}`); + const option = this._findOption(`-${arg[1]}`); if (option) { if (option.required || option.optional) { // option with value following in same argument @@ -967,7 +926,7 @@ Command.prototype.parseOptions = function(argv) { // Look for known long flag with value, like --foo=bar if (/^--[^=]+=/.test(arg)) { const index = arg.indexOf('='); - const option = this.optionFor(arg.slice(0, index)); + const option = this._findOption(arg.slice(0, index)); if (option && (option.required || option.optional)) { this.emit(`option:${option.name()}`, arg.slice(index + 1)); continue; @@ -1447,6 +1406,18 @@ Command.prototype.help = function(cb) { this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)'); }; +/** + * Output help information and exit. Display for error situations. + * + * @api private + */ + +Command.prototype._helpAndError = function() { + this.outputHelp(); + // message: do not have all displayed text available so only passing placeholder. + this._exit(1, 'commander.help', '(outputHelp)'); +}; + /** * Camel-case the given `flag` * @@ -1554,17 +1525,6 @@ function humanReadableArgName(arg) { : '[' + nameOutput + ']'; } -// for versions before node v0.8 when there weren't `fs.existsSync` -function exists(file) { - try { - if (fs.statSync(file).isFile()) { - return true; - } - } catch (e) { - return false; - } -} - /** * Scan arguments and increment port number for inspect calls (to avoid conflicts when spawning new command). * diff --git a/tests/command.action.test.js b/tests/command.action.test.js index 4253e0d4b..447028933 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -46,13 +46,19 @@ test('when .action on program with required argument and argument supplied then }); test('when .action on program with required argument and argument not supplied then action not called', () => { + // Optional. Use internal knowledge to suppress output to keep test output clean. + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); const actionMock = jest.fn(); const program = new commander.Command(); program + .exitOverride() .arguments('') .action(actionMock); - program.parse(['node', 'test']); + expect(() => { + program.parse(['node', 'test']); + }).toThrow(); expect(actionMock).not.toHaveBeenCalled(); + consoleErrorSpy.mockRestore(); }); // Changes made in #729 to call program action handler diff --git a/tests/command.asterisk.test.js b/tests/command.asterisk.test.js index 19d0257f9..c458b7b8c 100644 --- a/tests/command.asterisk.test.js +++ b/tests/command.asterisk.test.js @@ -10,7 +10,7 @@ const commander = require('../'); // // Historical: the event 'command:*' used to also be shared by the action handler on the program. -describe(".command('*')", () => { +describe.skip(".command('*')", () => { test('when no arguments then asterisk action not called', () => { const mockAction = jest.fn(); const program = new commander.Command(); @@ -58,7 +58,7 @@ describe(".command('*')", () => { }); // Test .on explicitly rather than assuming covered by .command -describe(".on('command:*')", () => { +describe.skip(".on('command:*')", () => { test('when no arguments then listener not called', () => { const mockAction = jest.fn(); const program = new commander.Command(); diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index db5fd148c..2521c673e 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -135,9 +135,8 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - // This is effectively treated as a deliberate request for help, rather than an error. expect(writeSpy).toHaveBeenCalled(); - expectCommanderError(caughtErr, 0, 'commander.helpDisplayed', '(outputHelp)'); + expectCommanderError(caughtErr, 1, 'commander.help', '(outputHelp)'); }); test('when specify --version then throw CommanderError', () => { diff --git a/tests/command.help.test.js b/tests/command.help.test.js index b61fbc016..6112d44e4 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -11,7 +11,7 @@ test('when call helpInformation for program then help format is as expected (usa program .command('my-command '); const expectedHelpInformation = -`Usage: test [options] [command] +`Usage: hack [options] [command] Options: -h, --help output usage information @@ -20,7 +20,7 @@ Commands: my-command `; - program.parse(['node', 'test']); + program._name = 'hack'; const helpInformation = program.helpInformation(); expect(helpInformation).toBe(expectedHelpInformation); }); @@ -30,7 +30,7 @@ test('when use .description for command then help incudes description', () => { program .command('simple-command') .description('custom-description'); - program.parse(['node', 'test']); + program._help = 'test'; const helpInformation = program.helpInformation(); expect(helpInformation).toMatch(/simple-command +custom-description/); }); diff --git a/tests/command.usage.test.js b/tests/command.usage.test.js index 7c3616813..655e09e96 100644 --- a/tests/command.usage.test.js +++ b/tests/command.usage.test.js @@ -3,10 +3,10 @@ const commander = require('../'); test('when default usage and check program help then starts with default usage', () => { const program = new commander.Command(); - program.parse(['node', 'test']); + program._name = 'hack'; const helpInformation = program.helpInformation(); - expect(helpInformation).toMatch(new RegExp('^Usage: test \\[options\\]')); + expect(helpInformation).toMatch(new RegExp('^Usage: hack \\[options\\]')); }); test('when custom usage and check program help then starts with custom usage', () => { @@ -15,10 +15,10 @@ test('when custom usage and check program help then starts with custom usage', ( program .usage(myUsage); - program.parse(['node', 'test']); + program._name = 'hack'; const helpInformation = program.helpInformation(); - expect(helpInformation).toMatch(new RegExp(`^Usage: test ${myUsage}`)); + expect(helpInformation).toMatch(new RegExp(`^Usage: hack ${myUsage}`)); }); test('when default usage and check subcommand help then starts with default usage including program name', () => { @@ -26,10 +26,10 @@ test('when default usage and check subcommand help then starts with default usag const subCommand = program .command('info'); - program.parse(['node', 'test']); + program._name = 'hack'; const helpInformation = subCommand.helpInformation(); - expect(helpInformation).toMatch(new RegExp('^Usage: test info \\[options\\]')); + expect(helpInformation).toMatch(new RegExp('^Usage: hack info \\[options\\]')); }); test('when custom usage and check subcommand help then starts with custom usage including program name', () => { @@ -39,8 +39,8 @@ test('when custom usage and check subcommand help then starts with custom usage .command('info') .usage(myUsage); - program.parse(['node', 'test']); + program._name = 'hack'; const helpInformation = subCommand.helpInformation(); - expect(helpInformation).toMatch(new RegExp(`^Usage: test info ${myUsage}`)); + expect(helpInformation).toMatch(new RegExp(`^Usage: hack info ${myUsage}`)); }); diff --git a/tests/options.mandatory.test.js b/tests/options.mandatory.test.js index 87b725473..921d7b7c1 100644 --- a/tests/options.mandatory.test.js +++ b/tests/options.mandatory.test.js @@ -218,10 +218,11 @@ describe('required command option with mandatory value not specified', () => { .exitOverride() .command('sub') .requiredOption('--subby ', 'description') - .action((cmd) => {}); + .action((cmd) => {}) + .command('sub2'); expect(() => { - program.parse(['node', 'test']); + program.parse(['node', 'test', 'sub2']); }).not.toThrow(); }); }); From 22c9ca5da083394e317997d0c4e306112402a77e Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 12 Jan 2020 23:15:12 +1300 Subject: [PATCH 02/37] Add check for requiredOption when calling executable subcommand --- index.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index c2b929fcd..132d75436 100644 --- a/index.js +++ b/index.js @@ -607,11 +607,6 @@ Command.prototype.parse = function(argv) { // if (args[0] === 'help') { // args[0] = args[1]; // args[1] = this._helpLongFlag; - // } else { - // // If calling through to executable subcommand we could check for help flags before failing, - // // but a somewhat unlikely case since program options not passed to executable subcommands. - // // Wait for reports to see if check needed and what usage pattern is. - // this._checkForMissingMandatoryOptions(); // } return this; @@ -643,6 +638,9 @@ Command.prototype._executeSubCommand = function(subcommand, args) { let launchWithNode = false; // Use node for source targets so do not need to get permissions correct, and on Windows. const sourceExt = ['.js', '.ts', '.mjs']; + // Not checking for help first. Unlikely to have mandatory and executable, and can't robustly test for help flags in external command. + this._checkForMissingMandatoryOptions(); + // Want the entry script as the reference for command name and directory for searching for other files. let scriptPath = this.rawArgs[1]; if (!fs.existsSync(scriptPath) || fs.statSync(scriptPath).isDirectory()) { @@ -832,12 +830,13 @@ Command.prototype._findOption = function(arg) { /** * Display an error message if a mandatory option does not have a value. + * Lazy calling after checking for help flags from leaf subcommand. * * @api private */ Command.prototype._checkForMissingMandatoryOptions = function() { - // Walk up hierarchy so can call from action handler after checking for displaying help. + // Walk up hierarchy so can call in subcommand after checking for displaying help. for (var cmd = this; cmd; cmd = cmd.parent) { cmd.options.forEach((anOption) => { if (anOption.mandatory && (cmd._getOptionValue(anOption.attributeName()) === undefined)) { @@ -1493,19 +1492,16 @@ function optionalWrap(str, width, indent) { * Output help information if help flags specified * * @param {Command} cmd - command to output help for - * @param {Array} options - array of options to search for -h or --help + * @param {Array} args - array of options to search for help flags * @api private */ -function outputHelpIfRequested(cmd, options) { - options = options || []; - - for (var i = 0; i < options.length; i++) { - if (options[i] === cmd._helpLongFlag || options[i] === cmd._helpShortFlag) { - cmd.outputHelp(); - // (Do not have all displayed text available so only passing placeholder.) - cmd._exit(0, 'commander.helpDisplayed', '(outputHelp)'); - } +function outputHelpIfRequested(cmd, args) { + const helpOption = 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)'); } } From de0f20ace028f4439930877550e6891b7ecf6d27 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 13 Jan 2020 08:56:09 +1300 Subject: [PATCH 03/37] Set program name using supported approach --- tests/command.help.test.js | 4 ++-- tests/command.usage.test.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 6112d44e4..5e91eaa5d 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -11,7 +11,7 @@ test('when call helpInformation for program then help format is as expected (usa program .command('my-command '); const expectedHelpInformation = -`Usage: hack [options] [command] +`Usage: test [options] [command] Options: -h, --help output usage information @@ -20,7 +20,7 @@ Commands: my-command `; - program._name = 'hack'; + program.name('test'); const helpInformation = program.helpInformation(); expect(helpInformation).toBe(expectedHelpInformation); }); diff --git a/tests/command.usage.test.js b/tests/command.usage.test.js index 655e09e96..6140ec93b 100644 --- a/tests/command.usage.test.js +++ b/tests/command.usage.test.js @@ -3,10 +3,10 @@ const commander = require('../'); test('when default usage and check program help then starts with default usage', () => { const program = new commander.Command(); - program._name = 'hack'; + program.name('test'); const helpInformation = program.helpInformation(); - expect(helpInformation).toMatch(new RegExp('^Usage: hack \\[options\\]')); + expect(helpInformation).toMatch(new RegExp('^Usage: test \\[options\\]')); }); test('when custom usage and check program help then starts with custom usage', () => { @@ -15,10 +15,10 @@ test('when custom usage and check program help then starts with custom usage', ( program .usage(myUsage); - program._name = 'hack'; + program.name('test'); const helpInformation = program.helpInformation(); - expect(helpInformation).toMatch(new RegExp(`^Usage: hack ${myUsage}`)); + expect(helpInformation).toMatch(new RegExp(`^Usage: test ${myUsage}`)); }); test('when default usage and check subcommand help then starts with default usage including program name', () => { @@ -26,10 +26,10 @@ test('when default usage and check subcommand help then starts with default usag const subCommand = program .command('info'); - program._name = 'hack'; + program.name('test'); const helpInformation = subCommand.helpInformation(); - expect(helpInformation).toMatch(new RegExp('^Usage: hack info \\[options\\]')); + expect(helpInformation).toMatch(new RegExp('^Usage: test info \\[options\\]')); }); test('when custom usage and check subcommand help then starts with custom usage including program name', () => { @@ -39,8 +39,8 @@ test('when custom usage and check subcommand help then starts with custom usage .command('info') .usage(myUsage); - program._name = 'hack'; + program.name('test'); const helpInformation = subCommand.helpInformation(); - expect(helpInformation).toMatch(new RegExp(`^Usage: hack info ${myUsage}`)); + expect(helpInformation).toMatch(new RegExp(`^Usage: test info ${myUsage}`)); }); From 7dc234920752b88d73eb47214619a84c21ac3785 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 13 Jan 2020 19:28:36 +1300 Subject: [PATCH 04/37] Add .addCommand, easy after previous work --- index.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 132d75436..c61e376d8 100644 --- a/index.js +++ b/index.js @@ -203,6 +203,18 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts return cmd; }; +/** + * @api public + */ + +Command.prototype.addCommand = function(cmd) { + if (!cmd._name) throw Error('error: addCommand name is not specified for command'); + + this.commands.push(cmd); + cmd.parent = this; + return this; +}; + /** * Define argument syntax for the top-level command. * @@ -720,7 +732,7 @@ Command.prototype._executeSubCommand = function(subcommand, args) { if (err.code === 'ENOENT') { console.error('error: %s(1) does not exist, try --help', bin); } else if (err.code === 'EACCES') { - console.error('error: %s(1) not executable. try chmod or run with root', bin); + console.error('error: %s(1) not executable', bin); } if (!exitCallback) { process.exit(1); From 5e43088e762afe60015a1db9e2388131313e6556 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 13 Jan 2020 20:19:34 +1300 Subject: [PATCH 05/37] Add support for default command using action handler - and remove stale _execs --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index c61e376d8..dd8d01674 100644 --- a/index.js +++ b/index.js @@ -122,7 +122,6 @@ exports.CommanderError = CommanderError; function Command(name) { this.commands = []; this.options = []; - this._execs = new Set(); this._allowUnknownOption = false; this._args = []; this._name = name || ''; @@ -182,9 +181,9 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts if (desc) { cmd.description(desc); cmd._executableHandler = true; - this._execs.add(cmd._name); - if (opts.isDefault) this._defaultCommandName = cmd._name; } + if (opts.isDefault) this._defaultCommandName = cmd._name; + cmd._noHelp = !!opts.noHelp; cmd._helpFlags = this._helpFlags; cmd._helpDescription = this._helpDescription; @@ -755,6 +754,7 @@ Command.prototype._dispatchSubcommand = function(operands, unknown) { if (subCommand) { operands = operands.slice(1); } else { + outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to subcommand subCommand = this._findCommand(this._defaultCommandName); } if (!subCommand) { From d4f87b2865bf45fb0e7341e2bfc67fee455f8fbd Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 14 Jan 2020 23:06:04 +1300 Subject: [PATCH 06/37] Add implicitHelpCommand and change help flags description --- Readme.md | 4 +-- Readme_zh-CN.md | 8 ++--- index.js | 65 +++++++++++++++++++------------------- tests/command.help.test.js | 2 +- tests/helpwrap.test.js | 10 +++--- 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/Readme.md b/Readme.md index e685f758e..47fbea7c3 100644 --- a/Readme.md +++ b/Readme.md @@ -444,7 +444,7 @@ Options: -b, --bbq Add bbq sauce -c, --cheese Add the specified type of cheese (default: "marble") -C, --no-cheese You do not want any cheese - -h, --help output usage information + -h, --help display help for command ``` ### Custom help @@ -488,7 +488,7 @@ Yields the following help output when `node script-name.js -h` or `node script-n Usage: custom-help [options] Options: - -h, --help output usage information + -h, --help display help for command -V, --version output the version number -f, --foo enable some foo -b, --bar enable some bar diff --git a/Readme_zh-CN.md b/Readme_zh-CN.md index ca2c8ace4..5347a567a 100644 --- a/Readme_zh-CN.md +++ b/Readme_zh-CN.md @@ -31,7 +31,7 @@ - [.help(cb)](#helpcb) - [自定义事件监听](#%e8%87%aa%e5%ae%9a%e4%b9%89%e4%ba%8b%e4%bb%b6%e7%9b%91%e5%90%ac) - [零碎知识](#%e9%9b%b6%e7%a2%8e%e7%9f%a5%e8%af%86) - - [避免选项命名冲突](#避免选项命名冲突) + - [避免选项命名冲突](#%e9%81%bf%e5%85%8d%e9%80%89%e9%a1%b9%e5%91%bd%e5%90%8d%e5%86%b2%e7%aa%81) - [TypeScript](#typescript) - [Node 选项例如 --harmony](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony) - [Node 调试](#node-%e8%b0%83%e8%af%95) @@ -39,7 +39,7 @@ - [例子](#%e4%be%8b%e5%ad%90) - [许可证](#%e8%ae%b8%e5%8f%af%e8%af%81) - [支持](#%e6%94%af%e6%8c%81) - - [企业使用Commander](#企业使用Commander) + - [企业使用Commander](#%e4%bc%81%e4%b8%9a%e4%bd%bf%e7%94%a8commander) ## 安装 @@ -435,7 +435,7 @@ Options: -b, --bbq Add bbq sauce -c, --cheese Add the specified type of cheese (default: "marble") -C, --no-cheese You do not want any cheese - -h, --help output usage information + -h, --help display help for command ``` ### 自定义帮助 @@ -474,7 +474,7 @@ console.log('stuff'); Usage: custom-help [options] Options: - -h, --help output usage information + -h, --help display help for command -V, --version output the version number -f, --foo enable some foo -b, --bar enable some bar diff --git a/index.js b/index.js index dd8d01674..fe6cd2e5f 100644 --- a/index.js +++ b/index.js @@ -135,9 +135,10 @@ function Command(name) { this._defaultCommandName = undefined; this._helpFlags = '-h, --help'; - this._helpDescription = 'output usage information'; + this._helpDescription = 'display help for command'; this._helpShortFlag = '-h'; this._helpLongFlag = '--help'; + this._implicitHelpCommand = undefined; } /** @@ -225,14 +226,27 @@ Command.prototype.arguments = function(desc) { }; /** - * Add an implicit `help [cmd]` subcommand - * which invokes `--help` for the given command. + * Override default decision whether to add implicit help command. * + * @return {Command} for chaining + * @api public + */ + +Command.prototype.addImplicitHelpCommand = function(enable) { + this._implicitHelpCommand = (enable === undefined) || !!enable; + return this; +}; + +/** + * @return {boolean} * @api private */ -Command.prototype.addImplicitHelpCommand = function() { - this.command('help [cmd]', 'display help for [cmd]'); +Command.prototype._hasImplicitHelpCommand = function() { + if (this._implicitHelpCommand === undefined) { + this._implicitHelpCommand = this.commands.length && !this._actionHandler && !this._findCommand('help'); + } + return this._implicitHelpCommand; }; /** @@ -599,9 +613,6 @@ Command.prototype._getOptionValue = function(key) { */ Command.prototype.parse = function(argv) { - // implicit help - // if (this._executables) this.addImplicitHelpCommand(); ???? - // store raw args this.rawArgs = argv; @@ -610,16 +621,6 @@ Command.prototype.parse = function(argv) { this._parseCommand([], argv.slice(2)); - // const args = this.args; - - // if (args[0] === 'help' && args.length === 1) this.help(); - - // // --help - // if (args[0] === 'help') { - // args[0] = args[1]; - // args[1] = this._helpLongFlag; - // } - return this; }; @@ -749,18 +750,9 @@ Command.prototype._executeSubCommand = function(subcommand, args) { /** * @api private */ -Command.prototype._dispatchSubcommand = function(operands, unknown) { - let subCommand = this._findCommand(operands[0]); - if (subCommand) { - operands = operands.slice(1); - } else { - outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to subcommand - subCommand = this._findCommand(this._defaultCommandName); - } - if (!subCommand) { - // ???? - throw new Error('Failed to find subcommand'); - } +Command.prototype._dispatchSubcommand = function(commandName, operands, unknown) { + const subCommand = this._findCommand(commandName); + if (!subCommand) this._helpAndError(); if (subCommand._executableHandler) { this._executeSubCommand(subCommand, operands.concat(unknown)); @@ -781,8 +773,17 @@ Command.prototype._parseCommand = function(operands, unknown) { unknown = parsed.unknown; this.args = operands.concat(unknown); - if (this._defaultCommandName || (operands && this._findCommand(operands[0]))) { - this._dispatchSubcommand(operands, unknown); + if (operands && this._findCommand(operands[0])) { + this._dispatchSubcommand(operands[0], operands.slice(1), unknown); + } else if (this._hasImplicitHelpCommand() && operands[0] === 'help') { + if (operands.length === 1) { + this.help(); + } else { + this._dispatchSubcommand(operands[1], [], [this._helpLongFlag]); + } + } else if (this._defaultCommandName) { + outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command + this._dispatchSubcommand(this._defaultCommandName, operands, unknown); } else { if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { // probaby missing subcommand and no handler, user needs help diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 5e91eaa5d..51e9ed5a7 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -14,7 +14,7 @@ test('when call helpInformation for program then help format is as expected (usa `Usage: test [options] [command] Options: - -h, --help output usage information + -h, --help display help for command Commands: my-command diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 1cfbd55df..61207c990 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -16,7 +16,7 @@ Options: -x -extra-long-option-switch kjsahdkajshkahd kajhsd akhds kashd kajhs dkha dkh aksd ka dkha kdh kasd ka kahs dkh sdkh askdh aksd kashdk ahsd kahs dkha skdh - -h, --help output usage information + -h, --help display help for command `; expect(program.helpInformation()).toBe(expectedOutput); @@ -36,7 +36,7 @@ test('when long option description and default then wrap and indent', () => { Options: -x -extra-long-option kjsahdkajshkahd kajhsd akhds (default: "aaa bbb ccc ddd eee fff ggg") - -h, --help output usage information + -h, --help display help for command `; expect(program.helpInformation()).toBe(expectedOutput); @@ -56,7 +56,7 @@ test('when long command description then wrap and indent', () => { Options: -x -extra-long-option-switch x - -h, --help output usage information + -h, --help display help for command Commands: alpha Lorem mollit quis dolor ex do eu quis ad insa @@ -80,7 +80,7 @@ test('when not enough room then help not wrapped', () => { `Usage: [options] [command] Options: - -h, --help output usage information + -h, --help display help for command Commands: 1234567801234567890x ${commandDescription} @@ -112,7 +112,7 @@ Options: Time can also be specified using special values: "dawn" - From night to sunrise. - -h, --help output usage information + -h, --help display help for command `; expect(program.helpInformation()).toBe(expectedOutput); From f51b0e323db2f023758b63fb04f8de8c6c0ddfc6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 14 Jan 2020 23:35:36 +1300 Subject: [PATCH 07/37] Add implicit help command to help --- index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fe6cd2e5f..a400540a4 100644 --- a/index.js +++ b/index.js @@ -1165,7 +1165,7 @@ Command.prototype.name = function(str) { */ Command.prototype.prepareCommands = function() { - return this.commands.filter(function(cmd) { + const commandDetails = this.commands.filter(function(cmd) { return !cmd._noHelp; }).map(function(cmd) { var args = cmd._args.map(function(arg) { @@ -1180,6 +1180,11 @@ Command.prototype.prepareCommands = function() { cmd._description ]; }); + + if (this._hasImplicitHelpCommand()) { + commandDetails.push(['help [command]', 'display help for command']); + } + return commandDetails; }; /** From e42152451a74277e0eba6fa7332f2578db07fa82 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 14 Jan 2020 23:45:48 +1300 Subject: [PATCH 08/37] Turn off implicit help command for most help tests --- tests/command.commandHelp.test.js | 2 ++ tests/command.help.test.js | 1 + tests/helpwrap.test.js | 2 ++ 3 files changed, 5 insertions(+) diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index 57428ce53..538adedea 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -5,6 +5,7 @@ const commander = require('../'); test('when program has command then appears in commandHelp', () => { const program = new commander.Command(); program + .addImplicitHelpCommand(false) .command('bare'); const commandHelp = program.commandHelp(); expect(commandHelp).toBe('Commands:\n bare\n'); @@ -13,6 +14,7 @@ test('when program has command then appears in commandHelp', () => { test('when program has command with optional arg then appears in commandHelp', () => { const program = new commander.Command(); program + .addImplicitHelpCommand(false) .command('bare [bare-arg]'); const commandHelp = program.commandHelp(); expect(commandHelp).toEqual('Commands:\n bare [bare-arg]\n'); diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 51e9ed5a7..5ed1daa42 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -18,6 +18,7 @@ Options: Commands: my-command + help [command] display help for command `; program.name('test'); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 61207c990..0baa114bb 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -49,6 +49,7 @@ test('when long command description then wrap and indent', () => { const program = new commander.Command(); program .option('-x -extra-long-option-switch', 'x') + .addImplicitHelpCommand(false) .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); const expectedOutput = @@ -74,6 +75,7 @@ test('when not enough room then help not wrapped', () => { const program = new commander.Command(); const commandDescription = 'description text of very long command which should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.'; program + .addImplicitHelpCommand(false) .command('1234567801234567890x', commandDescription); const expectedOutput = From a257e49c3a576605477d4103f343a1ab2c050552 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 17 Jan 2020 00:24:28 +1300 Subject: [PATCH 09/37] .addHelpCommand --- index.js | 39 +++++++++++++++++++++++-------- tests/command.commandHelp.test.js | 4 ++-- tests/helpwrap.test.js | 4 ++-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index a400540a4..dcc0167e3 100644 --- a/index.js +++ b/index.js @@ -138,7 +138,10 @@ function Command(name) { this._helpDescription = 'display help for command'; this._helpShortFlag = '-h'; this._helpLongFlag = '--help'; - this._implicitHelpCommand = undefined; + this._hasImplicitHelpCommand = undefined; + this._helpCommandName = 'help'; + this._helpCommandnameAndArgs = 'help [command]'; + this._helpCommandDescription = 'display help for command'; } /** @@ -190,6 +193,9 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts cmd._helpDescription = this._helpDescription; cmd._helpShortFlag = this._helpShortFlag; cmd._helpLongFlag = this._helpLongFlag; + cmd._helpCommandName = this._helpCommandName; + cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs; + cmd._helpCommandDescription = this._helpCommandDescription; cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; @@ -228,12 +234,25 @@ Command.prototype.arguments = function(desc) { /** * Override default decision whether to add implicit help command. * + * addHelpCommand() // force on + * addHelpCommand(false); // force off + * addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom detais + * * @return {Command} for chaining * @api public */ -Command.prototype.addImplicitHelpCommand = function(enable) { - this._implicitHelpCommand = (enable === undefined) || !!enable; +Command.prototype.addHelpCommand = function(enableOrNameAndArgs, description) { + if (enableOrNameAndArgs === false) { + this._hasImplicitHelpCommand = false; + } else { + this._hasImplicitHelpCommand = true; + if (typeof enableOrNameAndArgs === 'string') { + this._helpCommandName = enableOrNameAndArgs.split(' ')[0]; + this._helpCommandnameAndArgs = enableOrNameAndArgs; + } + this._helpCommandDescription = description || this._helpCommandDescription; + } return this; }; @@ -242,11 +261,11 @@ Command.prototype.addImplicitHelpCommand = function(enable) { * @api private */ -Command.prototype._hasImplicitHelpCommand = function() { - if (this._implicitHelpCommand === undefined) { - this._implicitHelpCommand = this.commands.length && !this._actionHandler && !this._findCommand('help'); +Command.prototype._lazyHasImplicitHelpCommand = function() { + if (this._hasImplicitHelpCommand === undefined) { + this._hasImplicitHelpCommand = this.commands.length && !this._actionHandler && !this._findCommand('help'); } - return this._implicitHelpCommand; + return this._hasImplicitHelpCommand; }; /** @@ -775,7 +794,7 @@ Command.prototype._parseCommand = function(operands, unknown) { if (operands && this._findCommand(operands[0])) { this._dispatchSubcommand(operands[0], operands.slice(1), unknown); - } else if (this._hasImplicitHelpCommand() && operands[0] === 'help') { + } else if (this._lazyHasImplicitHelpCommand() && operands[0] === this._helpCommandName) { if (operands.length === 1) { this.help(); } else { @@ -1181,8 +1200,8 @@ Command.prototype.prepareCommands = function() { ]; }); - if (this._hasImplicitHelpCommand()) { - commandDetails.push(['help [command]', 'display help for command']); + if (this._lazyHasImplicitHelpCommand()) { + commandDetails.push([this._helpCommandnameAndArgs, this._helpCommandDescription]); } return commandDetails; }; diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index 538adedea..37b3e24c3 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -5,7 +5,7 @@ const commander = require('../'); test('when program has command then appears in commandHelp', () => { const program = new commander.Command(); program - .addImplicitHelpCommand(false) + .addHelpCommand(false) .command('bare'); const commandHelp = program.commandHelp(); expect(commandHelp).toBe('Commands:\n bare\n'); @@ -14,7 +14,7 @@ test('when program has command then appears in commandHelp', () => { test('when program has command with optional arg then appears in commandHelp', () => { const program = new commander.Command(); program - .addImplicitHelpCommand(false) + .addHelpCommand(false) .command('bare [bare-arg]'); const commandHelp = program.commandHelp(); expect(commandHelp).toEqual('Commands:\n bare [bare-arg]\n'); diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 0baa114bb..444aa480a 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -49,7 +49,7 @@ test('when long command description then wrap and indent', () => { const program = new commander.Command(); program .option('-x -extra-long-option-switch', 'x') - .addImplicitHelpCommand(false) + .addHelpCommand(false) .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); const expectedOutput = @@ -75,7 +75,7 @@ test('when not enough room then help not wrapped', () => { const program = new commander.Command(); const commandDescription = 'description text of very long command which should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.'; program - .addImplicitHelpCommand(false) + .addHelpCommand(false) .command('1234567801234567890x', commandDescription); const expectedOutput = From 6b7a7c6e6ee047af699000fa7c961ba071d46eaf Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 18 Jan 2020 22:57:34 +1300 Subject: [PATCH 10/37] Remove addHelpCommand from tests and make match more narrow --- tests/command.commandHelp.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index 37b3e24c3..4f6198c94 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -5,17 +5,15 @@ const commander = require('../'); test('when program has command then appears in commandHelp', () => { const program = new commander.Command(); program - .addHelpCommand(false) .command('bare'); const commandHelp = program.commandHelp(); - expect(commandHelp).toBe('Commands:\n bare\n'); + expect(commandHelp).toMatch(/Commands:\n +bare\n/); }); test('when program has command with optional arg then appears in commandHelp', () => { const program = new commander.Command(); program - .addHelpCommand(false) .command('bare [bare-arg]'); const commandHelp = program.commandHelp(); - expect(commandHelp).toEqual('Commands:\n bare [bare-arg]\n'); + expect(commandHelp).toMatch(/Commands:\n +bare \[bare-arg\]\n/); }); From b5b062c686f52aaab89b58d0635e5de51da5b3ec Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 18 Jan 2020 22:57:53 +1300 Subject: [PATCH 11/37] Use test of complete default help output --- tests/helpwrap.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/helpwrap.test.js b/tests/helpwrap.test.js index 444aa480a..22f296693 100644 --- a/tests/helpwrap.test.js +++ b/tests/helpwrap.test.js @@ -1,6 +1,7 @@ const commander = require('../'); // Test auto wrap and indent with some manual strings. +// Fragile tests with complete help output. test('when long option description then wrap and indent', () => { const oldColumns = process.stdout.columns; @@ -49,7 +50,6 @@ test('when long command description then wrap and indent', () => { const program = new commander.Command(); program .option('-x -extra-long-option-switch', 'x') - .addHelpCommand(false) .command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.'); const expectedOutput = @@ -62,6 +62,7 @@ Options: Commands: alpha Lorem mollit quis dolor ex do eu quis ad insa a commodo esse. + help [command] display help for command `; expect(program.helpInformation()).toBe(expectedOutput); @@ -75,7 +76,6 @@ test('when not enough room then help not wrapped', () => { const program = new commander.Command(); const commandDescription = 'description text of very long command which should not be automatically be wrapped. Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu.'; program - .addHelpCommand(false) .command('1234567801234567890x', commandDescription); const expectedOutput = @@ -86,6 +86,7 @@ Options: Commands: 1234567801234567890x ${commandDescription} + help [command] display help for command `; expect(program.helpInformation()).toBe(expectedOutput); From 3be5ba3a0d0d565651b5b3cbd89b3f47d10bd423 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 18 Jan 2020 23:30:33 +1300 Subject: [PATCH 12/37] Add tests for whether implicit help appears in help --- index.js | 2 +- tests/command.helpCommand.test.js | 38 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/command.helpCommand.test.js diff --git a/index.js b/index.js index dcc0167e3..35f74b15d 100644 --- a/index.js +++ b/index.js @@ -1305,7 +1305,7 @@ Command.prototype.optionHelp = function() { */ Command.prototype.commandHelp = function() { - if (!this.commands.length) return ''; + if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return ''; var commands = this.prepareCommands(); var width = this.padWidth(); diff --git a/tests/command.helpCommand.test.js b/tests/command.helpCommand.test.js new file mode 100644 index 000000000..da9499379 --- /dev/null +++ b/tests/command.helpCommand.test.js @@ -0,0 +1,38 @@ +const commander = require('../'); + +describe('help command in displayed help', () => { + test('when program has no subcommands then no automatic help command', () => { + const program = new commander.Command(); + const helpInformation = program.helpInformation(); + expect(helpInformation).not.toMatch(/help \[command\]/); + }); + + test('when program has no subcommands and add help command then has help command', () => { + const program = new commander.Command(); + program.addHelpCommand(true); + const helpInformation = program.helpInformation(); + expect(helpInformation).toMatch(/help \[command\]/); + }); + + test('when program has subcommands then has automatic help command', () => { + const program = new commander.Command(); + program.command('foo'); + const helpInformation = program.helpInformation(); + expect(helpInformation).toMatch(/help \[command\]/); + }); + + test('when program has subcommands and suppress help command then no help command', () => { + const program = new commander.Command(); + program.addHelpCommand(false); + program.command('foo'); + const helpInformation = program.helpInformation(); + expect(helpInformation).not.toMatch(/help \[command\]/); + }); + + test('when add custom help command then custom help command', () => { + const program = new commander.Command(); + program.addHelpCommand('help command', 'help description'); + const helpInformation = program.helpInformation(); + expect(helpInformation).toMatch(/help command +help description/); + }); +}); From b4c055b9b36839b8dfb069b4d3f8dae14cef780c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 08:24:07 +1300 Subject: [PATCH 13/37] Add tests that help command dispatched to correct command --- tests/command.helpCommand.test.js | 64 ++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/command.helpCommand.test.js b/tests/command.helpCommand.test.js index da9499379..be2d5a5b3 100644 --- a/tests/command.helpCommand.test.js +++ b/tests/command.helpCommand.test.js @@ -1,6 +1,6 @@ const commander = require('../'); -describe('help command in displayed help', () => { +describe('help command listed in helpInformation', () => { test('when program has no subcommands then no automatic help command', () => { const program = new commander.Command(); const helpInformation = program.helpInformation(); @@ -36,3 +36,65 @@ describe('help command in displayed help', () => { expect(helpInformation).toMatch(/help command +help description/); }); }); + +describe('help command processed on correct command', () => { + // Use internal knowledge to suppress output to keep test output clean. + let consoleErrorSpy; + let writeSpy; + + beforeAll(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + }); + + afterEach(() => { + consoleErrorSpy.mockClear(); + writeSpy.mockClear(); + }); + + afterAll(() => { + consoleErrorSpy.mockRestore(); + writeSpy.mockRestore(); + }); + + test('when "program help" then program', () => { + const program = new commander.Command(); + program.exitOverride(); + program.command('sub1'); + program.exitOverride(() => { throw new Error('program'); }); + expect(() => { + program.parse('node test.js help'.split(' ')); + }).toThrow('program'); + }); + + test('when "program help sub1" then sub1', () => { + const program = new commander.Command(); + program.exitOverride(); + const sub1 = program.command('sub1'); + sub1.exitOverride(() => { throw new Error('sub1'); }); + expect(() => { + program.parse('node test.js help sub1'.split(' ')); + }).toThrow('sub1'); + }); + + test('when "program sub1 help sub2" then sub2', () => { + const program = new commander.Command(); + program.exitOverride(); + const sub1 = program.command('sub1'); + const sub2 = sub1.command('sub2'); + sub2.exitOverride(() => { throw new Error('sub2'); }); + expect(() => { + program.parse('node test.js sub1 help sub2'.split(' ')); + }).toThrow('sub2'); + }); + + test('when default command and "program help" then program', () => { + const program = new commander.Command(); + program.exitOverride(); + program.command('sub1', { isDefault: true }); + program.exitOverride(() => { throw new Error('program'); }); + expect(() => { + program.parse('node test.js help'.split(' ')); + }).toThrow('program'); + }); +}); From c4557328f28b9bdbedbe4c8a7f07d4b1983e375a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 08:30:39 +1300 Subject: [PATCH 14/37] Add simple nested subcommand test --- tests/command.nested.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/command.nested.test.js diff --git a/tests/command.nested.test.js b/tests/command.nested.test.js new file mode 100644 index 000000000..9445061b2 --- /dev/null +++ b/tests/command.nested.test.js @@ -0,0 +1,12 @@ +const commander = require('../'); + +test('when call nested subcommand then runs', () => { + const program = new commander.Command(); + const leafAction = jest.fn(); + program + .command('sub1') + .command('sub2') + .action(leafAction); + program.parse('node test.js sub1 sub2'.split(' ')); + expect(leafAction).toHaveBeenCalled(); +}); From bb9fe74fbee7bf8c5ee520157ee8de87c30245d5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 09:10:06 +1300 Subject: [PATCH 15/37] Add default command tests for action based subcommand --- tests/command.default.test.js | 61 +++++++++++++++++++ ...mmand.executableSubcommand.default.test.js | 27 -------- 2 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 tests/command.default.test.js delete mode 100644 tests/command.executableSubcommand.default.test.js diff --git a/tests/command.default.test.js b/tests/command.default.test.js new file mode 100644 index 000000000..ce54783cd --- /dev/null +++ b/tests/command.default.test.js @@ -0,0 +1,61 @@ +const childProcess = require('child_process'); +const path = require('path'); +const commander = require('../'); + +describe('default executable command', () => { + // Calling node explicitly so pm works without file suffix cross-platform. + const pm = path.join(__dirname, './fixtures/pm'); + + test('when default subcommand and no command then call default', (done) => { + childProcess.exec(`node ${pm}`, function(_error, stdout, stderr) { + expect(stdout).toBe('default\n'); + done(); + }); + }); + + test('when default subcommand and unrecognised argument then call default with argument', (done) => { + childProcess.exec(`node ${pm} an-argument`, function(_error, stdout, stderr) { + expect(stdout).toBe("default\n[ 'an-argument' ]\n"); + done(); + }); + }); + + test('when default subcommand and unrecognised option then call default with option', (done) => { + childProcess.exec(`node ${pm} --an-option`, function(_error, stdout, stderr) { + expect(stdout).toBe("default\n[ '--an-option' ]\n"); + done(); + }); + }); +}); + +describe('defaut action command', () => { + function makeProgram() { + const program = new commander.Command(); + const actionMock = jest.fn(); + program + .command('other'); + program + .command('default', { isDefault: true }) + .allowUnknownOption() + .action(actionMock); + return { program, actionMock }; + } + + test('when default subcommand and no command then call default', () => { + const { program, actionMock } = makeProgram(); + program.parse('node test.js'.split(' ')); + expect(actionMock).toHaveBeenCalled(); + }); + + test('when default subcommand and unrecognised argument then call default with argument', () => { + const { program, actionMock } = makeProgram(); + program.parse('node test.js an-argument'.split(' ')); + expect(actionMock).toHaveBeenCalled(); + }); + + test('when default subcommand and unrecognised option then call default with option', () => { + const { program, actionMock } = makeProgram(); + program.parse('node test.js --an-option'.split(' ')); + expect(actionMock).toHaveBeenCalled(); + }); +}); diff --git a/tests/command.executableSubcommand.default.test.js b/tests/command.executableSubcommand.default.test.js deleted file mode 100644 index c3cf1fe74..000000000 --- a/tests/command.executableSubcommand.default.test.js +++ /dev/null @@ -1,27 +0,0 @@ -const childProcess = require('child_process'); -const path = require('path'); - -// Calling node explicitly so pm works without file suffix cross-platform. - -const pm = path.join(__dirname, './fixtures/pm'); - -test('when default subcommand and no command then call default', (done) => { - childProcess.exec(`node ${pm}`, function(_error, stdout, stderr) { - expect(stdout).toBe('default\n'); - done(); - }); -}); - -test('when default subcommand and unrecognised argument then call default with argument', (done) => { - childProcess.exec(`node ${pm} an-argument`, function(_error, stdout, stderr) { - expect(stdout).toBe("default\n[ 'an-argument' ]\n"); - done(); - }); -}); - -test('when default subcommand and unrecognised option then call default with option', (done) => { - childProcess.exec(`node ${pm} --an-option`, function(_error, stdout, stderr) { - expect(stdout).toBe("default\n[ '--an-option' ]\n"); - done(); - }); -}); From f5b0798df932ab183c5f1cdf3670099e695d7cde Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 09:14:47 +1300 Subject: [PATCH 16/37] Remove mainModule, out of scope for current PR --- index.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/index.js b/index.js index 35f74b15d..a5eb23be5 100644 --- a/index.js +++ b/index.js @@ -673,14 +673,7 @@ Command.prototype._executeSubCommand = function(subcommand, args) { this._checkForMissingMandatoryOptions(); // Want the entry script as the reference for command name and directory for searching for other files. - let scriptPath = this.rawArgs[1]; - if (!fs.existsSync(scriptPath) || fs.statSync(scriptPath).isDirectory()) { - // When launched using "node foo" will be missing file extension, or "node ." will be pointing at module instead of file. - // Try process.mainModule.filename as a fallback. - if (process.mainModule && process.mainModule.filename) { - scriptPath = process.mainModule.filename; - } - } + const scriptPath = this.rawArgs[1]; let baseDir; try { From b28a0889a8a9913afda1d6488c0e1791d0813304 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 10:09:46 +1300 Subject: [PATCH 17/37] Add legacy asterisk handling and tests --- index.js | 8 ++++++-- tests/command.asterisk.test.js | 22 +++++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index a5eb23be5..b69477a12 100644 --- a/index.js +++ b/index.js @@ -824,8 +824,12 @@ Command.prototype._parseCommand = function(operands, unknown) { }); this._actionHandler(args); - // this.emit('command:' + this.name(), operands, unknown); // still needed???? - } else { + this.emit('command:' + this.name(), operands, unknown); // retain ???? + } else if (operands.length) { + // legacy behaviours, command and listener + if (this._findCommand('*')) { + this._dispatchSubcommand('*', operands, unknown); + } this.emit('command:*', operands, unknown); } } diff --git a/tests/command.asterisk.test.js b/tests/command.asterisk.test.js index c458b7b8c..cb815d835 100644 --- a/tests/command.asterisk.test.js +++ b/tests/command.asterisk.test.js @@ -10,14 +10,30 @@ const commander = require('../'); // // Historical: the event 'command:*' used to also be shared by the action handler on the program. -describe.skip(".command('*')", () => { +describe(".command('*')", () => { + // Use internal knowledge to suppress output to keep test output clean. + let writeMock; + + beforeAll(() => { + writeMock = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + }); + + afterAll(() => { + writeMock.mockRestore(); + }); + test('when no arguments then asterisk action not called', () => { const mockAction = jest.fn(); const program = new commander.Command(); program + .exitOverride() // to catch help .command('*') .action(mockAction); - program.parse(['node', 'test']); + try { + program.parse(['node', 'test']); + } catch (err) { + ; + } expect(mockAction).not.toHaveBeenCalled(); }); @@ -58,7 +74,7 @@ describe.skip(".command('*')", () => { }); // Test .on explicitly rather than assuming covered by .command -describe.skip(".on('command:*')", () => { +describe(".on('command:*')", () => { test('when no arguments then listener not called', () => { const mockAction = jest.fn(); const program = new commander.Command(); From 81185fea2b2bd3273ecbe275082a4accfdc114dd Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 21:52:54 +1300 Subject: [PATCH 18/37] Add more initialisation so object in known state --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index b69477a12..832bc80a0 100644 --- a/index.js +++ b/index.js @@ -133,7 +133,9 @@ function Command(name) { this._executableHandler = false; this._executableFile = undefined; // custom name for executable this._defaultCommandName = undefined; + this._exitCallback = undefined; + this._noHelp = false; this._helpFlags = '-h, --help'; this._helpDescription = 'display help for command'; this._helpShortFlag = '-h'; From 9d20c11725802aded8bd10e8cfa7e436797c4cc9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 21:53:10 +1300 Subject: [PATCH 19/37] Tests for addCommand --- tests/command.addCommand.test.js | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/command.addCommand.test.js diff --git a/tests/command.addCommand.test.js b/tests/command.addCommand.test.js new file mode 100644 index 000000000..688818953 --- /dev/null +++ b/tests/command.addCommand.test.js @@ -0,0 +1,43 @@ +const commander = require('../'); + +// simple sanity check subcommand works +test('when addCommand and specify subcommand then called', () => { + const program = new commander.Command(); + const leafAction = jest.fn(); + const sub = new commander.Command(); + sub + .name('sub') + .action(leafAction); + program + .addCommand(sub); + + program.parse('node test.js sub'.split(' ')); + expect(leafAction).toHaveBeenCalled(); +}); + +test('when commands added using .addCommand and .command then internals similar', () => { + const program1 = new commander.Command(); + program1.command('sub'); + const program2 = new commander.Command(); + program2.addCommand(new commander.Command('sub')); + + // This is a bit of a cheat to check using .addCommand() produces similar result to .command(), + // since .command() is well tested and understood. + + const cmd1 = program1.commands[0]; + const cmd2 = program2.commands[0]; + expect(cmd1.parent).toBe(program1); + expect(cmd2.parent).toBe(program2); + + for (const key of Object.keys(cmd1)) { + switch (typeof cmd1[key]) { + case 'string': + case 'boolean': + case 'number': + case 'undefined': + // Compare vaues in a way that will make some sense in test failure message. + expect(`${key}:${cmd1[key]}`).toEqual(`${key}:${cmd2[key]}`); + break; + } + } +}); From 460e3ec90757dbed5a291ec1319ae36101f41b24 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 19 Jan 2020 22:28:42 +1300 Subject: [PATCH 20/37] Add first cut at enhanced default error detection --- index.js | 12 ++++++++++-- tests/options.mandatory.test.js | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 832bc80a0..da13b0557 100644 --- a/index.js +++ b/index.js @@ -828,11 +828,19 @@ Command.prototype._parseCommand = function(operands, unknown) { this._actionHandler(args); this.emit('command:' + this.name(), operands, unknown); // retain ???? } else if (operands.length) { - // legacy behaviours, command and listener if (this._findCommand('*')) { this._dispatchSubcommand('*', operands, unknown); + } else if (this.listenerCount('command:*')) { + this.emit('command:*', operands, unknown); + } else if (this.commands.length) { + console.error(`Unrecognised command '${this.args[0]}'`); + this._helpAndError(); } - this.emit('command:*', operands, unknown); + } else if (this.commands.length) { + // This command has subcommands and nothing hooked up at this level, so display help. + this._helpAndError(); + } else { + // fall through for caller to handle after calling .parse() } } }; diff --git a/tests/options.mandatory.test.js b/tests/options.mandatory.test.js index 921d7b7c1..fb80d8d03 100644 --- a/tests/options.mandatory.test.js +++ b/tests/options.mandatory.test.js @@ -218,7 +218,8 @@ describe('required command option with mandatory value not specified', () => { .exitOverride() .command('sub') .requiredOption('--subby ', 'description') - .action((cmd) => {}) + .action((cmd) => {}); + program .command('sub2'); expect(() => { From 2a480a54def9ce709284932a65f5e46c4ceda7f7 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 21 Jan 2020 19:00:32 +1300 Subject: [PATCH 21/37] Add test that addCommand requires name --- index.js | 2 +- tests/command.addCommand.test.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index da13b0557..d5e6868b5 100644 --- a/index.js +++ b/index.js @@ -216,7 +216,7 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts */ Command.prototype.addCommand = function(cmd) { - if (!cmd._name) throw Error('error: addCommand name is not specified for command'); + if (!cmd._name) throw new Error('Command passed to .AddCommand must have a name'); this.commands.push(cmd); cmd.parent = this; diff --git a/tests/command.addCommand.test.js b/tests/command.addCommand.test.js index 688818953..d77e96562 100644 --- a/tests/command.addCommand.test.js +++ b/tests/command.addCommand.test.js @@ -35,9 +35,17 @@ test('when commands added using .addCommand and .command then internals similar' case 'boolean': case 'number': case 'undefined': - // Compare vaues in a way that will make some sense in test failure message. + // Compare vaues in a way that will be readable in test failure message. expect(`${key}:${cmd1[key]}`).toEqual(`${key}:${cmd2[key]}`); break; } } }); + +test('when command without name passed to .addCommand then throw', () => { + const program = new commander.Command(); + const cmd = new commander.Command(); + expect(() => { + program.addCommand(cmd); + }).toThrow(); +}); From 0736a773627cbb5991eb318beb2ea8c03039f49b Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 21 Jan 2020 21:01:19 +1300 Subject: [PATCH 22/37] Add block on automatic name generation for deeply nested executables --- tests/command.addCommand.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/command.addCommand.test.js b/tests/command.addCommand.test.js index d77e96562..d9773b17e 100644 --- a/tests/command.addCommand.test.js +++ b/tests/command.addCommand.test.js @@ -49,3 +49,21 @@ test('when command without name passed to .addCommand then throw', () => { program.addCommand(cmd); }).toThrow(); }); + +test('when executable command without custom executableFile passed to .addCommand then throw', () => { + const program = new commander.Command(); + const cmd = new commander.Command('sub'); + cmd.command('exec', 'exec description'); + expect(() => { + program.addCommand(cmd); + }).toThrow(); +}); + +test('when executable command with custom executableFile passed to .addCommand then ok', () => { + const program = new commander.Command(); + const cmd = new commander.Command('sub'); + cmd.command('exec', 'exec description', { executableFile: 'custom' }); + expect(() => { + program.addCommand(cmd); + }).not.toThrow(); +}); From ae611945984c6cd74155b276b29811b2fe62871b Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 21 Jan 2020 21:01:19 +1300 Subject: [PATCH 23/37] Add block on automatic name generation for deeply nested executables --- index.js | 12 ++++++++++++ tests/command.addCommand.test.js | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/index.js b/index.js index d5e6868b5..7ecaabac0 100644 --- a/index.js +++ b/index.js @@ -218,6 +218,18 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts Command.prototype.addCommand = function(cmd) { if (!cmd._name) throw new Error('Command passed to .AddCommand must have a name'); + // To keep things simple, block automatic name generation for deeply nested executables. + // Fail fast and detect when adding rather than later when parsing. + function checkExplicitNames(commandArray) { + commandArray.forEach((cmd) => { + if (cmd._executableHandler && !cmd._executableFile) { + throw new Error(`Must specify executableFile for deeply nested executable: ${cmd.name()}`); + } + checkExplicitNames(cmd.commands); + }); + } + checkExplicitNames(cmd.commands); + this.commands.push(cmd); cmd.parent = this; return this; diff --git a/tests/command.addCommand.test.js b/tests/command.addCommand.test.js index d77e96562..d9773b17e 100644 --- a/tests/command.addCommand.test.js +++ b/tests/command.addCommand.test.js @@ -49,3 +49,21 @@ test('when command without name passed to .addCommand then throw', () => { program.addCommand(cmd); }).toThrow(); }); + +test('when executable command without custom executableFile passed to .addCommand then throw', () => { + const program = new commander.Command(); + const cmd = new commander.Command('sub'); + cmd.command('exec', 'exec description'); + expect(() => { + program.addCommand(cmd); + }).toThrow(); +}); + +test('when executable command with custom executableFile passed to .addCommand then ok', () => { + const program = new commander.Command(); + const cmd = new commander.Command('sub'); + cmd.command('exec', 'exec description', { executableFile: 'custom' }); + expect(() => { + program.addCommand(cmd); + }).not.toThrow(); +}); From d740db26a34fc74add309656f0e9b413677a8b0c Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 21 Jan 2020 22:13:25 +1300 Subject: [PATCH 24/37] Fix describe name for tests --- tests/args.variadic.test.js | 2 +- tests/command.allowUnknownOptions.test.js | 2 +- tests/command.unknownOption.test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/args.variadic.test.js b/tests/args.variadic.test.js index aed5e4365..8be9f7a7c 100644 --- a/tests/args.variadic.test.js +++ b/tests/args.variadic.test.js @@ -2,7 +2,7 @@ const commander = require('../'); // Testing variadic arguments. Testing all the action arguments, but could test just variadicArg. -describe('.version', () => { +describe('variadic argument', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. let consoleErrorSpy; diff --git a/tests/command.allowUnknownOptions.test.js b/tests/command.allowUnknownOptions.test.js index f86ae9f75..da09df7a7 100644 --- a/tests/command.allowUnknownOptions.test.js +++ b/tests/command.allowUnknownOptions.test.js @@ -2,7 +2,7 @@ const commander = require('../'); // Not testing output, just testing whether an error is detected. -describe('.version', () => { +describe('allowUnknownOption', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. let consoleErrorSpy; diff --git a/tests/command.unknownOption.test.js b/tests/command.unknownOption.test.js index a4f571f68..18dbb6896 100644 --- a/tests/command.unknownOption.test.js +++ b/tests/command.unknownOption.test.js @@ -2,7 +2,7 @@ const commander = require('../'); // Checking for detection of unknown options, including regression tests for some past issues. -describe('.version', () => { +describe('unknownOption', () => { // Optional. Use internal knowledge to suppress output to keep test output clean. let consoleErrorSpy; From c529bff7cf82d6ded381a197ce2a709788bbe2e7 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 21 Jan 2020 22:14:29 +1300 Subject: [PATCH 25/37] Refine unknownCommand handling and add tests --- index.js | 16 +++++++-- tests/command.exitOverride.test.js | 17 +++++++++ tests/command.unknownCommand.test.js | 53 ++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 tests/command.unknownCommand.test.js diff --git a/index.js b/index.js index 7ecaabac0..3e4897717 100644 --- a/index.js +++ b/index.js @@ -845,8 +845,7 @@ Command.prototype._parseCommand = function(operands, unknown) { } else if (this.listenerCount('command:*')) { this.emit('command:*', operands, unknown); } else if (this.commands.length) { - console.error(`Unrecognised command '${this.args[0]}'`); - this._helpAndError(); + this.unknownCommand(); } } else if (this.commands.length) { // This command has subcommands and nothing hooked up at this level, so display help. @@ -1076,6 +1075,19 @@ Command.prototype.unknownOption = function(flag) { this._exit(1, 'commander.unknownOption', message); }; +/** + * Unknown command. + * + * @param {String} flag + * @api private + */ + +Command.prototype.unknownCommand = function() { + const message = `error: unrecognised command '${this.args[0]}' (use '${this._helpLongFlag}' for a list of commands)`; + console.error(message); + this._exit(1, 'commander.unknownCommand', message); +}; + /** * Variadic argument with `name` is not the last argument as required. * diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 2521c673e..89c237cc3 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -51,6 +51,23 @@ describe('.exitOverride and error details', () => { expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m'"); }); + test('when specify unknown command then throw CommanderError', () => { + const program = new commander.Command(); + program + .exitOverride() + .command('sub'); + + let caughtErr; + try { + program.parse(['node', 'test', 'oops']); + } catch (err) { + caughtErr = err; + } + + expect(consoleErrorSpy).toHaveBeenCalled(); + expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unrecognised command 'oops' (use '--help' for a list of commands)"); + }); + // Same error as above, but with custom handler. test('when supply custom handler then throw custom error', () => { const customError = new commander.CommanderError(123, 'custom-code', 'custom-message'); diff --git a/tests/command.unknownCommand.test.js b/tests/command.unknownCommand.test.js new file mode 100644 index 000000000..65e32ee49 --- /dev/null +++ b/tests/command.unknownCommand.test.js @@ -0,0 +1,53 @@ +const commander = require('../'); + +describe('unknownOption', () => { + // Optional. Use internal knowledge to suppress output to keep test output clean. + let consoleErrorSpy; + + beforeAll(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + }); + + afterEach(() => { + consoleErrorSpy.mockClear(); + }); + + afterAll(() => { + consoleErrorSpy.mockRestore(); + }); + + test('when unknown argument in simple program then no error', () => { + const program = new commander.Command(); + program.parse('node test.js unknown'.split(' ')); + }); + + test('when unknown command but action handler then no error', () => { + const program = new commander.Command(); + program.command('sub'); + program + .action(() => { }); + program.parse('node test.js unknown'.split(' ')); + }); + + test('when unknown command but listener then no error', () => { + const program = new commander.Command(); + program.command('sub'); + program + .on('command:*', () => { }); + program.parse('node test.js unknown'.split(' ')); + }); + + test('when unknown command then error', () => { + const program = new commander.Command(); + program + .exitOverride() + .command('sub'); + let caughtErr; + try { + program.parse('node test.js unknown'.split(' ')); + } catch (err) { + caughtErr = err; + } + expect(caughtErr.code).toBe('commander.unknownCommand'); + }); +}); From 365de9940aa5a960a0aa28e296dbe2c06b876f28 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 21 Jan 2020 22:58:46 +1300 Subject: [PATCH 26/37] Add suggestion to try help, when appropriate --- index.js | 10 +++++----- tests/command.executableSubcommand.lookup.test.js | 4 ++-- tests/command.exitOverride.test.js | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 3e4897717..4fd4acda6 100644 --- a/index.js +++ b/index.js @@ -756,7 +756,7 @@ Command.prototype._executeSubCommand = function(subcommand, args) { } proc.on('error', function(err) { if (err.code === 'ENOENT') { - console.error('error: %s(1) does not exist, try --help', bin); + console.error('error: %s(1) does not exist', bin); } else if (err.code === 'EACCES') { console.error('error: %s(1) not executable', bin); } @@ -838,7 +838,7 @@ Command.prototype._parseCommand = function(operands, unknown) { }); this._actionHandler(args); - this.emit('command:' + this.name(), operands, unknown); // retain ???? + this.emit('command:' + this.name(), operands, unknown); } else if (operands.length) { if (this._findCommand('*')) { this._dispatchSubcommand('*', operands, unknown); @@ -1056,7 +1056,7 @@ Command.prototype.optionMissingArgument = function(option, flag) { */ Command.prototype.missingMandatoryOptionValue = function(option) { - const message = `error: required option '${option.flags}' not specified`; + const message = `error: required option '${option.flags}' not specified (try '${this._helpLongFlag}' for more information)`; console.error(message); this._exit(1, 'commander.missingMandatoryOptionValue', message); }; @@ -1070,7 +1070,7 @@ Command.prototype.missingMandatoryOptionValue = function(option) { Command.prototype.unknownOption = function(flag) { if (this._allowUnknownOption) return; - const message = `error: unknown option '${flag}'`; + const message = `error: unknown option '${flag}' (try '${this._helpLongFlag}' for more information)`; console.error(message); this._exit(1, 'commander.unknownOption', message); }; @@ -1083,7 +1083,7 @@ Command.prototype.unknownOption = function(flag) { */ Command.prototype.unknownCommand = function() { - const message = `error: unrecognised command '${this.args[0]}' (use '${this._helpLongFlag}' for a list of commands)`; + const message = `error: unrecognised command '${this.args[0]}' (try '${this._helpLongFlag}' for more information)`; console.error(message); this._exit(1, 'commander.unknownCommand', message); }; diff --git a/tests/command.executableSubcommand.lookup.test.js b/tests/command.executableSubcommand.lookup.test.js index 6f28f8f5d..f18bfb694 100644 --- a/tests/command.executableSubcommand.lookup.test.js +++ b/tests/command.executableSubcommand.lookup.test.js @@ -12,7 +12,7 @@ test('when subcommand file missing then error', (done) => { // Get uncaught thrown error on Windows expect(stderr.length).toBeGreaterThan(0); } else { - expect(stderr).toBe('error: pm-list(1) does not exist, try --help\n'); + expect(stderr).toBe('error: pm-list(1) does not exist\n'); } done(); }); @@ -24,7 +24,7 @@ test('when alias subcommand file missing then error', (done) => { // Get uncaught thrown error on Windows expect(stderr.length).toBeGreaterThan(0); } else { - expect(stderr).toBe('error: pm-list(1) does not exist, try --help\n'); + expect(stderr).toBe('error: pm-list(1) does not exist\n'); } done(); }); diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 89c237cc3..494c7fea3 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -48,7 +48,7 @@ describe('.exitOverride and error details', () => { } expect(consoleErrorSpy).toHaveBeenCalled(); - expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m'"); + expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m' (try '--help' for more information)"); }); test('when specify unknown command then throw CommanderError', () => { @@ -65,7 +65,7 @@ describe('.exitOverride and error details', () => { } expect(consoleErrorSpy).toHaveBeenCalled(); - expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unrecognised command 'oops' (use '--help' for a list of commands)"); + expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unrecognised command 'oops' (try '--help' for more information)"); }); // Same error as above, but with custom handler. @@ -241,6 +241,6 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expectCommanderError(caughtErr, 1, 'commander.missingMandatoryOptionValue', `error: required option '${optionFlags}' not specified`); + expectCommanderError(caughtErr, 1, 'commander.missingMandatoryOptionValue', `error: required option '${optionFlags}' not specified (try '--help' for more information)`); }); }); From aa3805df6526344d17b714044328ad940c09def1 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 20:59:46 +1300 Subject: [PATCH 27/37] Fix typo --- tests/command.default.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/command.default.test.js b/tests/command.default.test.js index ce54783cd..619f0f97a 100644 --- a/tests/command.default.test.js +++ b/tests/command.default.test.js @@ -28,7 +28,7 @@ describe('default executable command', () => { }); }); -describe('defaut action command', () => { +describe('default action command', () => { function makeProgram() { const program = new commander.Command(); const actionMock = jest.fn(); From 4043f2b0c991847390b17950d028d9772f212418 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 21:02:49 +1300 Subject: [PATCH 28/37] Move common command configuration options in README, and add isDefault example program --- examples/defaultCommand.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/defaultCommand.js diff --git a/examples/defaultCommand.js b/examples/defaultCommand.js new file mode 100644 index 000000000..c8afa771f --- /dev/null +++ b/examples/defaultCommand.js @@ -0,0 +1,36 @@ +// const commander = require('commander'); // (normal include) +const commander = require('../'); // include commander in git clone of commander repo +const program = new commander.Command(); + +// Example program using the command configuration option isDefault to specify the default command. +// +// $ node defaultCommand.js build +// build +// $ node defaultCommand.js serve -p 8080 +// server on port 8080 +// $ node defaultCommand.js -p 443 +// server on port 443 + +program + .command('build') + .description('build web site for deployment') + .action(() => { + console.log('build'); + }); + +program + .command('deploy') + .description('deploy web site to production') + .action(() => { + console.log('deploy'); + }); + +program + .command('serve', { isDefault: true }) + .description('launch web server') + .option('-p,--port ', 'web port') + .action((opts) => { + console.log(`server on port ${opts.port}`); + }); + +program.parse(process.argv); From 24698c9f73a3befdd36773458c9f5d5edd0ae0eb Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 21:05:44 +1300 Subject: [PATCH 29/37] Add isDefault and example to README --- Readme.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Readme.md b/Readme.md index 47fbea7c3..6aa3be1db 100644 --- a/Readme.md +++ b/Readme.md @@ -11,7 +11,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Commander.js](#commanderjs) - [Installation](#installation) - - [Declaring program variable](#declaring-program-variable) + - [Declaring _program_ variable](#declaring-program-variable) - [Options](#options) - [Common option types, boolean and value](#common-option-types-boolean-and-value) - [Default option value](#default-option-value) @@ -33,7 +33,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Bits and pieces](#bits-and-pieces) - [Avoiding option name clashes](#avoiding-option-name-clashes) - [TypeScript](#typescript) - - [Node options such as --harmony](#node-options-such-as---harmony) + - [Node options such as `--harmony`](#node-options-such-as---harmony) - [Node debugging](#node-debugging) - [Override exit handling](#override-exit-handling) - [Examples](#examples) @@ -317,6 +317,8 @@ program .command('stop [service]', 'stop named service, or all if no name supplied'); ``` +Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified ([example](./examples/defaultCommand.js)). + ### Specify the argument syntax You use `.arguments` to specify the arguments for the top-level command, and for subcommands they are included in the `.command` call. Angled brackets (e.g. ``) indicate required input. Square brackets (e.g. `[optional]`) indicate optional input. @@ -397,13 +399,11 @@ async function main() { } ``` -A command's options on the command line are validated when the command is used. Any unknown options will be reported as an error. However, if an action-based command does not define an action, then the options are not validated. - -Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. +A command's options on the command line are validated when the command is used. Any unknown options will be reported as an error. ### Git-style executable (sub)commands -When `.command()` is invoked with a description argument, this tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools. +When `.command()` is invoked with a description argument, this tells commander that you're going to use separate executables for sub-commands, much like `git` and other popular tools. Commander will search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-subcommand`, like `pm-install`, `pm-search`. You can specify a custom name with the `executableFile` configuration option. @@ -422,9 +422,6 @@ program .parse(process.argv); ``` -Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified. -Specifying a name with `executableFile` will override the default constructed name. - If the program is designed to be installed globally, make sure the executables have proper modes, like `755`. ## Automated --help From cef94eb5904cd38ffcefdbd494beb7c05c2c1cab Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 21:34:15 +1300 Subject: [PATCH 30/37] Add nested commands --- Readme.md | 4 +++- examples/nestedCommands.js | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 examples/nestedCommands.js diff --git a/Readme.md b/Readme.md index 6aa3be1db..fd001ca4c 100644 --- a/Readme.md +++ b/Readme.md @@ -296,7 +296,9 @@ program.version('0.0.1', '-v, --vers', 'output the current version'); ## Commands -You can specify (sub)commands for your top-level command using `.command`. There are two ways these can be implemented: using an action handler attached to the command, or as a separate executable file (described in more detail later). In the first parameter to `.command` you specify the command name and any command arguments. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. +You can specify (sub)commands for your top-level command using `.command`. There are two ways these can be implemented: using an action handler attached to the command, or as a separate executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)). + +In the first parameter to `.command` you specify the command name and any command arguments. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. For example: diff --git a/examples/nestedCommands.js b/examples/nestedCommands.js new file mode 100644 index 000000000..3251ee5d8 --- /dev/null +++ b/examples/nestedCommands.js @@ -0,0 +1,47 @@ +// const commander = require('commander'); // (normal include) +const commander = require('../'); // include commander in git clone of commander repo +const program = new commander.Command(); + +// Commander supports nested subcommands. +// .command() can add a subcommand with an action handler or an executable. +// .addCommand() adds a prepared command with an actiomn handler. + +// Example output: +// +// $ node nestedCommands.js brew tea +// brew tea +// $ node nestedCommands.js heat jug +// heat jug + +// Add nested commands using `.command()`. +const brew = program.command('brew'); +brew + .command('tea') + .action(() => { + console.log('brew tea'); + }); +brew + .command('tea') + .action(() => { + console.log('brew tea'); + }); + +// Add nested commands using `.addCommand(). +// The command could be created separately in another module. +function makeHeatCommand() { + const heat = new commander.Command('heat'); + heat + .command('jug') + .action(() => { + console.log('heat jug'); + }); + heat + .command('pot') + .action(() => { + console.log('heat pot'); + }); + return heat; +} +program.addCommand(makeHeatCommand()); + +program.parse(process.argv); From e5b68f4497c0a3686e99a74bbeacc6fbb91b368d Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 22:19:55 +1300 Subject: [PATCH 31/37] Document .addHelpCommand, and tweaks --- Readme.md | 44 +++++++++++++++++++++++++++----------- Readme_zh-CN.md | 6 ++---- examples/custom-help | 4 +--- typings/commander-tests.ts | 4 +--- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Readme.md b/Readme.md index fd001ca4c..0dca3cf14 100644 --- a/Readme.md +++ b/Readme.md @@ -23,11 +23,12 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Specify the argument syntax](#specify-the-argument-syntax) - [Action handler (sub)commands](#action-handler-subcommands) - [Git-style executable (sub)commands](#git-style-executable-subcommands) - - [Automated --help](#automated---help) + - [Automated help](#automated-help) - [Custom help](#custom-help) - [.usage and .name](#usage-and-name) - [.outputHelp(cb)](#outputhelpcb) - [.helpOption(flags, description)](#helpoptionflags-description) + - [.addHelpCommand()](#addhelpcommand) - [.help(cb)](#helpcb) - [Custom event listeners](#custom-event-listeners) - [Bits and pieces](#bits-and-pieces) @@ -269,7 +270,7 @@ program program.parse(process.argv); ``` -``` +```bash $ pizza error: required option '-c, --cheese ' not specified ``` @@ -426,9 +427,10 @@ program If the program is designed to be installed globally, make sure the executables have proper modes, like `755`. -## Automated --help +## Automated help - The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free: + The help information is auto-generated based on the information commander already knows about your program. The default + help option is `-h,--help`. ```bash $ ./examples/pizza --help @@ -446,14 +448,22 @@ Options: -h, --help display help for command ``` +A `help` command is added by default if your command has subcommands. It can be used alone, or with a subcommand name to show +further help for the subcommand. These are effectively the same if the `shell` program has implicit help: + +```bash +shell help +shell --help + +shell help spawn +shell spawn --help +``` + ### Custom help - You can display arbitrary `-h, --help` information + You can display extra `-h, --help` information by listening for "--help". Commander will automatically - exit once you are done so that the remainder of your program - does not execute causing undesired behaviors, for example - in the following executable "stuff" will not output when - `--help` is used. + exit after displaying the help. ```js #!/usr/bin/env node @@ -466,9 +476,7 @@ program .option('-b, --bar', 'enable some bar') .option('-B, --baz', 'enable some baz'); -// must be before .parse() since -// node's emit() is immediate - +// must be before .parse() program.on('--help', function(){ console.log('') console.log('Examples:'); @@ -487,11 +495,11 @@ Yields the following help output when `node script-name.js -h` or `node script-n Usage: custom-help [options] Options: - -h, --help display help for command -V, --version output the version number -f, --foo enable some foo -b, --bar enable some bar -B, --baz enable some baz + -h, --help display help for command Examples: $ custom-help --help @@ -549,6 +557,16 @@ program .helpOption('-e, --HELP', 'read more information'); ``` +### .addHelpCommand() + +You can explicitly turn on or off the implicit help command with `.addHelpCommand()` and `.addHelpCommand(false)`. + +You can both turn on and customise the help command by supplying the name and description: + +```js +program.addHelpCommand('assist [command]', 'show assistance'); +``` + ### .help(cb) Output help information and exit immediately. diff --git a/Readme_zh-CN.md b/Readme_zh-CN.md index 5347a567a..147bc1c48 100644 --- a/Readme_zh-CN.md +++ b/Readme_zh-CN.md @@ -33,7 +33,7 @@ - [零碎知识](#%e9%9b%b6%e7%a2%8e%e7%9f%a5%e8%af%86) - [避免选项命名冲突](#%e9%81%bf%e5%85%8d%e9%80%89%e9%a1%b9%e5%91%bd%e5%90%8d%e5%86%b2%e7%aa%81) - [TypeScript](#typescript) - - [Node 选项例如 --harmony](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony) + - [Node 选项例如 `--harmony`](#node-%e9%80%89%e9%a1%b9%e4%be%8b%e5%a6%82---harmony) - [Node 调试](#node-%e8%b0%83%e8%af%95) - [重载退出(exit)处理](#%e9%87%8d%e8%bd%bd%e9%80%80%e5%87%baexit%e5%a4%84%e7%90%86) - [例子](#%e4%be%8b%e5%ad%90) @@ -453,9 +453,7 @@ program .option('-b, --bar', 'enable some bar') .option('-B, --baz', 'enable some baz'); -// must be before .parse() since -// node's emit() is immediate - +// must be before .parse() program.on('--help', function(){ console.log(''); console.log('Examples:'); diff --git a/examples/custom-help b/examples/custom-help index b4719ef50..668a48dfe 100755 --- a/examples/custom-help +++ b/examples/custom-help @@ -9,9 +9,7 @@ program .option('-b, --bar', 'enable some bar') .option('-B, --baz', 'enable some baz'); -// must be before .parse() since -// node's emit() is immediate - +// must be before .parse() program.on('--help', function() { console.log(''); console.log('Examples:'); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 7661bcf0e..465bbce0e 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -73,9 +73,7 @@ program .option('-b, --bar', 'enable some bar') .option('-B, --baz', 'enable some baz'); -// must be before .parse() since -// node's emit() is immediate - +// must be before .parse() program.on('--help', () => { console.log(' Examples:'); console.log(''); From b980c2d202d7c129d4bec66b7d662494d00d157a Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 22:45:04 +1300 Subject: [PATCH 32/37] Remove old default command, and rework command:* example --- Readme.md | 13 ++++--------- Readme_zh-CN.md | 6 ------ examples/deploy | 6 ------ 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/Readme.md b/Readme.md index 0dca3cf14..7f86990e6 100644 --- a/Readme.md +++ b/Readme.md @@ -581,9 +581,10 @@ program.on('option:verbose', function () { process.env.VERBOSE = this.verbose; }); -// error on unknown commands -program.on('command:*', function () { - console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args[0]]); +// custom error on unknown command +program.on('command:*', function (operands) { + console.error(`Invalid command '${operands[0]}'. Did you mean:`); + console.error(mySuggestions(operands[0])); process.exit(1); }); ``` @@ -703,12 +704,6 @@ program console.log(' $ deploy exec async'); }); -program - .command('*') - .action(function(env){ - console.log('deploying "%s"', env); - }); - program.parse(process.argv); ``` diff --git a/Readme_zh-CN.md b/Readme_zh-CN.md index 147bc1c48..7aceda565 100644 --- a/Readme_zh-CN.md +++ b/Readme_zh-CN.md @@ -668,12 +668,6 @@ program console.log(' $ deploy exec async'); }); -program - .command('*') - .action(function(env){ - console.log('deploying "%s"', env); - }); - program.parse(process.argv); ``` diff --git a/examples/deploy b/examples/deploy index d0821f86a..1ec4d2d26 100755 --- a/examples/deploy +++ b/examples/deploy @@ -34,10 +34,4 @@ program console.log(); }); -program - .command('*') - .action(function(env) { - console.log('deploying "%s"', env); - }); - program.parse(process.argv); From 30379ae117dc1a896242ab7d48269f60903a6346 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 22 Jan 2020 23:13:05 +1300 Subject: [PATCH 33/37] Document .addCommand --- Readme.md | 11 +++++++++-- index.js | 8 ++++++-- typings/commander-tests.ts | 3 +++ typings/index.d.ts | 14 ++++++-------- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Readme.md b/Readme.md index 7f86990e6..0a9601108 100644 --- a/Readme.md +++ b/Readme.md @@ -297,9 +297,11 @@ program.version('0.0.1', '-v, --vers', 'output the current version'); ## Commands -You can specify (sub)commands for your top-level command using `.command`. There are two ways these can be implemented: using an action handler attached to the command, or as a separate executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)). +You can specify (sub)commands for your top-level command using `.command()` or `.addCommand()`. There are two ways these can be implemented: using an action handler attached to the command, or as a separate executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)). -In the first parameter to `.command` you specify the command name and any command arguments. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. +In the first parameter to `.command()` you specify the command name and any command arguments. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. + +You can use `.addCommand()` to add an already configured subcommand to the program. For example: @@ -318,6 +320,11 @@ program program .command('start ', 'start named service') .command('stop [service]', 'stop named service, or all if no name supplied'); + +// Command prepared separately. +// Returns top-level command for adding more commands. +program + .addCommand(build.makeBuildCommand()); ``` Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified ([example](./examples/defaultCommand.js)). diff --git a/index.js b/index.js index 4fd4acda6..c87ed7ef7 100644 --- a/index.js +++ b/index.js @@ -212,8 +212,12 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts }; /** - * @api public - */ + * Add a prepared subcommand. + * + * @param {Command} cmd - new subcommand + * @return {Command} parent command for chaining + * @api public + */ Command.prototype.addCommand = function(cmd) { if (!cmd._name) throw new Error('Command passed to .AddCommand must have a name'); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 465bbce0e..a278c6c28 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -111,6 +111,9 @@ program .command("name1", "description") .command("name2", "description", { isDefault:true }) +const preparedCommand = new program.Command('prepared'); +program.addCommand(preparedCommand); + program .exitOverride(); diff --git a/typings/index.d.ts b/typings/index.d.ts index d700e8954..5ac15b8d6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -80,20 +80,18 @@ declare namespace commander { command(nameAndArgs: string, description: string, opts?: commander.CommandOptions): Command; /** - * Define argument syntax for the top-level command. - * - * @returns Command for chaining + * Add a prepared subcommand. + * + * @returns parent command for chaining */ - arguments(desc: string): Command; + addCommand(cmd: Command): Command; /** - * Parse expected `args`. - * - * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. + * Define argument syntax for the top-level command. * * @returns Command for chaining */ - parseExpectedArgs(args: string[]): Command; + arguments(desc: string): Command; /** * Register callback to use as replacement for calling process.exit. From 5fbec7e1db0e0dfaa204f1def0a3d9ce66bcef28 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jan 2020 12:32:21 +1300 Subject: [PATCH 34/37] Remove comment referring to removed code. --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index c87ed7ef7..2613a11f9 100644 --- a/index.js +++ b/index.js @@ -653,7 +653,7 @@ Command.prototype.parse = function(argv) { // store raw args this.rawArgs = argv; - // Guess name, used in usage in help. Not trying very hard. See _executeSubCommand for trying hard by checking files. + // Guess name, used in usage in help. this._name = this._name || path.basename(argv[1], path.extname(argv[1])); this._parseCommand([], argv.slice(2)); From c0d62d3b39176d0127bbb9ce1dde400865837131 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 27 Jan 2020 18:56:29 +1300 Subject: [PATCH 35/37] Revert the error tip "try --help", not happy with the wording --- index.js | 6 +++--- tests/command.exitOverride.test.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 2613a11f9..a1c5c043a 100644 --- a/index.js +++ b/index.js @@ -1060,7 +1060,7 @@ Command.prototype.optionMissingArgument = function(option, flag) { */ Command.prototype.missingMandatoryOptionValue = function(option) { - const message = `error: required option '${option.flags}' not specified (try '${this._helpLongFlag}' for more information)`; + const message = `error: required option '${option.flags}' not specified`; console.error(message); this._exit(1, 'commander.missingMandatoryOptionValue', message); }; @@ -1074,7 +1074,7 @@ Command.prototype.missingMandatoryOptionValue = function(option) { Command.prototype.unknownOption = function(flag) { if (this._allowUnknownOption) return; - const message = `error: unknown option '${flag}' (try '${this._helpLongFlag}' for more information)`; + const message = `error: unknown option '${flag}'`; console.error(message); this._exit(1, 'commander.unknownOption', message); }; @@ -1087,7 +1087,7 @@ Command.prototype.unknownOption = function(flag) { */ Command.prototype.unknownCommand = function() { - const message = `error: unrecognised command '${this.args[0]}' (try '${this._helpLongFlag}' for more information)`; + const message = `error: unrecognised command '${this.args[0]}'`; console.error(message); this._exit(1, 'commander.unknownCommand', message); }; diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 494c7fea3..31cc0c015 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -48,7 +48,7 @@ describe('.exitOverride and error details', () => { } expect(consoleErrorSpy).toHaveBeenCalled(); - expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m' (try '--help' for more information)"); + expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m'"); }); test('when specify unknown command then throw CommanderError', () => { @@ -65,7 +65,7 @@ describe('.exitOverride and error details', () => { } expect(consoleErrorSpy).toHaveBeenCalled(); - expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unrecognised command 'oops' (try '--help' for more information)"); + expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unrecognised command 'oops'"); }); // Same error as above, but with custom handler. @@ -241,6 +241,6 @@ describe('.exitOverride and error details', () => { caughtErr = err; } - expectCommanderError(caughtErr, 1, 'commander.missingMandatoryOptionValue', `error: required option '${optionFlags}' not specified (try '--help' for more information)`); + expectCommanderError(caughtErr, 1, 'commander.missingMandatoryOptionValue', `error: required option '${optionFlags}' not specified`); }); }); From fe896d12b79231e678aee6c1c67780eec05efa6a Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 27 Jan 2020 19:08:16 +1300 Subject: [PATCH 36/37] Say "unknown command", like "unknown option" --- index.js | 2 +- tests/command.exitOverride.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index a1c5c043a..a39140bf6 100644 --- a/index.js +++ b/index.js @@ -1087,7 +1087,7 @@ Command.prototype.unknownOption = function(flag) { */ Command.prototype.unknownCommand = function() { - const message = `error: unrecognised command '${this.args[0]}'`; + const message = `error: unknown command '${this.args[0]}'`; console.error(message); this._exit(1, 'commander.unknownCommand', message); }; diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 31cc0c015..fd75d5e3f 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -65,7 +65,7 @@ describe('.exitOverride and error details', () => { } expect(consoleErrorSpy).toHaveBeenCalled(); - expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unrecognised command 'oops'"); + expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unknown command 'oops'"); }); // Same error as above, but with custom handler. From 432bb5932a269be9986c417d589febe8bc58f8d2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 28 Jan 2020 20:54:14 +1300 Subject: [PATCH 37/37] Set properties to null rather than undefined in constructor --- index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index a39140bf6..dedcf936f 100644 --- a/index.js +++ b/index.js @@ -129,18 +129,18 @@ function Command(name) { this._storeOptionsAsProperties = true; // backwards compatible by default this._passCommandToAction = true; // backwards compatible by default this._actionResults = []; - this._actionHandler = undefined; + this._actionHandler = null; this._executableHandler = false; - this._executableFile = undefined; // custom name for executable - this._defaultCommandName = undefined; - this._exitCallback = undefined; + this._executableFile = null; // custom name for executable + this._defaultCommandName = null; + this._exitCallback = null; this._noHelp = false; this._helpFlags = '-h, --help'; this._helpDescription = 'display help for command'; this._helpShortFlag = '-h'; this._helpLongFlag = '--help'; - this._hasImplicitHelpCommand = undefined; + this._hasImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false this._helpCommandName = 'help'; this._helpCommandnameAndArgs = 'help [command]'; this._helpCommandDescription = 'display help for command'; @@ -202,7 +202,7 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; - cmd._executableFile = opts.executableFile; // Custom name for executable file + cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor this.commands.push(cmd); cmd._parseExpectedArgs(args); cmd.parent = this;