From 3a545f2ef7b49bdb9e1611e30d0c956b97407475 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Tue, 20 Aug 2019 16:06:55 +0000 Subject: [PATCH 01/12] command.forwardSubcommands --- Readme.md | 37 ++++++++++ examples/forward-subcommands.js | 41 ++++++++++++ index.js | 37 ++++++++++ test/test.command.forwardSubcommands.js | 89 +++++++++++++++++++++++++ typings/index.d.ts | 8 +++ 5 files changed, 212 insertions(+) create mode 100644 examples/forward-subcommands.js create mode 100644 test/test.command.forwardSubcommands.js diff --git a/Readme.md b/Readme.md index b23d01f2e..32245a008 100644 --- a/Readme.md +++ b/Readme.md @@ -404,6 +404,43 @@ Specifying a name with `executableFile` will override the default constructed na If the program is designed to be installed globally, make sure the executables have proper modes, like `755`. +### Sub commands (handlers) + +This approach does not spawn a new process in comparison to *'Git-style executable (sub)commands'* above. Instead it creates a new subcommand instance which can be initiated with own actions, options (and further subcommands). Then when upper level command is found it triggers delegates handling to lower levels. + +```js +const subCommand = program + .command('journal') + .description('Journal utils') // this should be separate line + .option('-q, --quiet') + .forwardSubcommands(); // instead of "action" + +subCommand + .command('list ') + .action(List); + +subCommand + .command('delete ') + .option('-f, --force') + .action(Delete); +``` + +``` +$ node myapp journal list myjournal1 +$ node myapp journal delete myjournal1 +``` + +Be aware of option handling. In the example above `--force` option is available in the command object passed to action. However, `--quiet` belongs to it's parent. + +```js +// invoked with "journal --quiet delete xxx --force" +function Delete(path, cmdInstance) { + console.log(cmdInstance.force); // true + console.log(cmdInstance.quiet); // false !!! + console.log(cmdInstance.parent.quiet); // true +} +``` + ## 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: diff --git a/examples/forward-subcommands.js b/examples/forward-subcommands.js new file mode 100644 index 000000000..9e67817aa --- /dev/null +++ b/examples/forward-subcommands.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +// This is an example of forwardSubcommands +// +// $ forward-subcommands journal list myjrnl1 +// $ forward-subcommands journal delete myjrnl1 +// +// Also with options +// $ forward-subcommands journal -q delete -f myjrnl1 + + +const commander = require('commander'); +const program = new commander.Command(); + +const subCommand = program + .command('journal') + .description('Journal utils') // this should be separate line + .option('-q, --quiet') + .forwardSubcommands(); // instead of "action" + +subCommand + .command('list ') + .description('List journal') + .action((path, ci) => { + console.log('List journal'); + console.log('Path is', path); + console.log('Quiet =', Boolean(ci.parent.quiet)); + }); + +subCommand + .command('delete ') + .description('Delete journal') + .option('-f, --force') + .action((path, ci) => { + console.log('List journal'); + console.log('Path is', path); + console.log('Quiet =', Boolean(ci.parent.quiet)); + console.log('Force =', Boolean(ci.force)); + }); + +program.parse(process.argv); diff --git a/index.js b/index.js index ce38da26b..53b0da6d3 100644 --- a/index.js +++ b/index.js @@ -1384,6 +1384,43 @@ Command.prototype.help = function(cb) { this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)'); }; +/** + * Creates an instance of sub command + * + * @returns {Command} which is the subcommand instance + */ + +Command.prototype.forwardSubcommands = function() { + var self = this; + var listener = function(args, unknown) { + // Parse any so-far unknown options + args = args || []; + unknown = unknown || []; + + var parsed = self.parseOptions(unknown); + if (parsed.args.length) args = parsed.args.concat(args); + unknown = parsed.unknown; + + // Output help if necessary + if (unknown.includes('--help') || unknown.includes('-h')) { + self.outputHelp(); + process.exit(0); + } + + self.parseArgs(args, unknown); + }; + + if (this._args.length > 0) { + console.error('forwardSubcommands cannot be applied to command with explicit args'); + } + + var parent = this.parent || this; + var name = parent === this ? '*' : this._name; + parent.on('command:' + name, listener); + if (this._alias) parent.on('command:' + this._alias, listener); + return this; +}; + /** * Camel-case the given `flag` * diff --git a/test/test.command.forwardSubcommands.js b/test/test.command.forwardSubcommands.js new file mode 100644 index 000000000..0c2bb19c5 --- /dev/null +++ b/test/test.command.forwardSubcommands.js @@ -0,0 +1,89 @@ +var program = require('../'); +var should = require('should'); +var result; +var commander; + +function resetResult() { + result = { + count: 0, + last: '', + param: undefined, + }; +} + +function touchCommand(cmd, param) { + result.count++; + result.last = cmd; + result.param = param; +} + +function assertResult(cmd, param) { + should.equal(result.last, cmd); + should.equal(result.count, 1); + if (param) should.equal(result.param, param); +} + +function createCommanderInstanceAndReset() { + resetResult(); + var root = new program.Command(); + + root + .command('cmd1') + .action(function () { touchCommand('command1') }); + + var cmd2 = root + .command('cmd2') + .option('-q, --quiet') + .forwardSubcommands(); + cmd2 + .command('sub21') + .option('-f, --force') + .action(function (cmdInstance) { touchCommand('subcommand21', cmdInstance) }); + cmd2 + .command('sub22 ') + .action(function (param) { touchCommand('subcommand22', param) }); + + var cmd3 = root + .command('cmd3') + .forwardSubcommands(); + cmd3 + .command('sub31') + .action(function () { touchCommand('subcommand31') }); + + return root; +} + +// ACTION TESTS + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd1']); +assertResult('command1'); + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd2', 'sub21']); +assertResult('subcommand21'); + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd2', 'sub22', 'theparam']); +assertResult('subcommand22', 'theparam'); + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd3', 'sub31']); +assertResult('subcommand31'); + +// OPTION TESTS + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd2', 'sub21', '-f']); +Boolean(result.param.force).should.be.true(); + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd2', '-q', 'sub21']); +Boolean(result.param.parent.quiet).should.be.true(); + +// Keeping as an example but this may be incorrect +// -q is a parameter to the first command layer +// so potentially it must be ignored if specified after second level +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd2', 'sub21', '-q']); +Boolean(result.param.parent.quiet).should.be.true(); diff --git a/typings/index.d.ts b/typings/index.d.ts index a2fc6a989..378796878 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -205,6 +205,14 @@ declare namespace commander { */ parseOptions(argv: string[]): commander.ParseOptionsResult; + /** + * Creates an instance of sub command + * + * @returns {Command} which is the subcommand instance + */ + + forwardSubcommands(): Command; + /** * Return an object containing options as key-value pairs * From e33ca8343b49d0322710f3ce45bdd0b27cd6ab66 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Tue, 20 Aug 2019 16:30:58 +0000 Subject: [PATCH 02/12] command.collectAllOptions --- Readme.md | 3 +- index.js | 21 +++++++++++ test/test.command.collectAllOptions.js | 50 ++++++++++++++++++++++++++ typings/index.d.ts | 10 ++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/test.command.collectAllOptions.js diff --git a/Readme.md b/Readme.md index 32245a008..4a1971d72 100644 --- a/Readme.md +++ b/Readme.md @@ -430,7 +430,7 @@ $ node myapp journal list myjournal1 $ node myapp journal delete myjournal1 ``` -Be aware of option handling. In the example above `--force` option is available in the command object passed to action. However, `--quiet` belongs to it's parent. +Be aware of option handling. In the example above `--force` option is available in the command object passed to action. However, `--quiet` belongs to it's parent. Along with the explicit reach you can use `collectAllOptions` - it collects option values from all levels and returns as an object. ```js // invoked with "journal --quiet delete xxx --force" @@ -438,6 +438,7 @@ function Delete(path, cmdInstance) { console.log(cmdInstance.force); // true console.log(cmdInstance.quiet); // false !!! console.log(cmdInstance.parent.quiet); // true + console.log(cmdInstance.collectAllOptions()); // { quiet: true, force: true } } ``` diff --git a/index.js b/index.js index 53b0da6d3..7ed6cabf2 100644 --- a/index.js +++ b/index.js @@ -1421,6 +1421,27 @@ Command.prototype.forwardSubcommands = function() { return this; }; +/** + * Returns an object with all options values, including parent options values + * This makes it especially useful with forwardSubcommands as it collects + * options from upper levels too + * + * @returns {Object} dictionary of option values + */ + +Command.prototype.collectAllOptions = function() { + var allOpts = {}; + var node = this; + while (node) { + allOpts = node.options + .map(o => o.attributeName()) + .filter(o => typeof node[o] !== 'function') + .reduce((r, o) => ({ [o]: node[o], ...r }), allOpts); // deeper opts enjoy the priority + node = node.parent; + } + return allOpts; +}; + /** * Camel-case the given `flag` * diff --git a/test/test.command.collectAllOptions.js b/test/test.command.collectAllOptions.js new file mode 100644 index 000000000..aa64875ab --- /dev/null +++ b/test/test.command.collectAllOptions.js @@ -0,0 +1,50 @@ +var program = require('../'); +var should = require('should'); +var result; +var commander; + +function createCommanderInstanceAndReset() { + result = null; + var root = new program.Command(); + + var cmd = root + .command('cmd') + .option('-q, --quiet') + .forwardSubcommands(); + + cmd + .command('sub1') + .option('-p, --purge') + .action(function () {}); + var sub = cmd + .command('sub2') + .option('-f, --force') + .forwardSubcommands(); + + sub + .command('subsub') + .option('-a, --alternative') + .action(function (commanderInstance) { + result = commanderInstance.collectAllOptions(); + }); + + return root; +} + +// ACTION TESTS + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd', '-q', 'sub2', '-f', 'subsub', '-a']); +should.deepEqual(result, { + quiet: true, + force: true, + alternative: true, +}); + +commander = createCommanderInstanceAndReset() +commander.parse(['node', 'test', 'cmd', '-q', 'sub2', 'subsub']); +should.deepEqual(result, { + quiet: true, + force: undefined, + alternative: undefined, +}); diff --git a/typings/index.d.ts b/typings/index.d.ts index 378796878..a41a65acb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -213,6 +213,16 @@ declare namespace commander { forwardSubcommands(): Command; + /** + * Returns an object with all options values, including parent options values + * This makes it especially useful with forwardSubcommands as it collects + * options from upper levels too + * + * @returns {Object} dictionary of option values + */ + + collectAllOptions(): Object; + /** * Return an object containing options as key-value pairs * From 602113c5ad61645b757b2e3fd4adbbcbb91a4656 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Tue, 20 Aug 2019 16:54:01 +0000 Subject: [PATCH 03/12] subcommand help improvement from tkausl --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 7ed6cabf2..535814ce8 100644 --- a/index.js +++ b/index.js @@ -1402,7 +1402,7 @@ Command.prototype.forwardSubcommands = function() { unknown = parsed.unknown; // Output help if necessary - if (unknown.includes('--help') || unknown.includes('-h')) { + if ((unknown.includes('--help') || unknown.includes('-h')) && (!args || !self.listeners('command:' + args[0]))) { self.outputHelp(); process.exit(0); } From 268886e58567e6c554b732b902f5b534dbeb0985 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Mon, 16 Sep 2019 21:13:02 +0000 Subject: [PATCH 04/12] useSubcommand draft --- examples/use-subcommand.js | 58 ++++++++++++++++++++++++++++++++++++++ index.js | 40 ++++++++++++++++++++++++++ typings/index.d.ts | 9 ++++++ 3 files changed, 107 insertions(+) create mode 100755 examples/use-subcommand.js diff --git a/examples/use-subcommand.js b/examples/use-subcommand.js new file mode 100755 index 000000000..90af11bfb --- /dev/null +++ b/examples/use-subcommand.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +// This is an example of useSubcommand +// +// $ use-subcommand journal list myjounal +// $ use-subcommand journal delete myjounal +// +// With options +// $ use-subcommand journal -q delete -f myjounal + +// const { Command } = require('commander'); +const { Command } = require('..'); + +function importSubCommand() { + const journalCmd = new Command() + .name('journal') + .description('Journal utils'); + + journalCmd + .command('list ') + .description('List journal') + .action((path, cmdInstance) => { + console.log('List journal'); + console.log('Path is', path); + console.log('Quiet =', Boolean(cmdInstance.parent.quiet)); + }); + + journalCmd + .command('delete ') + .description('Delete journal') + .option('-f, --force') + .action((path, cmdInstance) => { + console.log('List journal'); + console.log('Path is', path); + console.log('Quiet =', Boolean(cmdInstance.parent.quiet)); + console.log('Force =', Boolean(cmdInstance.force)); + }); + + return journalCmd; +} + +// this is supposedly a module, so in real case this would be `require` +const journalSubCommand = importSubCommand(); + +const program = new Command(); +program + .option('-q, --quiet') + .useSubcommand(journalSubCommand); + +program + .command('hello ') + .description('Greeting') + .action((name, cmdInstance) => { + console.log(`Hello ${name}!`); + }); + +if (process.argv.length <= 2) program.help(); +program.parse(process.argv); diff --git a/index.js b/index.js index 535814ce8..90b43d4b6 100644 --- a/index.js +++ b/index.js @@ -1384,6 +1384,46 @@ Command.prototype.help = function(cb) { this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)'); }; +/** + * Add action-like sub command + * command name is taken from name() property - must be defined + * + * @returns {Command} `this` instance + */ + +Command.prototype.useSubcommand = function(subCommand) { + if (this._args.length > 0) throw Error('useSubcommand cannot be applied to a command with explicit args'); + if (!subCommand._name) throw Error('subCommand name is not specified'); + + var self = subCommand; + var listener = function(args, unknown) { + // Parse any so-far unknown options + args = args || []; + unknown = unknown || []; + + var parsed = self.parseOptions(unknown); + if (parsed.args.length) args = parsed.args.concat(args); + unknown = parsed.unknown; + + // Output help if necessary + const helpRequested = unknown.includes('--help') || unknown.includes('-h'); + const noFutherCommands = !args || !self.listeners('command:' + args[0]); + if (helpRequested && noFutherCommands) { + self.outputHelp(); + process.exit(0); + } + + self.parseArgs(args, unknown); + }; + + for (const label of [subCommand._name, subCommand._alias]) { + if (label) this.on('command:' + label, listener); + } + this.commands.push(subCommand); + subCommand.parent = this; + return this; +}; + /** * Creates an instance of sub command * diff --git a/typings/index.d.ts b/typings/index.d.ts index a41a65acb..bdeb80d41 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -213,6 +213,15 @@ declare namespace commander { forwardSubcommands(): Command; + /** + * Add action-like sub command + * command name is taken from name() property - must be defined + * + * @returns {Command} `this` instance + */ + + useSubcommand(subCommand : Command): Command; + /** * Returns an object with all options values, including parent options values * This makes it especially useful with forwardSubcommands as it collects From 58ca5b1d61e32d8eb9ab523003cb73efb4b71426 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 18:22:02 +0000 Subject: [PATCH 05/12] improve example --- examples/use-subcommand.js | 33 +++++++++++++++++++++------------ index.js | 5 +++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/examples/use-subcommand.js b/examples/use-subcommand.js index 90af11bfb..a26e5b31e 100755 --- a/examples/use-subcommand.js +++ b/examples/use-subcommand.js @@ -1,14 +1,15 @@ #!/usr/bin/env node // This is an example of useSubcommand +// and collectAllOptions // -// $ use-subcommand journal list myjounal -// $ use-subcommand journal delete myjounal -// -// With options -// $ use-subcommand journal -q delete -f myjounal +// try +// $ use-subcommand journal list myjounal +// $ use-subcommand journal delete myjounal +// or with options +// $ use-subcommand journal -q delete -f myjounal -// const { Command } = require('commander'); +// const { Command } = require('commander'); << would be in a real program const { Command } = require('..'); function importSubCommand() { @@ -22,7 +23,9 @@ function importSubCommand() { .action((path, cmdInstance) => { console.log('List journal'); console.log('Path is', path); - console.log('Quiet =', Boolean(cmdInstance.parent.quiet)); + console.log('Quiet =', Boolean(cmdInstance.parent.parent.quiet)); + // list is a child of journal, which is a child of main cmd + console.log('collectAllOptions:', cmdInstance.collectAllOptions()); }); journalCmd @@ -32,8 +35,9 @@ function importSubCommand() { .action((path, cmdInstance) => { console.log('List journal'); console.log('Path is', path); - console.log('Quiet =', Boolean(cmdInstance.parent.quiet)); + console.log('Quiet =', Boolean(cmdInstance.parent.parent.quiet)); console.log('Force =', Boolean(cmdInstance.force)); + console.log('collectAllOptions:', cmdInstance.collectAllOptions()); }); return journalCmd; @@ -44,8 +48,7 @@ const journalSubCommand = importSubCommand(); const program = new Command(); program - .option('-q, --quiet') - .useSubcommand(journalSubCommand); + .option('-q, --quiet'); program .command('hello ') @@ -54,5 +57,11 @@ program console.log(`Hello ${name}!`); }); -if (process.argv.length <= 2) program.help(); -program.parse(process.argv); +program + .useSubcommand(journalSubCommand); + +if (process.argv.length <= 2) { + program.help(); +} else { + program.parse(process.argv); +} diff --git a/index.js b/index.js index 90b43d4b6..f54588eb7 100644 --- a/index.js +++ b/index.js @@ -1463,8 +1463,9 @@ Command.prototype.forwardSubcommands = function() { /** * Returns an object with all options values, including parent options values - * This makes it especially useful with forwardSubcommands as it collects - * options from upper levels too + * This makes it especially useful with useSubcommand as it collects + * options from the whole command chain, including parent levels. + * beware that subcommand opts enjoy the priority over the parent ones * * @returns {Object} dictionary of option values */ From 3feec04284dbbbf093baeceb603e7cf21e41d976 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 18:32:43 +0000 Subject: [PATCH 06/12] adopt _exit approach --- index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index f54588eb7..3ec86002b 100644 --- a/index.js +++ b/index.js @@ -1395,25 +1395,24 @@ Command.prototype.useSubcommand = function(subCommand) { if (this._args.length > 0) throw Error('useSubcommand cannot be applied to a command with explicit args'); if (!subCommand._name) throw Error('subCommand name is not specified'); - var self = subCommand; var listener = function(args, unknown) { // Parse any so-far unknown options args = args || []; unknown = unknown || []; - var parsed = self.parseOptions(unknown); + var parsed = subCommand.parseOptions(unknown); if (parsed.args.length) args = parsed.args.concat(args); unknown = parsed.unknown; // Output help if necessary const helpRequested = unknown.includes('--help') || unknown.includes('-h'); - const noFutherCommands = !args || !self.listeners('command:' + args[0]); + const noFutherCommands = !args || !subCommand.listeners('command:' + args[0]); if (helpRequested && noFutherCommands) { - self.outputHelp(); - process.exit(0); + subCommand.outputHelp(); + subCommand._exit(0, 'commander.useSubcommand.listener', `outputHelp(${subCommand._name})`); } - self.parseArgs(args, unknown); + subCommand.parseArgs(args, unknown); }; for (const label of [subCommand._name, subCommand._alias]) { From eb9f3408a11e5a39fbedaae09b8f4464ab2d0f0e Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 18:49:54 +0000 Subject: [PATCH 07/12] improvements in help logic --- index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 3ec86002b..803c05c86 100644 --- a/index.js +++ b/index.js @@ -1405,9 +1405,11 @@ Command.prototype.useSubcommand = function(subCommand) { unknown = parsed.unknown; // Output help if necessary - const helpRequested = unknown.includes('--help') || unknown.includes('-h'); - const noFutherCommands = !args || !subCommand.listeners('command:' + args[0]); - if (helpRequested && noFutherCommands) { + + const helpRequested = unknown.includes(subCommand._helpLongFlag) || unknown.includes(subCommand._helpShortFlag); + const noFutherValidCommands = args.length === 0 || !subCommand.listeners('command:' + args[0]); + const noFurtherCommandsButExpected = args.length === 0 && unknown.length === 0 && subCommand.commands.length > 0; + if (helpRequested && noFutherValidCommands || noFurtherCommandsButExpected) { subCommand.outputHelp(); subCommand._exit(0, 'commander.useSubcommand.listener', `outputHelp(${subCommand._name})`); } From b355a98995b8b8b488beb05c772fa5382bd18a56 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 19:06:47 +0000 Subject: [PATCH 08/12] doc update --- Readme.md | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/Readme.md b/Readme.md index 4a1971d72..386802f48 100644 --- a/Readme.md +++ b/Readme.md @@ -406,37 +406,49 @@ If the program is designed to be installed globally, make sure the executables h ### Sub commands (handlers) -This approach does not spawn a new process in comparison to *'Git-style executable (sub)commands'* above. Instead it creates a new subcommand instance which can be initiated with own actions, options (and further subcommands). Then when upper level command is found it triggers delegates handling to lower levels. +This approach does not spawn a new process in comparison to *'Git-style executable (sub)commands'* above. Instead it forwards arguments to a subcommand instance which can be initiated with own actions, options (and further subcommands). See also `use-subcommand.js` in `examples` dir. ```js -const subCommand = program - .command('journal') - .description('Journal utils') // this should be separate line - .option('-q, --quiet') - .forwardSubcommands(); // instead of "action" +const { Command } = require('commander'); + +// supposedly sub commands would be defined in a separate module +const subCommand = new Command(); + +subCommand + // Name is mandatory ! it will be the expected arg to trigger the sub command + .name('journal') + .description('Journal utils'); subCommand .command('list ') - .action(List); + .action(listActionHandler); subCommand .command('delete ') .option('-f, --force') - .action(Delete); + .action(deleteActionHandler); + +// ... and this is supposedly in the main program ... +const program = new Command(); +program + .option('-q, --quiet'); + .useSubcommand(subCommand); // forward args, starting with "journal" (subCommand.name()) to this instance + ``` +Invocation: ``` $ node myapp journal list myjournal1 -$ node myapp journal delete myjournal1 +$ node myapp -q journal delete myjournal1 -f ``` -Be aware of option handling. In the example above `--force` option is available in the command object passed to action. However, `--quiet` belongs to it's parent. Along with the explicit reach you can use `collectAllOptions` - it collects option values from all levels and returns as an object. +Be aware of option handling. In the example above `--force` option directly belongs to the command object passed to action handler (in last param). However, `--quiet` belongs to it's parent! Along with the explicit access you can use `collectAllOptions` - it collects option values from all levels and returns as an object. ```js // invoked with "journal --quiet delete xxx --force" -function Delete(path, cmdInstance) { - console.log(cmdInstance.force); // true - console.log(cmdInstance.quiet); // false !!! +function deleteActionHandler(path, cmdInstance) { + console.log(cmdInstance.force); // true + console.log(cmdInstance.quiet); // undefined ! console.log(cmdInstance.parent.quiet); // true console.log(cmdInstance.collectAllOptions()); // { quiet: true, force: true } } From 471aaf5800fdb9444d5e91d6ad48b01384c8c581 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 19:10:28 +0000 Subject: [PATCH 09/12] remove forwardSubcommands code and typing --- index.js | 37 ------------------------------------- typings/index.d.ts | 9 --------- 2 files changed, 46 deletions(-) diff --git a/index.js b/index.js index 803c05c86..80a45e6fa 100644 --- a/index.js +++ b/index.js @@ -1425,43 +1425,6 @@ Command.prototype.useSubcommand = function(subCommand) { return this; }; -/** - * Creates an instance of sub command - * - * @returns {Command} which is the subcommand instance - */ - -Command.prototype.forwardSubcommands = function() { - var self = this; - var listener = function(args, unknown) { - // Parse any so-far unknown options - args = args || []; - unknown = unknown || []; - - var parsed = self.parseOptions(unknown); - if (parsed.args.length) args = parsed.args.concat(args); - unknown = parsed.unknown; - - // Output help if necessary - if ((unknown.includes('--help') || unknown.includes('-h')) && (!args || !self.listeners('command:' + args[0]))) { - self.outputHelp(); - process.exit(0); - } - - self.parseArgs(args, unknown); - }; - - if (this._args.length > 0) { - console.error('forwardSubcommands cannot be applied to command with explicit args'); - } - - var parent = this.parent || this; - var name = parent === this ? '*' : this._name; - parent.on('command:' + name, listener); - if (this._alias) parent.on('command:' + this._alias, listener); - return this; -}; - /** * Returns an object with all options values, including parent options values * This makes it especially useful with useSubcommand as it collects diff --git a/typings/index.d.ts b/typings/index.d.ts index bdeb80d41..286b9bfd2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -211,15 +211,6 @@ declare namespace commander { * @returns {Command} which is the subcommand instance */ - forwardSubcommands(): Command; - - /** - * Add action-like sub command - * command name is taken from name() property - must be defined - * - * @returns {Command} `this` instance - */ - useSubcommand(subCommand : Command): Command; /** From 4efc5663afc77316753bbbe8d99f17c1267f0709 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 19:13:06 +0000 Subject: [PATCH 10/12] delete forward subcommands example --- examples/forward-subcommands.js | 41 --------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 examples/forward-subcommands.js diff --git a/examples/forward-subcommands.js b/examples/forward-subcommands.js deleted file mode 100644 index 9e67817aa..000000000 --- a/examples/forward-subcommands.js +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env node - -// This is an example of forwardSubcommands -// -// $ forward-subcommands journal list myjrnl1 -// $ forward-subcommands journal delete myjrnl1 -// -// Also with options -// $ forward-subcommands journal -q delete -f myjrnl1 - - -const commander = require('commander'); -const program = new commander.Command(); - -const subCommand = program - .command('journal') - .description('Journal utils') // this should be separate line - .option('-q, --quiet') - .forwardSubcommands(); // instead of "action" - -subCommand - .command('list ') - .description('List journal') - .action((path, ci) => { - console.log('List journal'); - console.log('Path is', path); - console.log('Quiet =', Boolean(ci.parent.quiet)); - }); - -subCommand - .command('delete ') - .description('Delete journal') - .option('-f, --force') - .action((path, ci) => { - console.log('List journal'); - console.log('Path is', path); - console.log('Quiet =', Boolean(ci.parent.quiet)); - console.log('Force =', Boolean(ci.force)); - }); - -program.parse(process.argv); From 7e3f0786dd4e50c51f26db768f9cfc2204db0750 Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 19:19:16 +0000 Subject: [PATCH 11/12] linter fixes --- index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 80a45e6fa..4abf5bc90 100644 --- a/index.js +++ b/index.js @@ -1387,7 +1387,7 @@ Command.prototype.help = function(cb) { /** * Add action-like sub command * command name is taken from name() property - must be defined - * + * * @returns {Command} `this` instance */ @@ -1397,7 +1397,7 @@ Command.prototype.useSubcommand = function(subCommand) { var listener = function(args, unknown) { // Parse any so-far unknown options - args = args || []; + args = args || []; unknown = unknown || []; var parsed = subCommand.parseOptions(unknown); @@ -1405,11 +1405,11 @@ Command.prototype.useSubcommand = function(subCommand) { unknown = parsed.unknown; // Output help if necessary - + const helpRequested = unknown.includes(subCommand._helpLongFlag) || unknown.includes(subCommand._helpShortFlag); const noFutherValidCommands = args.length === 0 || !subCommand.listeners('command:' + args[0]); const noFurtherCommandsButExpected = args.length === 0 && unknown.length === 0 && subCommand.commands.length > 0; - if (helpRequested && noFutherValidCommands || noFurtherCommandsButExpected) { + if ((helpRequested && noFutherValidCommands) || noFurtherCommandsButExpected) { subCommand.outputHelp(); subCommand._exit(0, 'commander.useSubcommand.listener', `outputHelp(${subCommand._name})`); } From 777450b913a3f4cd40d3bdc49661da34832d63ea Mon Sep 17 00:00:00 2001 From: Alexander Tsybulsky Date: Sat, 21 Dec 2019 20:18:17 +0000 Subject: [PATCH 12/12] tests for useSubcommand and collectAllOptions --- test/test.command.collectAllOptions.js | 50 ----------- test/test.command.forwardSubcommands.js | 89 -------------------- tests/command.collectAllOptions.test.js | 59 +++++++++++++ tests/command.useSubcommand.test.js | 107 ++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 139 deletions(-) delete mode 100644 test/test.command.collectAllOptions.js delete mode 100644 test/test.command.forwardSubcommands.js create mode 100644 tests/command.collectAllOptions.test.js create mode 100644 tests/command.useSubcommand.test.js diff --git a/test/test.command.collectAllOptions.js b/test/test.command.collectAllOptions.js deleted file mode 100644 index aa64875ab..000000000 --- a/test/test.command.collectAllOptions.js +++ /dev/null @@ -1,50 +0,0 @@ -var program = require('../'); -var should = require('should'); -var result; -var commander; - -function createCommanderInstanceAndReset() { - result = null; - var root = new program.Command(); - - var cmd = root - .command('cmd') - .option('-q, --quiet') - .forwardSubcommands(); - - cmd - .command('sub1') - .option('-p, --purge') - .action(function () {}); - var sub = cmd - .command('sub2') - .option('-f, --force') - .forwardSubcommands(); - - sub - .command('subsub') - .option('-a, --alternative') - .action(function (commanderInstance) { - result = commanderInstance.collectAllOptions(); - }); - - return root; -} - -// ACTION TESTS - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd', '-q', 'sub2', '-f', 'subsub', '-a']); -should.deepEqual(result, { - quiet: true, - force: true, - alternative: true, -}); - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd', '-q', 'sub2', 'subsub']); -should.deepEqual(result, { - quiet: true, - force: undefined, - alternative: undefined, -}); diff --git a/test/test.command.forwardSubcommands.js b/test/test.command.forwardSubcommands.js deleted file mode 100644 index 0c2bb19c5..000000000 --- a/test/test.command.forwardSubcommands.js +++ /dev/null @@ -1,89 +0,0 @@ -var program = require('../'); -var should = require('should'); -var result; -var commander; - -function resetResult() { - result = { - count: 0, - last: '', - param: undefined, - }; -} - -function touchCommand(cmd, param) { - result.count++; - result.last = cmd; - result.param = param; -} - -function assertResult(cmd, param) { - should.equal(result.last, cmd); - should.equal(result.count, 1); - if (param) should.equal(result.param, param); -} - -function createCommanderInstanceAndReset() { - resetResult(); - var root = new program.Command(); - - root - .command('cmd1') - .action(function () { touchCommand('command1') }); - - var cmd2 = root - .command('cmd2') - .option('-q, --quiet') - .forwardSubcommands(); - cmd2 - .command('sub21') - .option('-f, --force') - .action(function (cmdInstance) { touchCommand('subcommand21', cmdInstance) }); - cmd2 - .command('sub22 ') - .action(function (param) { touchCommand('subcommand22', param) }); - - var cmd3 = root - .command('cmd3') - .forwardSubcommands(); - cmd3 - .command('sub31') - .action(function () { touchCommand('subcommand31') }); - - return root; -} - -// ACTION TESTS - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd1']); -assertResult('command1'); - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd2', 'sub21']); -assertResult('subcommand21'); - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd2', 'sub22', 'theparam']); -assertResult('subcommand22', 'theparam'); - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd3', 'sub31']); -assertResult('subcommand31'); - -// OPTION TESTS - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd2', 'sub21', '-f']); -Boolean(result.param.force).should.be.true(); - -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd2', '-q', 'sub21']); -Boolean(result.param.parent.quiet).should.be.true(); - -// Keeping as an example but this may be incorrect -// -q is a parameter to the first command layer -// so potentially it must be ignored if specified after second level -commander = createCommanderInstanceAndReset() -commander.parse(['node', 'test', 'cmd2', 'sub21', '-q']); -Boolean(result.param.parent.quiet).should.be.true(); diff --git a/tests/command.collectAllOptions.test.js b/tests/command.collectAllOptions.test.js new file mode 100644 index 000000000..74352e8ad --- /dev/null +++ b/tests/command.collectAllOptions.test.js @@ -0,0 +1,59 @@ +var { Command } = require('../'); + +function createCommanderInstance(mockFn) { + const subCmd = new Command() + .name('sub_cmd') + .option('-f, --force'); + subCmd + .command('sub_sub_cmd') + .option('-d, --delete') + .action(mockFn); + + const root = new Command(); + root + .option('-q, --quiet'); + root + .useSubcommand(subCmd); + + return root; +} + +// TESTS + +test('should collect options from all 3 levels when all passed', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + program.parse(['node', 'test', 'sub_cmd', 'sub_sub_cmd', '-f', '-q', '-d']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('sub_sub_cmd'); + expect(actionMock.mock.calls[0][0].collectAllOptions()).toEqual({ + quiet: true, + force: true, + delete: true + }); +}); + +test('should collect options from all 3 levels when just some passed', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + program.parse(['node', 'test', 'sub_cmd', 'sub_sub_cmd', '-q']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('sub_sub_cmd'); + + const allOpts = actionMock.mock.calls[0][0].collectAllOptions(); + expect(allOpts).toEqual({ + quiet: true, + force: undefined, + delete: undefined + }); + // The attrs are enumerable, just undefined ! + expect(Object.keys(allOpts).sort()).toEqual(['delete', 'force', 'quiet']); +}); diff --git a/tests/command.useSubcommand.test.js b/tests/command.useSubcommand.test.js new file mode 100644 index 000000000..ab560f659 --- /dev/null +++ b/tests/command.useSubcommand.test.js @@ -0,0 +1,107 @@ +var { Command } = require('../'); + +function createCommanderInstance(mockFn) { + const cmd2 = new Command() + .name('cmd2'); + cmd2 + .command('subCmd') + .action(mockFn); + + const cmd3 = new Command() + .name('cmd3') + .option('-q, --quiet'); + cmd3 + .command('subWithOpt') + .option('-f, --force') + .action(mockFn); + cmd3 + .command('subWithParam ') + .action(mockFn); + + const root = new Command(); + root + .command('cmd1') + .action(mockFn); + root + .useSubcommand(cmd2) + .useSubcommand(cmd3); + + return root; +} + +// TESTS + +test('should envoke 1 level command', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + program.parse(['node', 'test', 'cmd1']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('cmd1'); +}); + +test('should envoke 2 level sub command', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + program.parse(['node', 'test', 'cmd2', 'subCmd']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('subCmd'); +}); + +test('should envoke 2 level sub command with subcommand options', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + program.parse(['node', 'test', 'cmd3', 'subWithOpt']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('subWithOpt'); + expect(actionMock.mock.calls[0][0].force).toBeFalsy(); + + actionMock.mockReset(); + program.parse(['node', 'test', 'cmd3', 'subWithOpt', '-f']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('subWithOpt'); + expect(actionMock.mock.calls[0][0].force).toBeTruthy(); +}); + +test('should envoke 2 level sub command with with subcommand param', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + program.parse(['node', 'test', 'cmd3', 'subWithParam', 'theparam']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(2); + expect(actionMock.mock.calls[0][0]).toBe('theparam'); + expect(actionMock.mock.calls[0][1] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][1].name()).toBe('subWithParam'); +}); + +test('should envoke 2 level sub command with options on several levels', () => { + const actionMock = jest.fn(); + const program = createCommanderInstance(actionMock); + + // -f belongs to subWithOpt, -q belongs to cmd3 + program.parse(['node', 'test', 'cmd3', 'subWithOpt', '-f', '-q']); + + expect(actionMock).toHaveBeenCalledTimes(1); + expect(actionMock.mock.calls[0].length).toBe(1); + expect(actionMock.mock.calls[0][0] instanceof Command).toBeTruthy(); + expect(actionMock.mock.calls[0][0].name()).toBe('subWithOpt'); + expect(actionMock.mock.calls[0][0].force).toBeTruthy(); + expect(actionMock.mock.calls[0][0].quiet).toBeUndefined(); + expect(actionMock.mock.calls[0][0].parent.quiet).toBeTruthy(); +});