From 76cc5e8eb5d273765678925b1b78114ae237130d Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 30 Dec 2019 07:50:58 +1300 Subject: [PATCH 1/5] Refactor action handling --- index.js | 61 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 7022b73fe..072e03b79 100644 --- a/index.js +++ b/index.js @@ -302,23 +302,10 @@ Command.prototype._exit = function(exitCode, code, message) { }; /** - * Register callback `fn` for the command. - * - * Examples: - * - * program - * .command('help') - * .description('display verbose help') - * .action(function() { - * // output help here - * }); - * - * @param {Function} fn - * @return {Command} for chaining - * @api public + * @api private */ -Command.prototype.action = function(fn) { +Command.prototype._addCommandListener = function() { var self = this; var listener = function(args, unknown) { // Parse any so-far unknown options @@ -353,6 +340,40 @@ Command.prototype.action = function(fn) { } }); + self.emit('action', args); + }; + + 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); +}; + +/** + * Register callback `fn` for the command. + * + * Examples: + * + * program + * .command('help') + * .description('display verbose help') + * .action(function() { + * // output help here + * }); + * + * @param {Function} fn + * @return {Command} for chaining + * @api public + */ + +Command.prototype.action = function(fn) { + var self = this; + this._addCommandListener(); + + var listener = function(args) { // The .action callback takes an extra parameter which is the command itself. var expectedArgsCount = self._args.length; var actionArgs = args.slice(0, expectedArgsCount); @@ -368,14 +389,10 @@ Command.prototype.action = function(fn) { fn.apply(self, actionArgs); }; - 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.on('action', listener); + if (this._alias) this.on('action', listener); + return this; }; From 98fc3a9b27f2b0379f6477858b9b7ded9fca0497 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 30 Dec 2019 09:19:10 +1300 Subject: [PATCH 2/5] Subcommand chaining first cut --- index.js | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 072e03b79..8880bc244 100644 --- a/index.js +++ b/index.js @@ -314,33 +314,35 @@ Command.prototype._addCommandListener = function() { var parsed = self.parseOptions(unknown); - // Output help if necessary - outputHelpIfNecessary(self, parsed.unknown); - self._checkForMissingMandatoryOptions(); - - // If there are still any unknown options, then we simply - // die, unless someone asked for help, in which case we give it - // to them, and then we die. - if (parsed.unknown.length > 0) { - self.unknownOption(parsed.unknown[0]); - } - // Leftover arguments need to be pushed back. Fixes issue #56 if (parsed.args.length) args = parsed.args.concat(args); - self._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); - } + if (args.length && self.listenerCount(`command:${args[0]}`)) { + self.parseArgs(args, parsed.unknown); + } else { + // Output help if necessary + outputHelpIfNecessary(self, parsed.unknown); + self._checkForMissingMandatoryOptions(); - args[i] = args.splice(i); + // If there are still any unknown options, then we simply die. + if (parsed.unknown.length > 0) { + self.unknownOption(parsed.unknown[0]); } - }); - self.emit('action', args); + self._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); + } + }); + + self.emit('action', args); + } }; var parent = this.parent || this; From 3f8f12a20e8ae45f40be7eaa0cd2ca099b3aaac5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 30 Dec 2019 09:43:34 +1300 Subject: [PATCH 3/5] Subcommand adding first cut --- index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/index.js b/index.js index 8880bc244..d3989e12b 100644 --- a/index.js +++ b/index.js @@ -198,6 +198,18 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts return cmd; }; +/** + * @api public + */ +Command.prototype.addCommand = function(cmd) { + if (!cmd._name) throw Error('addCommand name is not specified for command'); + + this.commands.push(cmd); + cmd.parent = this; + cmd._addCommandListener(); + return this; +}; + /** * Define argument syntax for the top-level command. * From 399f1f9e1be6beee5cf26535c0ea6e966ac7117f Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 30 Dec 2019 15:05:23 +1300 Subject: [PATCH 4/5] Support intermediate nested command level without action handler --- index.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index d3989e12b..3b4acd745 100644 --- a/index.js +++ b/index.js @@ -193,6 +193,7 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts this.commands.push(cmd); cmd.parseExpectedArgs(args); cmd.parent = this; + if (!desc) cmd._addCommandListener(); // Add listener to handle nested subcommand if (desc) return this; return cmd; @@ -357,13 +358,21 @@ Command.prototype._addCommandListener = function() { } }; - var parent = this.parent || this; - if (parent === this) { - parent.on('program-command', listener); - } else { - parent.on('command:' + this._name, listener); + // Add listener to parent, or program. + let eventEmitter = this.parent; + let eventName = `command:${this._name}`; + if (!eventEmitter) { + eventEmitter = this; // program + eventName = 'program-command'; + } + // Avoid adding listener twice (currently adding on demand from multiple places). + if (eventEmitter.listenerCount(eventName) === 0) { + eventEmitter.on(eventName, listener); + } + const aliasEventName = `command:${this._alias}`; + if (this._alias && eventEmitter.listenerCount(aliasEventName) === 0) { + eventEmitter.on(aliasEventName, listener); } - if (this._alias) parent.on('command:' + this._alias, listener); }; /** From 5dbbba01f1ebc7d3214293dbb9598b0ae7bbdc82 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 2 Jan 2020 18:23:43 +1300 Subject: [PATCH 5/5] Paranoia, only emit program-command from root comment --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3b4acd745..934275ef3 100644 --- a/index.js +++ b/index.js @@ -914,8 +914,10 @@ Command.prototype.parseArgs = function(args, unknown) { if (this.listeners('command:' + name).length) { this.emit('command:' + args.shift(), args, unknown); } else { - this.emit('program-command', args, unknown); this.emit('command:*', args, unknown); + if (!this.parent) { + this.emit('program-command', args, unknown); + } } } else { outputHelpIfNecessary(this, unknown); @@ -925,7 +927,7 @@ Command.prototype.parseArgs = function(args, unknown) { this.unknownOption(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) { + if (!this.parent && this._args.filter(function(a) { return a.required; }).length === 0) { this.emit('program-command'); } }