diff --git a/lib/command.js b/lib/command.js index 9a3d03e7d..0ebeddb8e 100644 --- a/lib/command.js +++ b/lib/command.js @@ -1128,14 +1128,18 @@ Expecting one of '${allowedValues.join("', '")}'`); this.getOptionValue(option.attributeName()) === undefined, ) .forEach((option) => { - // check for lone negated option: --no-foo without a --foo option - const positiveLongFlag = option.long.replace(/^--no-/, '--'); - if (!this._findOption(positiveLongFlag)) { - this.setOptionValueWithSource( - option.attributeName(), - true, - 'default', - ); + // Check for lone negated option: a --no-foo without any positive + // option sharing its attribute. Matching by attribute (like + // DualOptions) rather than by reconstructing the --foo flag string + // also recognises a differently-spelled positive such as -f or + // --fooBar as the pair, so it is not wrongly treated as lone. + const attributeName = option.attributeName(); + const hasPositiveOption = this.options.some( + (candidate) => + !candidate.negate && candidate.attributeName() === attributeName, + ); + if (!hasPositiveOption) { + this.setOptionValueWithSource(attributeName, true, 'default'); } }); diff --git a/tests/options.bool.test.js b/tests/options.bool.test.js index 0b24f2293..aa80e87d4 100644 --- a/tests/options.bool.test.js +++ b/tests/options.bool.test.js @@ -120,6 +120,29 @@ describe('boolean option with non-boolean default', () => { }); }); +describe('negated option paired with a differently-spelled positive', () => { + test('when short-only positive and negated then no implicit default', () => { + const program = new commander.Command(); + program.option('-c', 'add cheese').option('--no-c', 'remove cheese'); + program.parse([], { from: 'user' }); + assert.equal(program.opts().c, undefined); + }); + + test('when camelCase positive and kebab-case negated then no implicit default', () => { + const program = new commander.Command(); + program.option('--fooBar').option('--no-foo-bar'); + program.parse([], { from: 'user' }); + assert.equal(program.opts().fooBar, undefined); + }); + + test('when only negated (lone) then implicit default is still true', () => { + const program = new commander.Command(); + program.option('--no-sauce'); + program.parse([], { from: 'user' }); + assert.equal(program.opts().sauce, true); + }); +}); + // Regression test for #1301 with `-no-` in middle of option describe('regression test for -no- in middle of option name', () => { test('when option not specified then value is undefined', () => {