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

Warn about obscured help option #1931

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1bb30d7
Support custom flags from Option subclass in helpOption()
aweebit Aug 2, 2023
e8bea4a
Fix version() parameter type
aweebit Aug 3, 2023
e4d00db
Change initial variable values in test for better error messages
aweebit Aug 2, 2023
d03566b
Warn about obscured help option
aweebit Aug 3, 2023
9857b1e
Store help option instance instead of just flags
aweebit Aug 3, 2023
371751e
Merge branch 'feature/createOption-in-helpOption' into feature/obscur…
aweebit Aug 3, 2023
eacaee8
Warn about help option obscured by version option
aweebit Aug 3, 2023
da1e153
Add missing _hasHelpOption check in copyInheritedSettings test
aweebit Aug 3, 2023
0e5a621
Merge branch 'feature/createOption-in-helpOption' into feature/obscur…
aweebit Aug 3, 2023
35e9571
Do not use undefined long help option flag in legacy code
aweebit Aug 3, 2023
5128d5f
Merge branch 'feature/fixes' into feature/createOption-in-helpOption
aweebit Aug 3, 2023
d03ef92
Merge branch 'feature/createOption-in-helpOption' into feature/obscur…
aweebit Aug 3, 2023
97f3069
Check for undefined in Option.is()
aweebit Aug 4, 2023
b6ef1e1
Merge branch 'feature/createOption-in-helpOption' into feature/obscur…
aweebit Aug 4, 2023
7335a9c
Add missing obscured help flag tests
aweebit Aug 4, 2023
d0d2c5e
Remove NODE_ENV check
aweebit Aug 11, 2023
daa3fed
Merge branch 'feature/createOption-in-helpOption' into feature/obscur…
aweebit Aug 12, 2023
a253ec6
Revert "Fix version() parameter type"
aweebit Aug 12, 2023
8dd417f
Fix help for commands with executable handler & only a short help flag
aweebit Aug 13, 2023
513a4b5
Merge branch 'feature/fixes' into feature/createOption-in-helpOption
aweebit Aug 13, 2023
aa6e9b3
Merge branch 'feature/createOption-in-helpOption' into feature/obscur…
aweebit Aug 13, 2023
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
104 changes: 74 additions & 30 deletions lib/command.js
Expand Up @@ -7,7 +7,7 @@ const process = require('process');
const { Argument, humanReadableArgName } = require('./argument.js');
const { CommanderError } = require('./error.js');
const { Help } = require('./help.js');
const { Option, splitOptionFlags, DualOptions } = require('./option.js');
const { Option, DualOptions } = require('./option.js');
const { suggestSimilar } = require('./suggestSimilar');

// @ts-check
Expand Down Expand Up @@ -67,16 +67,13 @@ class Command extends EventEmitter {
};

this._hidden = false;
this._hasHelpOption = true;
this._helpFlags = '-h, --help';
this._helpDescription = 'display help for command';
this._helpShortFlag = '-h';
this._helpLongFlag = '--help';
this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false

this._helpConfiguration = {};
this._helpCommandName = 'help';
this._helpCommandnameAndArgs = 'help [command]';
this._helpCommandDescription = 'display help for command';
this._helpConfiguration = {};
this.helpOption('-h, --help', 'display help for command');
}

/**
Expand All @@ -89,15 +86,6 @@ class Command extends EventEmitter {
*/
copyInheritedSettings(sourceCommand) {
this._outputConfiguration = sourceCommand._outputConfiguration;
this._hasHelpOption = sourceCommand._hasHelpOption;
this._helpFlags = sourceCommand._helpFlags;
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._helpConfiguration = sourceCommand._helpConfiguration;
this._exitCallback = sourceCommand._exitCallback;
this._storeOptionsAsProperties = sourceCommand._storeOptionsAsProperties;
this._combineFlagAndOptionalValue = sourceCommand._combineFlagAndOptionalValue;
Expand All @@ -106,6 +94,16 @@ class Command extends EventEmitter {
this._showHelpAfterError = sourceCommand._showHelpAfterError;
this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError;

this._helpConfiguration = sourceCommand._helpConfiguration;
this._helpCommandName = sourceCommand._helpCommandName;
this._helpCommandnameAndArgs = sourceCommand._helpCommandnameAndArgs;
this._helpCommandDescription = sourceCommand._helpCommandDescription;
if (sourceCommand._hasHelpOption) {
this.helpOption(sourceCommand._helpFlags, sourceCommand._helpDescription);
} else {
this._hasHelpOption = false;
}

return this;
}

Expand Down Expand Up @@ -500,13 +498,33 @@ Expecting one of '${allowedValues.join("', '")}'`);
return new Option(flags, description);
}

/**
* Check for option flag conflicts.
* Register option if no conflicts found.
* Throw otherwise.
*
* @param {Option} option
* @api private
*/

_registerOption(option) {
this.options.push(option);
this._checkForObscuredHelpOption((matchingOptionFlags) => (
`Help option '${this._helpOption.flags}' is obscured after adding option '${option.flags}'${matchingOptionFlags.length > 1
? `
- conflicts with options '${matchingOptionFlags.join("' and '")}'`
: ''}`));
}

/**
* Add an option.
*
* @param {Option} option
* @return {Command} `this` command for chaining
*/
addOption(option) {
this._registerOption(option);

const oname = option.name();
const name = option.attributeName();

Expand All @@ -521,9 +539,6 @@ Expecting one of '${allowedValues.join("', '")}'`);
this.setOptionValueWithSource(name, option.defaultValue, 'default');
}

// register the option
this.options.push(option);

// handler for cli and env supplied values
const handleOptionValue = (val, invalidValueMessage, valueSource) => {
// val is null for optional option used without an optional-argument.
Expand Down Expand Up @@ -1101,7 +1116,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
}

// Fallback to parsing the help flag to invoke the help.
return this._dispatchSubcommand(subcommandName, [], [this._helpLongFlag]);
return this._dispatchSubcommand(subcommandName, [], [
this._helpOption.long || this._helpOption.short
]);
}

/**
Expand Down Expand Up @@ -1820,7 +1837,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
description = description || 'output the version number';
const versionOption = this.createOption(flags, description);
this._versionOptionName = versionOption.attributeName();
this.options.push(versionOption);
this._registerOption(versionOption);
this.on('option:' + versionOption.name(), () => {
this._outputConfiguration.writeOut(`${str}\n`);
this._exit(0, 'commander.version', str);
Expand Down Expand Up @@ -2034,7 +2051,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
}
context.write(helpInformation);

this.emit(this._helpLongFlag); // deprecated
if (this._helpOption.long) {
this.emit(this._helpOption.long); // deprecated
}
this.emit('afterHelp', context);
getCommandAndParents(this).forEach(command => command.emit('afterAllHelp', context));
}
Expand All @@ -2050,20 +2069,45 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/

helpOption(flags, description) {
if (typeof flags === 'boolean') {
this._hasHelpOption = flags;
if (flags !== undefined && typeof flags !== 'string') {
this._hasHelpOption = !!flags;
return this;
}
this._helpFlags = flags || this._helpFlags;
this._helpDescription = description || this._helpDescription;
this._hasHelpOption = true;
this._helpFlags = flags = flags || this._helpFlags;
this._helpDescription = description = description || this._helpDescription;

const helpFlags = splitOptionFlags(this._helpFlags);
this._helpShortFlag = helpFlags.shortFlag;
this._helpLongFlag = helpFlags.longFlag;
this._helpOption = this.createOption(flags, description);
this._checkForObscuredHelpOption((matchingOptionFlags) => (
`Newly added help option '${this._helpOption.flags}' is obscured
- conflicts with ${matchingOptionFlags.length > 1 ? 'options' : 'option'} '${matchingOptionFlags.join("' and '")}'`));

return this;
}

/**
* @api private
*/

_checkForObscuredHelpOption(makeMessage) {
if (this._hasHelpOption) {
const shortMatchingOption = this._helpOption.short &&
this._findOption(this._helpOption.short);
if (shortMatchingOption || !this._helpOption.short) {
const longMatchingOption = this._helpOption.long &&
this._findOption(this._helpOption.long);
if (longMatchingOption || !this._helpOption.long) {
const matchingOptionFlags = (
shortMatchingOption === longMatchingOption
? [shortMatchingOption]
: [shortMatchingOption, longMatchingOption]
).filter(option => option).map(option => option.flags);
console.warn(makeMessage(matchingOptionFlags));
}
}
}
}

/**
* Output help information and exit.
*
Expand Down Expand Up @@ -2124,7 +2168,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/

function outputHelpIfRequested(cmd, args) {
const helpOption = cmd._hasHelpOption && args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag);
const helpOption = cmd._hasHelpOption && args.find(arg => arg === cmd._helpOption.long || arg === cmd._helpOption.short);
if (helpOption) {
cmd.outputHelp();
// (Do not have all displayed text available so only passing placeholder.)
Expand Down
8 changes: 4 additions & 4 deletions lib/help.js
Expand Up @@ -71,14 +71,14 @@ class Help {
visibleOptions(cmd) {
const visibleOptions = cmd.options.filter((option) => !option.hidden);
// Implicit help
const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag);
const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag);
const showShortHelpFlag = cmd._hasHelpOption && cmd._helpOption.short && !cmd._findOption(cmd._helpOption.short);
const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpOption.long);
if (showShortHelpFlag || showLongHelpFlag) {
let helpOption;
if (!showShortHelpFlag) {
helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription);
helpOption = cmd.createOption(cmd._helpOption.long, cmd._helpDescription);
} else if (!showLongHelpFlag) {
helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription);
helpOption = cmd.createOption(cmd._helpOption.short, cmd._helpDescription);
} else {
helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/option.js
Expand Up @@ -220,13 +220,13 @@ class Option {
/**
* Check if `arg` matches the short or long flag.
*
* @param {string} arg
* @param {string | undefined} arg
* @return {boolean}
* @api private
*/

is(arg) {
return this.short === arg || this.long === arg;
return arg !== undefined && (this.short === arg || this.long === arg);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions tests/command.addHelpText.test.js
Expand Up @@ -181,7 +181,7 @@ describe('context checks with full parse', () => {
});

test('when help requested then context.error is false', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -193,7 +193,7 @@ describe('context checks with full parse', () => {
});

test('when help for error then context.error is true', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -206,7 +206,7 @@ describe('context checks with full parse', () => {
});

test('when help on program then context.command is program', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -218,7 +218,7 @@ describe('context checks with full parse', () => {
});

test('when help on subcommand and "before" subcommand then context.command is subcommand', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride();
Expand All @@ -231,7 +231,7 @@ describe('context checks with full parse', () => {
});

test('when help on subcommand and "beforeAll" on program then context.command is subcommand', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand Down
5 changes: 3 additions & 2 deletions tests/command.copySettings.test.js
Expand Up @@ -44,10 +44,11 @@ describe('copyInheritedSettings property tests', () => {

source.helpOption('-Z, --zz', 'ddd');
cmd.copyInheritedSettings(source);
expect(cmd._hasHelpOption).toBeTruthy();
expect(cmd._helpFlags).toBe('-Z, --zz');
expect(cmd._helpDescription).toBe('ddd');
expect(cmd._helpShortFlag).toBe('-Z');
expect(cmd._helpLongFlag).toBe('--zz');
expect(cmd._helpOption.short).toBe('-Z');
expect(cmd._helpOption.long).toBe('--zz');
});

test('when copyInheritedSettings then copies addHelpCommand(name, description)', () => {
Expand Down