Skip to content

Commit 0c957ea

Browse files
committed
util: support --no- for argument with boolean type for parseArgs
1 parent 1b96527 commit 0c957ea

File tree

3 files changed

+107
-9
lines changed

3 files changed

+107
-9
lines changed

doc/api/util.md

+6
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,8 @@ changes:
14291429
* `allowPositionals` {boolean} Whether this command accepts positional
14301430
arguments.
14311431
**Default:** `false` if `strict` is `true`, otherwise `true`.
1432+
* `allowNegative` {boolean} Whether allow negative options.
1433+
**Default:** `false`.
14321434
* `tokens` {boolean} Return the parsed tokens. This is useful for extending
14331435
the built-in behavior, from adding additional checks through to reprocessing
14341436
the tokens in different ways.
@@ -1445,6 +1447,10 @@ Provides a higher level API for command-line argument parsing than interacting
14451447
with `process.argv` directly. Takes a specification for the expected arguments
14461448
and returns a structured object with the parsed options and positionals.
14471449

1450+
for an argument with type `boolean`, If the argument is passed with
1451+
the `--no-` prefix, the value of the argument will be set to the opposite
1452+
of its default value.
1453+
14481454
```mjs
14491455
import { parseArgs } from 'node:util';
14501456
const args = ['-f', '--bar', 'b'];

lib/internal/util/parse_args/parse_args.js

+31-9
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,24 @@ To specify an option argument starting with a dash use ${example}.`;
9494
* @param {object} token - from tokens as available from parseArgs
9595
*/
9696
function checkOptionUsage(config, token) {
97-
if (!ObjectHasOwn(config.options, token.name)) {
98-
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
99-
token.rawName, config.allowPositionals);
97+
let tokenName = token.name;
98+
if (!ObjectHasOwn(config.options, tokenName)) {
99+
// Check for negated boolean option.
100+
if (config.allowNegative && StringPrototypeStartsWith(tokenName, 'no-')) {
101+
tokenName = StringPrototypeSlice(tokenName, 3);
102+
if (!ObjectHasOwn(config.options, tokenName) || optionsGetOwn(config.options, tokenName, 'type') !== 'boolean') {
103+
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
104+
token.rawName, config.allowPositionals);
105+
}
106+
} else {
107+
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
108+
token.rawName, config.allowPositionals);
109+
}
100110
}
101111

102-
const short = optionsGetOwn(config.options, token.name, 'short');
103-
const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
104-
const type = optionsGetOwn(config.options, token.name, 'type');
112+
const short = optionsGetOwn(config.options, tokenName, 'short');
113+
const shortAndLong = `${short ? `-${short}, ` : ''}--${tokenName}`;
114+
const type = optionsGetOwn(config.options, tokenName, 'type');
105115
if (type === 'string' && typeof token.value !== 'string') {
106116
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
107117
}
@@ -118,12 +128,22 @@ function checkOptionUsage(config, token) {
118128
* @param {string|undefined} optionValue - value from user args
119129
* @param {object} options - option configs, from parseArgs({ options })
120130
* @param {object} values - option values returned in `values` by parseArgs
131+
* @param {boolean} allowNegative - allow negative optinons if true
121132
*/
122-
function storeOption(longOption, optionValue, options, values) {
133+
function storeOption(longOption, optionValue, options, values, allowNegative) {
123134
if (longOption === '__proto__') {
124135
return; // No. Just no.
125136
}
126137

138+
if (allowNegative && StringPrototypeStartsWith(longOption, 'no-')) {
139+
// Boolean option negation: --no-foo
140+
const longOptionWithoutPrefixNo = StringPrototypeSlice(longOption, 3);
141+
if (optionsGetOwn(options, longOptionWithoutPrefixNo, 'type') !== 'string') {
142+
longOption = StringPrototypeSlice(longOption, 3);
143+
optionValue = false;
144+
}
145+
}
146+
127147
// We store based on the option value rather than option type,
128148
// preserving the users intent for author to deal with.
129149
const newValue = optionValue ?? true;
@@ -290,15 +310,17 @@ const parseArgs = (config = kEmptyObject) => {
290310
const strict = objectGetOwn(config, 'strict') ?? true;
291311
const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
292312
const returnTokens = objectGetOwn(config, 'tokens') ?? false;
313+
const allowNegative = objectGetOwn(config, 'allowNegative') ?? false;
293314
const options = objectGetOwn(config, 'options') ?? { __proto__: null };
294315
// Bundle these up for passing to strict-mode checks.
295-
const parseConfig = { args, strict, options, allowPositionals };
316+
const parseConfig = { args, strict, options, allowPositionals, allowNegative };
296317

297318
// Validate input configuration.
298319
validateArray(args, 'args');
299320
validateBoolean(strict, 'strict');
300321
validateBoolean(allowPositionals, 'allowPositionals');
301322
validateBoolean(returnTokens, 'tokens');
323+
validateBoolean(allowNegative, 'allowNegative');
302324
validateObject(options, 'options');
303325
ArrayPrototypeForEach(
304326
ObjectEntries(options),
@@ -360,7 +382,7 @@ const parseArgs = (config = kEmptyObject) => {
360382
checkOptionUsage(parseConfig, token);
361383
checkOptionLikeValue(token);
362384
}
363-
storeOption(token.name, token.value, options, result.values);
385+
storeOption(token.name, token.value, options, result.values, parseConfig.allowNegative);
364386
} else if (token.kind === 'positional') {
365387
if (!allowPositionals) {
366388
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);

test/parallel/test-parse-args.mjs

+70
Original file line numberDiff line numberDiff line change
@@ -992,3 +992,73 @@ test('multiple as false should expect a String', () => {
992992
}, /"options\.alpha\.default" property must be of type string/
993993
);
994994
});
995+
996+
// Test negative options
997+
test('disable negative options and args are started with "--no-" prefix', () => {
998+
const args = ['--no-alpha'];
999+
const options = { alpha: { type: 'boolean' } };
1000+
assert.throws(() => {
1001+
parseArgs({ args, options });
1002+
}, {
1003+
code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
1004+
});
1005+
});
1006+
1007+
test('args are passed `type: "string"` and allow negative options', () => {
1008+
const args = ['--no-alpha', 'value'];
1009+
const options = { alpha: { type: 'string' } };
1010+
assert.throws(() => {
1011+
parseArgs({ args, options, allowNegative: true });
1012+
}, {
1013+
code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
1014+
});
1015+
});
1016+
1017+
test('args are passed `type: "boolean"` and allow negative options', () => {
1018+
const args = ['--no-alpha'];
1019+
const options = { alpha: { type: 'boolean' } };
1020+
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
1021+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
1022+
});
1023+
1024+
test('args are passed `default: "true"` and allow negative options', () => {
1025+
const args = ['--no-alpha'];
1026+
const options = { alpha: { type: 'boolean', default: true } };
1027+
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
1028+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
1029+
});
1030+
1031+
test('args are passed `default: "false" and allow negative options', () => {
1032+
const args = ['--no-alpha'];
1033+
const options = { alpha: { type: 'boolean', default: false } };
1034+
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
1035+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
1036+
});
1037+
1038+
test('allow negative options and multiple as true', () => {
1039+
const args = ['--no-alpha', '--alpha', '--no-alpha'];
1040+
const options = { alpha: { type: 'boolean', multiple: true } };
1041+
const expected = { values: { __proto__: null, alpha: [false, true, false] }, positionals: [] };
1042+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
1043+
});
1044+
1045+
test('allow negative options and passed multiple arguments', () => {
1046+
const args = ['--alpha', '--no-alpha', '--alpha'];
1047+
const options = { alpha: { type: 'boolean' } };
1048+
const expected = { values: { __proto__: null, alpha: true }, positionals: [] };
1049+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
1050+
});
1051+
1052+
test('allow negative options when normal arguments', () => {
1053+
const holdArgv = process.argv;
1054+
process.argv = [process.argv0, 'script.js', '--no-foo'];
1055+
const holdExecArgv = process.execArgv;
1056+
process.execArgv = [];
1057+
const result = parseArgs({ strict: false, allowNegative: true });
1058+
1059+
const expected = { values: { __proto__: null, foo: false },
1060+
positionals: [] };
1061+
assert.deepStrictEqual(result, expected);
1062+
process.argv = holdArgv;
1063+
process.execArgv = holdExecArgv;
1064+
});

0 commit comments

Comments
 (0)