Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support showing global options in help #1828

Merged
merged 6 commits into from Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Readme.md
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
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
@@ -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
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
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
@@ -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
@@ -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
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
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