From c05a6c4bb15abbe4a93ee1ac2b4749ef1aa1d6ea Mon Sep 17 00:00:00 2001 From: Nowell Strite Date: Wed, 27 Aug 2014 09:56:06 -0400 Subject: [PATCH] Completed nested command support. Nested command support was almost completely available, however, a few things seemed to be missing to me. * `this` context on `action` event listeners was not bound properly. * We output `help` too early, the deepest referenced command should handle help. --- index.js | 55 +++++++++-------- test/test.options.command-context.js | 21 +++++++ test/test.options.nested-commands.js | 88 ++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 test/test.options.command-context.js create mode 100644 test/test.options.nested-commands.js diff --git a/index.js b/index.js index 9ab649a49..e4cbba735 100644 --- a/index.js +++ b/index.js @@ -215,37 +215,44 @@ Command.prototype.action = function(fn){ args = args || []; unknown = unknown || []; - var parsed = self.parseOptions(unknown); - // Output help if necessary - outputHelpIfNecessary(self, parsed.unknown); + var parsed = self.parseOptions(args.concat(unknown)); - // 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]); + if (parsed.args.length && self.listeners(parsed.args[0]).length) { + var result = self.parseArgs(parsed.args, parsed.unknown); } + else { + // 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) { + // Output help if necessary + outputHelpIfNecessary(self, parsed.unknown); + + 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 && null == parsed.args[i]) { + // Output help if necessary + outputHelpIfNecessary(self, parsed.unknown); - self._args.forEach(function(arg, i){ - if (arg.required && null == args[i]) { - self.missingArgument(arg.name); - } - }); + self.missingArgument(arg.name); + } + }); - // Always append ourselves to the end of the arguments, - // to make sure we match the number of arguments the user - // expects - if (self._args.length) { - args[self._args.length] = self; - } else { - args.push(self); - } - fn.apply(this, args); + // Always append ourselves to the end of the arguments, + // to make sure we match the number of arguments the user + // expects + if (self._args.length) { + parsed.args[self._args.length] = self; + } else { + parsed.args.push(self); + } + + fn.apply(self, parsed.args); + } }; this.parent.on(this._name, listener); if (this._alias) this.parent.on(this._alias, listener); diff --git a/test/test.options.command-context.js b/test/test.options.command-context.js new file mode 100644 index 000000000..acb1410b7 --- /dev/null +++ b/test/test.options.command-context.js @@ -0,0 +1,21 @@ +/** + * Module dependencies. + */ + +var program = require('../') + , should = require('should'); + +program + .version('0.0.1'); + +var commandContext; +var optionContext; + +var setup = program.command('setup') + .description('run setup commands for all envs') + .action(function(env, options){ + commandContext = this; + }); + +program.parse(['node', 'test', 'setup']); +commandContext._name.should.equal('setup'); diff --git a/test/test.options.nested-commands.js b/test/test.options.nested-commands.js new file mode 100644 index 000000000..4d6360f7e --- /dev/null +++ b/test/test.options.nested-commands.js @@ -0,0 +1,88 @@ +/** + * Module dependencies. + */ + +var program = require('../') + , should = require('should'); + +var commandContext; + +program + .version('0.0.1') + .on('--help', function() { + helpCommand = this._name; + }); + +var setupCommand = program + .command('setup') + .action(function(env, options){ + commandContext = this; + }) + .on('--help', function() { + helpCommand = this._name; + }); + +var addCommand = setupCommand + .command('add') + .option('--value [value]') + .action(function() { + commandContext = this; + }) + .on('--help', function() { + helpCommand = this._name; + }); + +var remoteCommand = addCommand + .command('remote') + .action(function() { + commandContext = this; + }) + .on('--help', function() { + helpCommand = this._name; + }); + +program.parse(['node', 'test', 'setup']); +commandContext._name.should.equal('setup'); + +program.parse(['node', 'test', 'setup', 'add']); +commandContext._name.should.equal('add'); + +program.parse(['node', 'test', 'setup', 'add', '--value', 'true']); +commandContext._name.should.equal('add'); + +program.parse(['node', 'test', 'setup', 'add', 'remote']); +commandContext._name.should.equal('remote'); + +// Make sure we still catch errors with required values for options +var exceptionOccurred = false; +var oldProcessExit = process.exit; +var oldConsoleError = console.error; +process.exit = function() { exceptionOccurred = true; throw new Error(); }; +console.error = function() {}; + +try { + program.parse(['node', 'test', '--help']); +} catch(ex) { +} +helpCommand.should.equal('test'); + +try { + program.parse(['node', 'test', 'setup', '--help']); +} catch(ex) { +} +helpCommand.should.equal('setup'); + +try { + program.parse(['node', 'test', 'setup', 'add', '--help']); +} catch(ex) { +} +helpCommand.should.equal('add'); + +try { + program.parse(['node', 'test', 'setup', 'add', 'remote', '--help']); +} catch(ex) { +} +helpCommand.should.equal('remote'); + +process.exit = oldProcessExit; +exceptionOccurred.should.be.true;