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. 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..ec5f30fa2 --- /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 global-options-nested.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 diff --git a/lib/help.js b/lib/help.js index 2b8c95596..90d9d68cc 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. 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)); + } + /** * 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..5d9cb8642 --- /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));