Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add support for nested commands (without executables) #1129

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
124 changes: 83 additions & 41 deletions index.js
Expand Up @@ -193,11 +193,24 @@ 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;
};

/**
* @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.
*
Expand Down Expand Up @@ -301,6 +314,67 @@ Command.prototype._exit = function(exitCode, code, message) {
process.exit(exitCode);
};

/**
* @api private
*/

Command.prototype._addCommandListener = function() {
var self = this;
var listener = function(args, unknown) {
// Parse any so-far unknown options
args = args || [];
unknown = unknown || [];

var parsed = self.parseOptions(unknown);

// Leftover arguments need to be pushed back. Fixes issue #56
if (parsed.args.length) args = parsed.args.concat(args);

if (args.length && self.listenerCount(`command:${args[0]}`)) {
self.parseArgs(args, parsed.unknown);
} else {
// Output help if necessary
outputHelpIfNecessary(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]);
}

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);
}
};

// 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);
}
};

/**
* Register callback `fn` for the command.
*
Expand All @@ -320,39 +394,9 @@ 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
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);
}

args[i] = args.splice(i);
}
});
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);
Expand All @@ -368,14 +412,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;
};

Expand Down Expand Up @@ -874,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);
Expand All @@ -885,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');
}
}
Expand Down