From e8a1f91a9c4128ff805bcb43b32de8021bff194d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Dec 2022 18:55:43 +1300 Subject: [PATCH 1/6] Add global options to Help --- lib/help.js | 69 ++++++++++++++++++++++--- tests/help.padWidth.test.js | 13 +++++ tests/help.showGlobalOptions.test.js | 62 ++++++++++++++++++++++ tests/help.visibleGlobalOptions.test.js | 45 ++++++++++++++++ typings/index.d.ts | 5 ++ typings/index.test-d.ts | 3 ++ 6 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 tests/help.showGlobalOptions.test.js create mode 100644 tests/help.visibleGlobalOptions.test.js diff --git a/lib/help.js b/lib/help.js index 2b8c95596..0b8773b6b 100644 --- a/lib/help.js +++ b/lib/help.js @@ -16,6 +16,7 @@ class Help { this.helpWidth = undefined; this.sortSubcommands = false; this.sortOptions = false; + this.showGlobalOptions = false; } /** @@ -45,6 +46,21 @@ class Help { return visibleCommands; } + /** + * Compare options for sort. + * + * @param {Option} a + * @param {Option} b + * @returns number + */ + compareOptions(a, b) { + const getSortKey = (option) => { + // WYSIWYG for order displayed in help with short before long, no special handling for negated. + return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, ''); + }; + return getSortKey(a).localeCompare(getSortKey(b)); + } + /** * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. * @@ -69,17 +85,32 @@ class Help { visibleOptions.push(helpOption); } if (this.sortOptions) { - const getSortKey = (option) => { - // WYSIWYG for order displayed in help with short before long, no special handling for negated. - return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, ''); - }; - visibleOptions.sort((a, b) => { - return getSortKey(a).localeCompare(getSortKey(b)); - }); + visibleOptions.sort(this.compareOptions); } return visibleOptions; } + /** + * Get an array of the visible global options. (Not including help.) + * + * @param {Command} cmd + * @returns {Option[]} + */ + + visibleGlobalOptions(cmd) { + if (!this.showGlobalOptions) return []; + + const globalOptions = []; + for (let parentCmd = cmd.parent; parentCmd; parentCmd = parentCmd.parent) { + const visibleOptions = parentCmd.options.filter((option) => !option.hidden); + globalOptions.push(...visibleOptions); + } + if (this.sortOptions) { + globalOptions.sort(this.compareOptions); + } + return globalOptions; + } + /** * Get an array of the arguments if any have a description. * @@ -168,6 +199,20 @@ class Help { }, 0); } + /** + * Get the longest global option term length. + * + * @param {Command} cmd + * @param {Help} helper + * @returns {number} + */ + + longestGlobalOptionTermLength(cmd, helper) { + return helper.visibleGlobalOptions(cmd).reduce((max, option) => { + return Math.max(max, helper.optionTerm(option).length); + }, 0); + } + /** * Get the longest argument term length. * @@ -341,6 +386,15 @@ class Help { output = output.concat(['Options:', formatList(optionList), '']); } + if (this.showGlobalOptions) { + const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => { + return formatItem(helper.optionTerm(option), helper.optionDescription(option)); + }); + if (globalOptionList.length > 0) { + output = output.concat(['Global Options:', formatList(globalOptionList), '']); + } + } + // Commands const commandList = helper.visibleCommands(cmd).map((cmd) => { return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd)); @@ -363,6 +417,7 @@ class Help { padWidth(cmd, helper) { return Math.max( helper.longestOptionTermLength(cmd, helper), + helper.longestGlobalOptionTermLength(cmd, helper), helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper) ); diff --git a/tests/help.padWidth.test.js b/tests/help.padWidth.test.js index affa3e201..b9ace7ae0 100644 --- a/tests/help.padWidth.test.js +++ b/tests/help.padWidth.test.js @@ -28,6 +28,19 @@ describe('padWidth', () => { expect(helper.padWidth(program, helper)).toEqual(longestThing.length); }); + test('when global option term longest return global option length', () => { + const longestThing = '--very-long-thing-bigger-than-others'; + const program = new commander.Command(); + program + .argument('', 'desc') + .option(longestThing) + .configureHelp({ showGlobalOptions: true }); + const sub = program + .command('sub'); + const helper = sub.createHelp(); + expect(helper.padWidth(sub, helper)).toEqual(longestThing.length); + }); + test('when command term longest return command length', () => { const longestThing = 'very-long-thing-bigger-than-others'; const program = new commander.Command(); diff --git a/tests/help.showGlobalOptions.test.js b/tests/help.showGlobalOptions.test.js new file mode 100644 index 000000000..dd1e68a0a --- /dev/null +++ b/tests/help.showGlobalOptions.test.js @@ -0,0 +1,62 @@ +const commander = require('../'); + +test('when default configuration then global options hidden', () => { + const program = new commander.Command(); + program + .option('--global'); + const sub = program.command('sub'); + expect(sub.helpInformation()).not.toContain('global'); +}); + +test('when showGlobalOptions:true then program options shown', () => { + const program = new commander.Command(); + program + .option('--global') + .configureHelp({ showGlobalOptions: true }); + const sub = program.command('sub'); + expect(sub.helpInformation()).toContain('global'); +}); + +test('when showGlobalOptions:true and no global options then global options header not shown', () => { + const program = new commander.Command(); + program + .configureHelp({ showGlobalOptions: true }); + const sub = program.command('sub'); + expect(sub.helpInformation()).not.toContain('Global'); +}); + +test('when showGlobalOptions:true and nested commands then combined nested options shown program last', () => { + const program = new commander.Command(); + program + .option('--global') + .configureHelp({ showGlobalOptions: true }); + const sub1 = program.command('sub1') + .option('--sub1'); + const sub2 = sub1.command('sub2'); + expect(sub2.helpInformation()).toContain(`Global Options: + --sub1 + --global +`); +}); + +test('when showGlobalOptions:true and sortOptions: true then global options sorted', () => { + const program = new commander.Command(); + program + .option('-3') + .option('-4') + .option('-2') + .configureHelp({ showGlobalOptions: true, sortOptions: true }); + const sub1 = program.command('sub1') + .option('-6') + .option('-1') + .option('-5'); + const sub2 = sub1.command('sub2'); + expect(sub2.helpInformation()).toContain(`Global Options + -1 + -2 + -3 + -4 + -5 + -6 +`); +}); diff --git a/tests/help.visibleGlobalOptions.test.js b/tests/help.visibleGlobalOptions.test.js new file mode 100644 index 000000000..00b8e7f25 --- /dev/null +++ b/tests/help.visibleGlobalOptions.test.js @@ -0,0 +1,45 @@ +const commander = require('../'); + +test('when default configuration then return empty array', () => { + const program = new commander.Command(); + program + .option('--global'); + const sub = program.command('sub'); + const helper = sub.createHelp(); + expect(helper.visibleGlobalOptions(program)).toEqual([]); +}); + +test('when showGlobalOptions:true then return program options', () => { + const program = new commander.Command(); + program + .option('--global') + .configureHelp({ showGlobalOptions: true }); + const sub = program.command('sub'); + const helper = sub.createHelp(); + const visibleOptionNames = helper.visibleGlobalOptions(sub).map(option => option.name()); + expect(visibleOptionNames).toEqual(['global']); +}); + +test('when showGlobalOptions:true and program has version then return version', () => { + const program = new commander.Command(); + program + .configureHelp({ showGlobalOptions: true }) + .version('1.2.3'); + const sub = program.command('sub'); + const helper = sub.createHelp(); + const visibleOptionNames = helper.visibleGlobalOptions(sub).map(option => option.name()); + expect(visibleOptionNames).toEqual(['version']); +}); + +test('when showGlobalOptions:true and nested commands then return combined global options', () => { + const program = new commander.Command(); + program + .configureHelp({ showGlobalOptions: true }) + .option('--global'); + const sub1 = program.command('sub1') + .option('--sub1'); + const sub2 = sub1.command('sub2'); + const helper = sub2.createHelp(); + const visibleOptionNames = helper.visibleGlobalOptions(sub2).map(option => option.name()); + expect(visibleOptionNames).toEqual(['sub1', 'global']); +}); diff --git a/typings/index.d.ts b/typings/index.d.ts index b69ea9104..7c76805d8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -199,6 +199,7 @@ export class Help { helpWidth?: number; sortSubcommands: boolean; sortOptions: boolean; + showGlobalOptions: boolean; constructor(); @@ -224,6 +225,8 @@ export class Help { visibleCommands(cmd: Command): Command[]; /** Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. */ visibleOptions(cmd: Command): Option[]; + /** Get an array of the visible global options. (Not including help.) */ + visibleGlobalOptions(cmd: Command): Option[]; /** Get an array of the arguments which have descriptions. */ visibleArguments(cmd: Command): Argument[]; @@ -231,6 +234,8 @@ export class Help { longestSubcommandTermLength(cmd: Command, helper: Help): number; /** Get the longest option term length. */ longestOptionTermLength(cmd: Command, helper: Help): number; + /** Get the longest global option term length. */ + longestGlobalOptionTermLength(cmd: Command, helper: Help): number; /** Get the longest argument term length. */ longestArgumentTermLength(cmd: Command, helper: Help): number; /** Calculate the pad width from the maximum term length. */ diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 7b8af12ea..7df4cc9e8 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -366,6 +366,7 @@ const helperArgument = new commander.Argument(''); expectType(helper.helpWidth); expectType(helper.sortSubcommands); expectType(helper.sortOptions); +expectType(helper.showGlobalOptions); expectType(helper.subcommandTerm(helperCommand)); expectType(helper.commandUsage(helperCommand)); @@ -378,10 +379,12 @@ expectType(helper.argumentDescription(helperArgument)); expectType(helper.visibleCommands(helperCommand)); expectType(helper.visibleOptions(helperCommand)); +expectType(helper.visibleGlobalOptions(helperCommand)); expectType(helper.visibleArguments(helperCommand)); expectType(helper.longestSubcommandTermLength(helperCommand, helper)); expectType(helper.longestOptionTermLength(helperCommand, helper)); +expectType(helper.longestGlobalOptionTermLength(helperCommand, helper)); expectType(helper.longestArgumentTermLength(helperCommand, helper)); expectType(helper.padWidth(helperCommand, helper)); From 79309bf8bb1c12b64820fe1e15b4407b7756400d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Dec 2022 19:03:10 +1300 Subject: [PATCH 2/6] Fix test --- tests/help.showGlobalOptions.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/help.showGlobalOptions.test.js b/tests/help.showGlobalOptions.test.js index dd1e68a0a..5d9cb8642 100644 --- a/tests/help.showGlobalOptions.test.js +++ b/tests/help.showGlobalOptions.test.js @@ -51,7 +51,7 @@ test('when showGlobalOptions:true and sortOptions: true then global options sort .option('-1') .option('-5'); const sub2 = sub1.command('sub2'); - expect(sub2.helpInformation()).toContain(`Global Options + expect(sub2.helpInformation()).toContain(`Global Options: -1 -2 -3 From b75738880483848025462db5d7c98a449c3c13d2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Dec 2022 19:30:48 +1300 Subject: [PATCH 3/6] Clarify comment about option sorting --- lib/help.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/help.js b/lib/help.js index 0b8773b6b..90d9d68cc 100644 --- a/lib/help.js +++ b/lib/help.js @@ -55,7 +55,7 @@ class Help { */ compareOptions(a, b) { const getSortKey = (option) => { - // WYSIWYG for order displayed in help with short before long, no special handling for negated. + // WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated. return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, ''); }; return getSortKey(a).localeCompare(getSortKey(b)); From a56e311548356bfb0be396f6d3c9c151cf23db3f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Dec 2022 19:53:56 +1300 Subject: [PATCH 4/6] Add contrasting global option example --- ...bal-options.js => global-options-added.js} | 10 +++--- examples/global-options-nested.js | 32 +++++++++++++++++++ examples/optsWithGlobals.js | 24 -------------- 3 files changed, 37 insertions(+), 29 deletions(-) rename examples/{global-options.js => global-options-added.js} (85%) create mode 100644 examples/global-options-nested.js delete mode 100644 examples/optsWithGlobals.js diff --git a/examples/global-options.js b/examples/global-options-added.js similarity index 85% rename from examples/global-options.js rename to examples/global-options-added.js index 61ca65236..a064f9a83 100644 --- a/examples/global-options.js +++ b/examples/global-options-added.js @@ -7,7 +7,7 @@ // The code in this example assumes there is just one level of subcommands. // // (A different pattern for a "global" option is to add it to the root command, rather -// than to the subcommand. That is not shown here.) +// than to the subcommand. See global-options-nested.js.) // const { Command } = require('commander'); // (normal include) const { Command } = require('../'); // include commander in git clone of commander repo @@ -45,7 +45,7 @@ program.commands.forEach((cmd) => { program.parse(); // Try the following: -// node common-options.js --help -// node common-options.js print --help -// node common-options.js serve --help -// node common-options.js serve --debug --verbose +// node global-options-added.js --help +// node global-options-added.js print --help +// node global-options-added.js serve --help +// node global-options-added.js serve --debug --verbose diff --git a/examples/global-options-nested.js b/examples/global-options-nested.js new file mode 100644 index 000000000..2813be439 --- /dev/null +++ b/examples/global-options-nested.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +// This example shows global options on the program which affect all the subcommands. +// See how to work with global options in the subcommand and display them in the help. +// +// (A different pattern for a "global" option is to add it to the subcommands, rather +// than to the program. See global-options-added.js.) + +// const { Command } = require('commander'); // (normal include) +const { Command } = require('../'); // include commander in git clone of commander repo + +const program = new Command(); + +program + .configureHelp({ showGlobalOptions: true }) + .option('-g, --global'); + +program + .command('sub') + .option('-l, --local') + .action((options, cmd) => { + console.log({ + opts: cmd.opts(), + optsWithGlobals: cmd.optsWithGlobals() + }); + }); + +program.parse(); + +// Try the following: +// node global-options-nested.js --global sub --local +// node optsWithGlobals.js sub --help diff --git a/examples/optsWithGlobals.js b/examples/optsWithGlobals.js deleted file mode 100644 index 366597c06..000000000 --- a/examples/optsWithGlobals.js +++ /dev/null @@ -1,24 +0,0 @@ -// const { Command } = require('commander'); // (normal include) -const { Command } = require('../'); // include commander in git clone of commander repo - -// Show use of .optsWithGlobals(), and compare with .opts(). - -const program = new Command(); - -program - .option('-g, --global'); - -program - .command('sub') - .option('-l, --local') - .action((options, cmd) => { - console.log({ - opts: cmd.opts(), - optsWithGlobals: cmd.optsWithGlobals() - }); - }); - -program.parse(); - -// Try the following: -// node optsWithGlobals.js --global sub --local From 36b942008f06bceb31e88f7940917065a7305708 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Dec 2022 19:58:27 +1300 Subject: [PATCH 5/6] Add mention of showGlobalOptions in README --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 48675a940..2e377bcc3 100644 --- a/Readme.md +++ b/Readme.md @@ -915,6 +915,7 @@ The data properties are: - `helpWidth`: specify the wrap width, useful for unit tests - `sortSubcommands`: sort the subcommands alphabetically - `sortOptions`: sort the options alphabetically +- `showGlobalOptions`: show a section with the global options from the parent command(s) There are methods getting the visible lists of arguments, options, and subcommands. There are methods for formatting the items in the lists, with each item having a _term_ and _description_. Take a look at `.formatHelp()` to see how they are used. From 311270b5b359325ca0e0c4591acc9a43f41c0e3d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 3 Dec 2022 19:59:53 +1300 Subject: [PATCH 6/6] Fix filename in instructions --- examples/global-options-nested.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/global-options-nested.js b/examples/global-options-nested.js index 2813be439..ec5f30fa2 100644 --- a/examples/global-options-nested.js +++ b/examples/global-options-nested.js @@ -29,4 +29,4 @@ program.parse(); // Try the following: // node global-options-nested.js --global sub --local -// node optsWithGlobals.js sub --help +// node global-options-nested.js sub --help