Skip to content

Commit

Permalink
Support showing global options in help (#1828)
Browse files Browse the repository at this point in the history
Add help configuration to show the global options.
  • Loading branch information
shadowspawn authored Dec 7, 2022
1 parent 198c6e4 commit 0ae5b2f
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 36 deletions.
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 5 additions & 5 deletions examples/global-options.js → examples/global-options-added.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
32 changes: 32 additions & 0 deletions examples/global-options-nested.js
Original file line number Diff line number Diff line change
@@ -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
24 changes: 0 additions & 24 deletions examples/optsWithGlobals.js

This file was deleted.

69 changes: 62 additions & 7 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Help {
this.helpWidth = undefined;
this.sortSubcommands = false;
this.sortOptions = false;
this.showGlobalOptions = false;
}

/**
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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));
Expand All @@ -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)
);
Expand Down
13 changes: 13 additions & 0 deletions tests/help.padWidth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<file>', '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();
Expand Down
62 changes: 62 additions & 0 deletions tests/help.showGlobalOptions.test.js
Original file line number Diff line number Diff line change
@@ -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
`);
});
45 changes: 45 additions & 0 deletions tests/help.visibleGlobalOptions.test.js
Original file line number Diff line number Diff line change
@@ -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']);
});
5 changes: 5 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export class Help {
helpWidth?: number;
sortSubcommands: boolean;
sortOptions: boolean;
showGlobalOptions: boolean;

constructor();

Expand All @@ -224,13 +225,17 @@ 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[];

/** Get the longest command term length. */
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. */
Expand Down
3 changes: 3 additions & 0 deletions typings/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ const helperArgument = new commander.Argument('<file>');
expectType<number | undefined>(helper.helpWidth);
expectType<boolean>(helper.sortSubcommands);
expectType<boolean>(helper.sortOptions);
expectType<boolean>(helper.showGlobalOptions);

expectType<string>(helper.subcommandTerm(helperCommand));
expectType<string>(helper.commandUsage(helperCommand));
Expand All @@ -378,10 +379,12 @@ expectType<string>(helper.argumentDescription(helperArgument));

expectType<commander.Command[]>(helper.visibleCommands(helperCommand));
expectType<commander.Option[]>(helper.visibleOptions(helperCommand));
expectType<commander.Option[]>(helper.visibleGlobalOptions(helperCommand));
expectType<commander.Argument[]>(helper.visibleArguments(helperCommand));

expectType<number>(helper.longestSubcommandTermLength(helperCommand, helper));
expectType<number>(helper.longestOptionTermLength(helperCommand, helper));
expectType<number>(helper.longestGlobalOptionTermLength(helperCommand, helper));
expectType<number>(helper.longestArgumentTermLength(helperCommand, helper));
expectType<number>(helper.padWidth(helperCommand, helper));

Expand Down

0 comments on commit 0ae5b2f

Please sign in to comment.