Skip to content

Commit 0ae5b2f

Browse files
authored
Support showing global options in help (#1828)
Add help configuration to show the global options.
1 parent 198c6e4 commit 0ae5b2f

10 files changed

+228
-36
lines changed

Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ The data properties are:
915915
- `helpWidth`: specify the wrap width, useful for unit tests
916916
- `sortSubcommands`: sort the subcommands alphabetically
917917
- `sortOptions`: sort the options alphabetically
918+
- `showGlobalOptions`: show a section with the global options from the parent command(s)
918919

919920
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.
920921

examples/global-options.js renamed to examples/global-options-added.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// The code in this example assumes there is just one level of subcommands.
88
//
99
// (A different pattern for a "global" option is to add it to the root command, rather
10-
// than to the subcommand. That is not shown here.)
10+
// than to the subcommand. See global-options-nested.js.)
1111

1212
// const { Command } = require('commander'); // (normal include)
1313
const { Command } = require('../'); // include commander in git clone of commander repo
@@ -45,7 +45,7 @@ program.commands.forEach((cmd) => {
4545
program.parse();
4646

4747
// Try the following:
48-
// node common-options.js --help
49-
// node common-options.js print --help
50-
// node common-options.js serve --help
51-
// node common-options.js serve --debug --verbose
48+
// node global-options-added.js --help
49+
// node global-options-added.js print --help
50+
// node global-options-added.js serve --help
51+
// node global-options-added.js serve --debug --verbose

examples/global-options-nested.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env node
2+
3+
// This example shows global options on the program which affect all the subcommands.
4+
// See how to work with global options in the subcommand and display them in the help.
5+
//
6+
// (A different pattern for a "global" option is to add it to the subcommands, rather
7+
// than to the program. See global-options-added.js.)
8+
9+
// const { Command } = require('commander'); // (normal include)
10+
const { Command } = require('../'); // include commander in git clone of commander repo
11+
12+
const program = new Command();
13+
14+
program
15+
.configureHelp({ showGlobalOptions: true })
16+
.option('-g, --global');
17+
18+
program
19+
.command('sub')
20+
.option('-l, --local')
21+
.action((options, cmd) => {
22+
console.log({
23+
opts: cmd.opts(),
24+
optsWithGlobals: cmd.optsWithGlobals()
25+
});
26+
});
27+
28+
program.parse();
29+
30+
// Try the following:
31+
// node global-options-nested.js --global sub --local
32+
// node global-options-nested.js sub --help

examples/optsWithGlobals.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

lib/help.js

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Help {
1616
this.helpWidth = undefined;
1717
this.sortSubcommands = false;
1818
this.sortOptions = false;
19+
this.showGlobalOptions = false;
1920
}
2021

2122
/**
@@ -45,6 +46,21 @@ class Help {
4546
return visibleCommands;
4647
}
4748

49+
/**
50+
* Compare options for sort.
51+
*
52+
* @param {Option} a
53+
* @param {Option} b
54+
* @returns number
55+
*/
56+
compareOptions(a, b) {
57+
const getSortKey = (option) => {
58+
// WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.
59+
return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, '');
60+
};
61+
return getSortKey(a).localeCompare(getSortKey(b));
62+
}
63+
4864
/**
4965
* Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.
5066
*
@@ -69,17 +85,32 @@ class Help {
6985
visibleOptions.push(helpOption);
7086
}
7187
if (this.sortOptions) {
72-
const getSortKey = (option) => {
73-
// WYSIWYG for order displayed in help with short before long, no special handling for negated.
74-
return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, '');
75-
};
76-
visibleOptions.sort((a, b) => {
77-
return getSortKey(a).localeCompare(getSortKey(b));
78-
});
88+
visibleOptions.sort(this.compareOptions);
7989
}
8090
return visibleOptions;
8191
}
8292

93+
/**
94+
* Get an array of the visible global options. (Not including help.)
95+
*
96+
* @param {Command} cmd
97+
* @returns {Option[]}
98+
*/
99+
100+
visibleGlobalOptions(cmd) {
101+
if (!this.showGlobalOptions) return [];
102+
103+
const globalOptions = [];
104+
for (let parentCmd = cmd.parent; parentCmd; parentCmd = parentCmd.parent) {
105+
const visibleOptions = parentCmd.options.filter((option) => !option.hidden);
106+
globalOptions.push(...visibleOptions);
107+
}
108+
if (this.sortOptions) {
109+
globalOptions.sort(this.compareOptions);
110+
}
111+
return globalOptions;
112+
}
113+
83114
/**
84115
* Get an array of the arguments if any have a description.
85116
*
@@ -168,6 +199,20 @@ class Help {
168199
}, 0);
169200
}
170201

202+
/**
203+
* Get the longest global option term length.
204+
*
205+
* @param {Command} cmd
206+
* @param {Help} helper
207+
* @returns {number}
208+
*/
209+
210+
longestGlobalOptionTermLength(cmd, helper) {
211+
return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
212+
return Math.max(max, helper.optionTerm(option).length);
213+
}, 0);
214+
}
215+
171216
/**
172217
* Get the longest argument term length.
173218
*
@@ -341,6 +386,15 @@ class Help {
341386
output = output.concat(['Options:', formatList(optionList), '']);
342387
}
343388

389+
if (this.showGlobalOptions) {
390+
const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
391+
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
392+
});
393+
if (globalOptionList.length > 0) {
394+
output = output.concat(['Global Options:', formatList(globalOptionList), '']);
395+
}
396+
}
397+
344398
// Commands
345399
const commandList = helper.visibleCommands(cmd).map((cmd) => {
346400
return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
@@ -363,6 +417,7 @@ class Help {
363417
padWidth(cmd, helper) {
364418
return Math.max(
365419
helper.longestOptionTermLength(cmd, helper),
420+
helper.longestGlobalOptionTermLength(cmd, helper),
366421
helper.longestSubcommandTermLength(cmd, helper),
367422
helper.longestArgumentTermLength(cmd, helper)
368423
);

tests/help.padWidth.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ describe('padWidth', () => {
2828
expect(helper.padWidth(program, helper)).toEqual(longestThing.length);
2929
});
3030

31+
test('when global option term longest return global option length', () => {
32+
const longestThing = '--very-long-thing-bigger-than-others';
33+
const program = new commander.Command();
34+
program
35+
.argument('<file>', 'desc')
36+
.option(longestThing)
37+
.configureHelp({ showGlobalOptions: true });
38+
const sub = program
39+
.command('sub');
40+
const helper = sub.createHelp();
41+
expect(helper.padWidth(sub, helper)).toEqual(longestThing.length);
42+
});
43+
3144
test('when command term longest return command length', () => {
3245
const longestThing = 'very-long-thing-bigger-than-others';
3346
const program = new commander.Command();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const commander = require('../');
2+
3+
test('when default configuration then global options hidden', () => {
4+
const program = new commander.Command();
5+
program
6+
.option('--global');
7+
const sub = program.command('sub');
8+
expect(sub.helpInformation()).not.toContain('global');
9+
});
10+
11+
test('when showGlobalOptions:true then program options shown', () => {
12+
const program = new commander.Command();
13+
program
14+
.option('--global')
15+
.configureHelp({ showGlobalOptions: true });
16+
const sub = program.command('sub');
17+
expect(sub.helpInformation()).toContain('global');
18+
});
19+
20+
test('when showGlobalOptions:true and no global options then global options header not shown', () => {
21+
const program = new commander.Command();
22+
program
23+
.configureHelp({ showGlobalOptions: true });
24+
const sub = program.command('sub');
25+
expect(sub.helpInformation()).not.toContain('Global');
26+
});
27+
28+
test('when showGlobalOptions:true and nested commands then combined nested options shown program last', () => {
29+
const program = new commander.Command();
30+
program
31+
.option('--global')
32+
.configureHelp({ showGlobalOptions: true });
33+
const sub1 = program.command('sub1')
34+
.option('--sub1');
35+
const sub2 = sub1.command('sub2');
36+
expect(sub2.helpInformation()).toContain(`Global Options:
37+
--sub1
38+
--global
39+
`);
40+
});
41+
42+
test('when showGlobalOptions:true and sortOptions: true then global options sorted', () => {
43+
const program = new commander.Command();
44+
program
45+
.option('-3')
46+
.option('-4')
47+
.option('-2')
48+
.configureHelp({ showGlobalOptions: true, sortOptions: true });
49+
const sub1 = program.command('sub1')
50+
.option('-6')
51+
.option('-1')
52+
.option('-5');
53+
const sub2 = sub1.command('sub2');
54+
expect(sub2.helpInformation()).toContain(`Global Options:
55+
-1
56+
-2
57+
-3
58+
-4
59+
-5
60+
-6
61+
`);
62+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const commander = require('../');
2+
3+
test('when default configuration then return empty array', () => {
4+
const program = new commander.Command();
5+
program
6+
.option('--global');
7+
const sub = program.command('sub');
8+
const helper = sub.createHelp();
9+
expect(helper.visibleGlobalOptions(program)).toEqual([]);
10+
});
11+
12+
test('when showGlobalOptions:true then return program options', () => {
13+
const program = new commander.Command();
14+
program
15+
.option('--global')
16+
.configureHelp({ showGlobalOptions: true });
17+
const sub = program.command('sub');
18+
const helper = sub.createHelp();
19+
const visibleOptionNames = helper.visibleGlobalOptions(sub).map(option => option.name());
20+
expect(visibleOptionNames).toEqual(['global']);
21+
});
22+
23+
test('when showGlobalOptions:true and program has version then return version', () => {
24+
const program = new commander.Command();
25+
program
26+
.configureHelp({ showGlobalOptions: true })
27+
.version('1.2.3');
28+
const sub = program.command('sub');
29+
const helper = sub.createHelp();
30+
const visibleOptionNames = helper.visibleGlobalOptions(sub).map(option => option.name());
31+
expect(visibleOptionNames).toEqual(['version']);
32+
});
33+
34+
test('when showGlobalOptions:true and nested commands then return combined global options', () => {
35+
const program = new commander.Command();
36+
program
37+
.configureHelp({ showGlobalOptions: true })
38+
.option('--global');
39+
const sub1 = program.command('sub1')
40+
.option('--sub1');
41+
const sub2 = sub1.command('sub2');
42+
const helper = sub2.createHelp();
43+
const visibleOptionNames = helper.visibleGlobalOptions(sub2).map(option => option.name());
44+
expect(visibleOptionNames).toEqual(['sub1', 'global']);
45+
});

typings/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export class Help {
199199
helpWidth?: number;
200200
sortSubcommands: boolean;
201201
sortOptions: boolean;
202+
showGlobalOptions: boolean;
202203

203204
constructor();
204205

@@ -224,13 +225,17 @@ export class Help {
224225
visibleCommands(cmd: Command): Command[];
225226
/** Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. */
226227
visibleOptions(cmd: Command): Option[];
228+
/** Get an array of the visible global options. (Not including help.) */
229+
visibleGlobalOptions(cmd: Command): Option[];
227230
/** Get an array of the arguments which have descriptions. */
228231
visibleArguments(cmd: Command): Argument[];
229232

230233
/** Get the longest command term length. */
231234
longestSubcommandTermLength(cmd: Command, helper: Help): number;
232235
/** Get the longest option term length. */
233236
longestOptionTermLength(cmd: Command, helper: Help): number;
237+
/** Get the longest global option term length. */
238+
longestGlobalOptionTermLength(cmd: Command, helper: Help): number;
234239
/** Get the longest argument term length. */
235240
longestArgumentTermLength(cmd: Command, helper: Help): number;
236241
/** Calculate the pad width from the maximum term length. */

typings/index.test-d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ const helperArgument = new commander.Argument('<file>');
366366
expectType<number | undefined>(helper.helpWidth);
367367
expectType<boolean>(helper.sortSubcommands);
368368
expectType<boolean>(helper.sortOptions);
369+
expectType<boolean>(helper.showGlobalOptions);
369370

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

379380
expectType<commander.Command[]>(helper.visibleCommands(helperCommand));
380381
expectType<commander.Option[]>(helper.visibleOptions(helperCommand));
382+
expectType<commander.Option[]>(helper.visibleGlobalOptions(helperCommand));
381383
expectType<commander.Argument[]>(helper.visibleArguments(helperCommand));
382384

383385
expectType<number>(helper.longestSubcommandTermLength(helperCommand, helper));
384386
expectType<number>(helper.longestOptionTermLength(helperCommand, helper));
387+
expectType<number>(helper.longestGlobalOptionTermLength(helperCommand, helper));
385388
expectType<number>(helper.longestArgumentTermLength(helperCommand, helper));
386389
expectType<number>(helper.padWidth(helperCommand, helper));
387390

0 commit comments

Comments
 (0)