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

Refactor help command implementation to hold actual Command #2087

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
48f9fd9
Bump @types/jest from 29.5.6 to 29.5.7 (#2064)
dependabot[bot] Nov 11, 2023
902124d
Bump eslint from 8.52.0 to 8.53.0 (#2061)
dependabot[bot] Nov 11, 2023
c20a71d
Bump @types/node from 20.8.9 to 20.8.10 (#2063)
dependabot[bot] Nov 11, 2023
6b74d26
Bump @typescript-eslint/eslint-plugin from 6.9.0 to 6.9.1 (#2060)
dependabot[bot] Nov 11, 2023
2978456
Bump @typescript-eslint/parser from 6.9.0 to 6.9.1 (#2062)
dependabot[bot] Nov 11, 2023
883d326
Bump @types/jest from 29.5.7 to 29.5.8 (#2076)
dependabot[bot] Nov 14, 2023
f234c25
Bump @types/node from 20.8.10 to 20.9.0 (#2074)
dependabot[bot] Nov 14, 2023
c1b2526
Bump @typescript-eslint/eslint-plugin from 6.9.1 to 6.10.0 (#2072)
dependabot[bot] Nov 14, 2023
47a48c9
Bump @typescript-eslint/parser from 6.9.1 to 6.10.0 (#2073)
dependabot[bot] Nov 14, 2023
8eed8c2
Bump eslint-plugin-n from 16.2.0 to 16.3.1 (#2075)
dependabot[bot] Nov 14, 2023
7685a2b
Add .helpCommand and store custom command
shadowspawn Nov 26, 2023
12fe03b
Add explicit getter for implict help command
shadowspawn Nov 26, 2023
1a022b6
Add tests and update docs
shadowspawn Dec 29, 2023
2b02ed4
Lint and comments
shadowspawn Dec 29, 2023
18e36c7
Keep it simple and .helpCommand() always returns this
shadowspawn Dec 31, 2023
eb7e72a
Add typing tests and tweaks
shadowspawn Jan 6, 2024
ffd5a44
Remove redundant !!
shadowspawn Jan 11, 2024
da5e03e
Merge remote-tracking branch 'upstream/release/12.x' into feature/hel…
shadowspawn Jan 11, 2024
30c60e9
Make old and new code match
shadowspawn Jan 11, 2024
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
10 changes: 6 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [.usage](#usage)
- [.description and .summary](#description-and-summary)
- [.helpOption(flags, description)](#helpoptionflags-description)
- [.addHelpCommand()](#addhelpcommand)
- [.helpCommand()](#helpcommand)
- [More configuration](#more-configuration-2)
- [Custom event listeners](#custom-event-listeners)
- [Bits and pieces](#bits-and-pieces)
Expand Down Expand Up @@ -904,16 +904,18 @@ program
.helpOption('-e, --HELP', 'read more information');
```

### .addHelpCommand()
### .helpCommand()

A help command is added by default if your command has subcommands. You can explicitly turn on or off the implicit help command with `.addHelpCommand()` and `.addHelpCommand(false)`.
A help command is added by default if your command has subcommands. You can explicitly turn on or off the implicit help command with `.helpCommand(true)` and `.helpCommand(false)`.

You can both turn on and customise the help command by supplying the name and description:

```js
program.addHelpCommand('assist [command]', 'show assistance');
program.helpCommand('assist [command]', 'show assistance');
```

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

### More configuration

The built-in help is formatted using the Help class.
Expand Down
22 changes: 22 additions & 0 deletions docs/deprecated.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ They are currently still available for backwards compatibility, but should not b
- [Short option flag longer than a single character](#short-option-flag-longer-than-a-single-character)
- [Import from `commander/esm.mjs`](#import-from-commanderesmmjs)
- [cmd.\_args](#cmd_args)
- [.addHelpCommand(string|boolean|undefined)](#addhelpcommandstringbooleanundefined)
- [Removed](#removed)
- [Default import of global Command object](#default-import-of-global-command-object)

Expand Down Expand Up @@ -205,6 +206,27 @@ const registeredArguments = program.registeredArguments;

Deprecated from Commander v11.

### .addHelpCommand(string|boolean|undefined)

This was originally used with a variety of parameters, but not by passing a Command object despite the "add" name.

```js
program.addHelpCommand('assist [command]');
program.addHelpCommand('assist', 'show assistance');
program.addHelpCommand(false);

```

In new code you configure the help command with `.helpCommand()`. Or use `.addHelpCommand()` which now takes a Command object, like `.addCommand()`.

```js
program.helpCommand('assist [command]');
program.helpCommand('assist', 'show assistance');
program.helpCommand(false);

program.addHelpCommand(new Command('assist').argument('[command]').description('show assistance'));

```
## Removed

### Default import of global Command object
Expand Down
92 changes: 63 additions & 29 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ class Command extends EventEmitter {
this._helpDescription = 'display help for command';
this._helpShortFlag = '-h';
this._helpLongFlag = '--help';
this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false
this._helpCommandName = 'help';
this._helpCommandnameAndArgs = 'help [command]';
this._helpCommandDescription = 'display help for command';
this._addImplicitHelpCommand = undefined; // undecided whether true or false yet, not inherited
/** @type {Command} */
this._helpCommand = undefined; // lazy initialised, inherited
this._helpConfiguration = {};
}

Expand All @@ -93,9 +92,7 @@ class Command extends EventEmitter {
this._helpDescription = sourceCommand._helpDescription;
this._helpShortFlag = sourceCommand._helpShortFlag;
this._helpLongFlag = sourceCommand._helpLongFlag;
this._helpCommandName = sourceCommand._helpCommandName;
this._helpCommandnameAndArgs = sourceCommand._helpCommandnameAndArgs;
this._helpCommandDescription = sourceCommand._helpCommandDescription;
this._helpCommand = sourceCommand._helpCommand;
this._helpConfiguration = sourceCommand._helpConfiguration;
this._exitCallback = sourceCommand._exitCallback;
this._storeOptionsAsProperties = sourceCommand._storeOptionsAsProperties;
Expand Down Expand Up @@ -369,39 +366,76 @@ class Command extends EventEmitter {
}

/**
* Override default decision whether to add implicit help command.
* Customise or override default help command. By default a help command is automatically added if your command has subcommands.
*
* addHelpCommand() // force on
* addHelpCommand(false); // force off
* addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom details
* program.helpCommand('help [cmd]');
* program.helpCommand('help [cmd]', 'show help');
* program.helpCommand(false); // suppress default help command
* program.helpCommand(true); // add help command even if no subcommands
*
* @param {string|boolean} enableOrNameAndArgs - enable with custom name and/or arguments, or boolean to override whether added
* @param {string} [description] - custom description
* @return {Command} `this` command for chaining
*/

addHelpCommand(enableOrNameAndArgs, description) {
if (enableOrNameAndArgs === false) {
this._addImplicitHelpCommand = false;
} else {
this._addImplicitHelpCommand = true;
if (typeof enableOrNameAndArgs === 'string') {
this._helpCommandName = enableOrNameAndArgs.split(' ')[0];
this._helpCommandnameAndArgs = enableOrNameAndArgs;
}
this._helpCommandDescription = description || this._helpCommandDescription;
helpCommand(enableOrNameAndArgs, description) {
if (typeof enableOrNameAndArgs === 'boolean') {
this._addImplicitHelpCommand = enableOrNameAndArgs;
return this;
}

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

const helpCommand = this.createCommand(helpName);
helpCommand.helpOption(false);
if (helpArgs) helpCommand.arguments(helpArgs);
if (helpDescription) helpCommand.description(helpDescription);

this._addImplicitHelpCommand = true;
this._helpCommand = helpCommand;

return this;
}

/**
* @return {boolean}
* @package internal use only
* Add prepared custom help command.
*
* @param {(Command|string|boolean)} helpCommand - custom help command, or deprecated enableOrNameAndArgs as for `.helpCommand()`
* @param {string} [deprecatedDescription] - deprecated custom description used with custom name only
* @return {Command} `this` command for chaining
*/
addHelpCommand(helpCommand, deprecatedDescription) {
// If not passed an object, call through to helpCommand for backwards compatibility,
// as addHelpCommand was originally used like helpCommand is now.
if (typeof helpCommand !== 'object') {
this.helpCommand(helpCommand, deprecatedDescription);
return this;
}

this._addImplicitHelpCommand = true;
this._helpCommand = helpCommand;
return this;
}

_hasImplicitHelpCommand() {
if (this._addImplicitHelpCommand === undefined) {
return this.commands.length && !this._actionHandler && !this._findCommand('help');
/**
* Lazy create help command.
*
* @return {(Command|null)}
* @package
*/
_getHelpCommand() {
const hasImplicitHelpCommand = this._addImplicitHelpCommand ??
(this.commands.length && !this._actionHandler && !this._findCommand('help'));

if (hasImplicitHelpCommand) {
if (this._helpCommand === undefined) {
this.helpCommand(undefined, undefined); // use default name and description
}
return this._helpCommand;
}
return this._addImplicitHelpCommand;
return null;
}

/**
Expand Down Expand Up @@ -1315,7 +1349,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
if (operands && this._findCommand(operands[0])) {
return this._dispatchSubcommand(operands[0], operands.slice(1), unknown);
}
if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) {
if (this._getHelpCommand() && operands[0] === this._getHelpCommand().name()) {
return this._dispatchHelpCommand(operands[1]);
}
if (this._defaultCommandName) {
Expand Down Expand Up @@ -1572,7 +1606,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
operands.push(arg);
if (args.length > 0) unknown.push(...args);
break;
} else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) {
} else if (this._getHelpCommand() && arg === this._getHelpCommand().name()) {
operands.push(arg);
if (args.length > 0) operands.push(...args);
break;
Expand Down
9 changes: 2 additions & 7 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,8 @@ class Help {

visibleCommands(cmd) {
const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden);
if (cmd._hasImplicitHelpCommand()) {
// Create a command matching the implicit help command.
const [, helpName, helpArgs] = cmd._helpCommandnameAndArgs.match(/([^ ]+) *(.*)/);
const helpCommand = cmd.createCommand(helpName)
.helpOption(false);
helpCommand.description(cmd._helpCommandDescription);
if (helpArgs) helpCommand.arguments(helpArgs);
const helpCommand = cmd._getHelpCommand();
if (helpCommand && !helpCommand._hidden) {
visibleCommands.push(helpCommand);
}
if (this.sortSubcommands) {
Expand Down