Skip to content

Commit 2b5871d

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

File tree

3 files changed

+89
-6
lines changed

3 files changed

+89
-6
lines changed

doc/api/util.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,10 @@ Provides a higher level API for command-line argument parsing than interacting
14451445
with `process.argv` directly. Takes a specification for the expected arguments
14461446
and returns a structured object with the parsed options and positionals.
14471447

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

lib/internal/util/parse_args/parse_args.js

Lines changed: 25 additions & 6 deletions
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+
if (StringPrototypeStartsWith(tokenName, 'no-')) {
100+
tokenName = StringPrototypeSlice(tokenName, 3);
101+
if (!ObjectHasOwn(config.options, tokenName) ||
102+
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
}
@@ -124,6 +134,15 @@ function storeOption(longOption, optionValue, options, values) {
124134
return; // No. Just no.
125135
}
126136

137+
if (StringPrototypeStartsWith(longOption, 'no-') && !ObjectHasOwn(options, longOption)) {
138+
// Boolean option negation: --no-foo
139+
const longOptionWithoutPrefixNo = StringPrototypeSlice(longOption, 3);
140+
if (optionsGetOwn(options, longOptionWithoutPrefixNo, 'type') !== 'string') {
141+
longOption = longOptionWithoutPrefixNo;
142+
optionValue = optionsGetOwn(options, longOption, 'default') === false;
143+
}
144+
}
145+
127146
// We store based on the option value rather than option type,
128147
// preserving the users intent for author to deal with.
129148
const newValue = optionValue ?? true;

test/parallel/test-parse-args.mjs

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

0 commit comments

Comments
 (0)