Skip to content

Commit 63eed4a

Browse files
authored
Make order not matter for adding combo positive/negative options (#2405)
1 parent 2e96cd3 commit 63eed4a

File tree

4 files changed

+65
-16
lines changed

4 files changed

+65
-16
lines changed

Readme.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,7 @@ cheese: stilton
277277
### Other option types, negatable boolean and boolean|value
278278

279279
You can define a boolean option long name with a leading `no-` to set the option value to false when used.
280-
Defined alone this also makes the option true by default.
281-
282-
If you define `--foo` first, adding `--no-foo` does not change the default value from what it would
283-
otherwise be.
280+
Defined alone without a matching positive option, this also makes the option true by default.
284281

285282
Example file: [options-negatable.js](./examples/options-negatable.js)
286283

examples/options-negatable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ console.log(`You ordered a pizza with ${sauceStr} and ${cheeseStr}`);
2222

2323
// Try the following:
2424
// node options-negatable.js
25-
// node options-negatable.js --sauce
25+
// node options-negatable.js --no-sauce
2626
// node options-negatable.js --cheese=blue
2727
// node options-negatable.js --no-sauce --no-cheese

lib/command.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -675,17 +675,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
675675
const name = option.attributeName();
676676

677677
// store default value
678-
if (option.negate) {
679-
// --no-foo is special and defaults foo to true, unless a --foo option is already defined
680-
const positiveLongFlag = option.long.replace(/^--no-/, '--');
681-
if (!this._findOption(positiveLongFlag)) {
682-
this.setOptionValueWithSource(
683-
name,
684-
option.defaultValue === undefined ? true : option.defaultValue,
685-
'default',
686-
);
687-
}
688-
} else if (option.defaultValue !== undefined) {
678+
if (option.defaultValue !== undefined) {
689679
this.setOptionValueWithSource(name, option.defaultValue, 'default');
690680
}
691681

@@ -1126,7 +1116,29 @@ Expecting one of '${allowedValues.join("', '")}'`);
11261116
}
11271117

11281118
_prepareForParse() {
1119+
// Save the state the first time, then restore the state before each subsequent parse.
11291120
if (this._savedState === null) {
1121+
// Do the special default of lone negated option to true, now that we have all the options.
1122+
// Filter for negated options that have not been processed already.
1123+
this.options
1124+
.filter(
1125+
(option) =>
1126+
option.negate &&
1127+
option.defaultValue === undefined &&
1128+
this.getOptionValue(option.attributeName()) === undefined,
1129+
)
1130+
.forEach((option) => {
1131+
// check for lone negated option: --no-foo without a --foo option
1132+
const positiveLongFlag = option.long.replace(/^--no-/, '--');
1133+
if (!this._findOption(positiveLongFlag)) {
1134+
this.setOptionValueWithSource(
1135+
option.attributeName(),
1136+
true,
1137+
'default',
1138+
);
1139+
}
1140+
});
1141+
11301142
this.saveStateBeforeParse();
11311143
} else {
11321144
this.restoreStateBeforeParse();

tests/options.bool.combo.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,43 @@ describe('boolean option combo with non-boolean default and preset', () => {
149149
expect(program.opts().pepper).toBe(false);
150150
});
151151
});
152+
153+
describe('only lone negative causes implicit default of true', () => {
154+
test('when lone negative then default value is true', () => {
155+
const program = new commander.Command();
156+
program.option('--no-pepper', 'remove pepper');
157+
program.parse([], { from: 'user' });
158+
expect(program.opts().pepper).toBe(true);
159+
});
160+
161+
test('when boolean combo and negative first then no implicit default', () => {
162+
// New behaviour in Commander 15, now only lone negative gets implicit default of true.
163+
const program = new commander.Command();
164+
program
165+
.option('--no-pepper', 'remove pepper')
166+
.option('--pepper', 'pepper only');
167+
program.parse([], { from: 'user' });
168+
expect(program.opts().pepper).toBeUndefined();
169+
});
170+
171+
test('when boolean combo and negative second then no implicit default', () => {
172+
const program = new commander.Command();
173+
program
174+
.option('--pepper', 'pepper only')
175+
.option('--no-pepper', 'remove pepper');
176+
program.parse([], { from: 'user' });
177+
expect(program.opts().pepper).toBeUndefined();
178+
});
179+
180+
test('when boolean combo and negative second with explicit default then get explicit default', () => {
181+
// Prior to Commander 15, negative second would ignore implicit and explicit default value.
182+
// Simpler rule now and the special default behaviour is for lone negative only. Add test since
183+
// intentional change of behaviour for unlikely edge case.
184+
const program = new commander.Command();
185+
program
186+
.option('--pepper', 'pepper only')
187+
.option('--no-pepper', 'remove pepper', 'plain');
188+
program.parse([], { from: 'user' });
189+
expect(program.opts().pepper).toBe('plain');
190+
});
191+
});

0 commit comments

Comments
 (0)