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

Add support for groups of options and commands in the help #2328

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
9 changes: 9 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [.description and .summary](#description-and-summary)
- [.helpOption(flags, description)](#helpoptionflags-description)
- [.helpCommand()](#helpcommand)
- [Help Groups](#help-groups)
- [More configuration](#more-configuration-2)
- [Custom event listeners](#custom-event-listeners)
- [Bits and pieces](#bits-and-pieces)
Expand Down Expand Up @@ -926,6 +927,14 @@ program.helpCommand('assist [command]', 'show assistance');

(Or use `.addHelpCommand()` to add a command you construct yourself.)

### Help Groups

The help by default lists options under the the heading `Options:` and commands under `Commands:`. You can create your own groups
with different headings. The high-level way is to set the desired group heading while adding the options and commands,
using `.optionsGroup()` and `.commandsGroup()`. The low-level way is using `.helpGroup()` on an individual `Option` or `Command`

Example file: [help-groups.js](./examples/help-groups.js)

### More configuration

The built-in help is formatted using the Help class.
Expand Down
75 changes: 75 additions & 0 deletions examples/help-groups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const { Command, Option } = require('commander');

// Show the two approaches for adding help groups, and how to customise the built-in help and version.

const program = new Command();
const devOptionsTitle = 'Development Options:';
const managementCommandsTitle = 'Management Commands:';

// The high-level approach is use .optionsGroup() and .commandsGroup() before adding the options/commands.
const docker1 = program
.command('docker1')
.description('help groups created using .optionsGroup() and .commandsGroup()')
.addOption(new Option('-h, --hostname <name>', 'container host name'))
.addOption(new Option('-p, --port <number>', 'container port number'))
.optionsGroup(devOptionsTitle)
.option('-d, --debug', 'add extra trace information')
.option('-w, --watch', 'run and relaunch service on file changes');

docker1
.command('run')
.description('create and run a new container from an image');
docker1.command('exec').description('execute a command in a running container');

docker1.commandsGroup(managementCommandsTitle);
docker1.command('images').description('manage images');
docker1.command('volumes').description('manage volumes');

// The low-level approach is using .helpGroup() on the Option or Command.
const docker2 = program
.command('docker2')
.description('help groups created using .helpGroup()')
.addOption(new Option('-h, --hostname <name>', 'container host name'))
.addOption(new Option('-p, --port <number>', 'container port number'))
.addOption(
new Option('-d, --debug', 'add extra trace information').helpGroup(
devOptionsTitle,
),
)
.addOption(
new Option(
'-w, --watch',
'run and relaunch service on file changes',
).helpGroup(devOptionsTitle),
);

docker2
.command('run')
.description('create and run a new container from an image');
docker2.command('exec').description('execute a command in a running container');

docker2
.command('images')
.description('manage images')
.helpGroup(managementCommandsTitle);
docker2
.command('volumes')
.description('manage volumes')
.helpGroup(managementCommandsTitle);

// Customise group for built-ins by configuring them with default group set.
program
.command('built-in')
.description('help groups for help and version')
.optionsGroup('Built-in Options:')
.version('v2.3.4')
.helpOption('-h, --help') // or .helpOption(true) to use default flags
.commandsGroup('Built-in Commands:')
.helpCommand('help [command]'); // or .helpCommand(true) to use default name

program.parse();

// Try the following:
// node help-groups.js help docker1
// node help-groups.js help docker2
// node help-groups.js help built-in
108 changes: 99 additions & 9 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ class Command extends EventEmitter {
/** @type {Command} */
this._helpCommand = undefined; // lazy initialised, inherited
this._helpConfiguration = {};
/** @type {string | undefined} */
this._helpGroupHeading = undefined; // soft initialised when added to parent
/** @type {string | undefined} */
this._defaultCommandGroup = undefined;
/** @type {string | undefined} */
this._defaultOptionGroup = undefined;
}

/**
Expand Down Expand Up @@ -400,11 +406,15 @@ class Command extends EventEmitter {
helpCommand(enableOrNameAndArgs, description) {
if (typeof enableOrNameAndArgs === 'boolean') {
this._addImplicitHelpCommand = enableOrNameAndArgs;
if (enableOrNameAndArgs && this._defaultCommandGroup) {
// make the command to store the group
this._initCommandGroup(this._getHelpCommand());
}
return this;
}

enableOrNameAndArgs = enableOrNameAndArgs ?? 'help [command]';
const [, helpName, helpArgs] = enableOrNameAndArgs.match(/([^ ]+) *(.*)/);
const nameAndArgs = enableOrNameAndArgs ?? 'help [command]';
const [, helpName, helpArgs] = nameAndArgs.match(/([^ ]+) *(.*)/);
const helpDescription = description ?? 'display help for command';

const helpCommand = this.createCommand(helpName);
Expand All @@ -414,6 +424,8 @@ class Command extends EventEmitter {

this._addImplicitHelpCommand = true;
this._helpCommand = helpCommand;
// init group unless lazy create
if (enableOrNameAndArgs || description) this._initCommandGroup(helpCommand);

return this;
}
Expand All @@ -435,6 +447,7 @@ class Command extends EventEmitter {

this._addImplicitHelpCommand = true;
this._helpCommand = helpCommand;
this._initCommandGroup(helpCommand);
return this;
}

Expand Down Expand Up @@ -613,6 +626,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
- already used by option '${matchingOption.flags}'`);
}

this._initOptionGroup(option);
this.options.push(option);
}

Expand Down Expand Up @@ -640,6 +654,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
);
}

this._initCommandGroup(command);
this.commands.push(command);
}

Expand Down Expand Up @@ -2294,6 +2309,75 @@ Expecting one of '${allowedValues.join("', '")}'`);
return this;
}

/**
* Set/get the help group heading for this subcommand in parent command's help.
*
* @param {string} [heading]
* @return {Command | string}
*/

helpGroup(heading) {
if (heading === undefined) return this._helpGroupHeading ?? '';
this._helpGroupHeading = heading;
return this;
}

/**
* Set/get the default help group heading for subcommands added to this command.
* (This does not override a group set directly on the subcommand using .helpGroup().)
*
* @example
* program.commandsGroup('Development Commands:);
* program.command('watch')...
* program.command('lint')...
* ...
*
* @param {string} [heading]
* @returns {Command | string}
*/
commandsGroup(heading) {
if (heading === undefined) return this._defaultCommandGroup ?? '';
this._defaultCommandGroup = heading;
return this;
}

/**
* Set/get the default help group heading for options added to this command.
* (This does not override a group set directly on the option using .helpGroup().)
*
* @example
* program
* .optionsGroup('Development Options:')
* .option('-d, --debug', 'output extra debugging')
* .option('-p, --profile', 'output profiling information')
*
* @param {string} [heading]
* @returns {Command | string}
*/
optionsGroup(heading) {
if (heading === undefined) return this._defaultOptionGroup ?? '';
this._defaultOptionGroup = heading;
return this;
}

/**
* @param {Option} option
* @private
*/
_initOptionGroup(option) {
if (this._defaultOptionGroup && !option.helpGroupHeading)
option.helpGroup(this._defaultOptionGroup);
}

/**
* @param {Command} cmd
* @private
*/
_initCommandGroup(cmd) {
if (this._defaultCommandGroup && !cmd.helpGroup())
cmd.helpGroup(this._defaultCommandGroup);
}

/**
* Set the name of the command from script filename, such as process.argv[1],
* or require.main.filename, or __filename.
Expand Down Expand Up @@ -2448,22 +2532,27 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/

helpOption(flags, description) {
// Support disabling built-in help option.
// Support enabling/disabling built-in help option.
if (typeof flags === 'boolean') {
// true is not an expected value. Do something sensible but no unit-test.
// istanbul ignore if
if (flags) {
this._helpOption = this._helpOption ?? undefined; // preserve existing option
if (this._helpOption === null) this._helpOption = undefined; // reenable
if (this._defaultOptionGroup) {
// make the option to store the group
this._initOptionGroup(this._getHelpOption());
}
} else {
this._helpOption = null; // disable
}
return this;
}

// Customise flags and description.
flags = flags ?? '-h, --help';
description = description ?? 'display help for command';
this._helpOption = this.createOption(flags, description);
this._helpOption = this.createOption(
flags ?? '-h, --help',
description ?? 'display help for command',
);
// init group unless lazy create
if (flags || description) this._initOptionGroup(this._helpOption);

return this;
}
Expand Down Expand Up @@ -2492,6 +2581,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/
addHelpOption(option) {
this._helpOption = option;
this._initOptionGroup(option);
return this;
}

Expand Down
Loading