Skip to content

Commit 50733a1

Browse files
kylo5abytargos
authored andcommitted
util: support --no- for argument with boolean type for parseArgs
PR-URL: #53107 Refs: #53095 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent befabe5 commit 50733a1

3 files changed

Lines changed: 111 additions & 14 deletions

File tree

‎doc/api/util.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,9 @@ added:
13901390
- v18.3.0
13911391
- v16.17.0
13921392
changes:
1393+
- version: REPLACEME
1394+
pr-url: https://github.com/nodejs/node/pull/53107
1395+
description: add support for allowing negative options in input `config`.
13931396
- version:
13941397
- v20.0.0
13951398
pr-url: https://github.com/nodejs/node/pull/46718
@@ -1429,6 +1432,9 @@ changes:
14291432
*`allowPositionals` {boolean} Whether this command accepts positional
14301433
arguments.
14311434
**Default:**`false` if `strict` is `true`, otherwise `true`.
1435+
*`allowNegative` {boolean} If `true`, allows explicitly setting boolean
1436+
options to `false` by prefixing the option name with `--no-`.
1437+
**Default:**`false`.
14321438
*`tokens` {boolean} Return the parsed tokens. This is useful for extending
14331439
the built-in behavior, from adding additional checks through to reprocessing
14341440
the tokens in different ways.
@@ -1511,9 +1517,9 @@ that appear more than once in args produce a token for each use. Short option
15111517
groups like `-xy` expand to a token for each option. So `-xxx` produces
15121518
three tokens.
15131519

1514-
For example to use the returned tokens to add support for a negated option
1515-
like `--no-color`, the tokens can be reprocessed to change the value stored
1516-
for the negated option.
1520+
For example, to add support for a negated option like `--no-color` (which
1521+
`allowNegative` supports when the option is of `boolean` type), the returned
1522+
tokens can be reprocessed to change the value stored for the negated option.
15171523

15181524
```mjs
15191525
import { parseArgs } from'node:util';

‎lib/internal/util/parse_args/parse_args.js‎

Lines changed: 32 additions & 11 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
functioncheckOptionUsage(config,token){
97-
if(!ObjectHasOwn(config.options,token.name)){
98-
thrownewERR_PARSE_ARGS_UNKNOWN_OPTION(
99-
token.rawName,config.allowPositionals);
97+
lettokenName=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+
thrownewERR_PARSE_ARGS_UNKNOWN_OPTION(
104+
token.rawName,config.allowPositionals);
105+
}
106+
}else{
107+
thrownewERR_PARSE_ARGS_UNKNOWN_OPTION(
108+
token.rawName,config.allowPositionals);
109+
}
100110
}
101111

102-
constshort=optionsGetOwn(config.options,token.name,'short');
103-
constshortAndLong=`${short ? `-${short}, ` : ''}--${token.name}`;
104-
consttype=optionsGetOwn(config.options,token.name,'type');
112+
constshort=optionsGetOwn(config.options,tokenName,'short');
113+
constshortAndLong=`${short ? `-${short}, ` : ''}--${tokenName}`;
114+
consttype=optionsGetOwn(config.options,tokenName,'type');
105115
if(type==='string'&&typeoftoken.value!=='string'){
106116
thrownewERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
107117
}
@@ -114,16 +124,25 @@ function checkOptionUsage(config, token) {
114124

115125
/**
116126
* Store the option value in `values`.
117-
* @param {string} longOption - long option name e.g. 'foo'
118-
* @param {string|undefined} optionValue - value from user args
127+
* @param {object} token - from tokens as available from parseArgs
119128
* @param {object} options - option configs, from parseArgs({ options })
120129
* @param {object} values - option values returned in `values` by parseArgs
130+
* @param {boolean} allowNegative - allow negative optinons if true
121131
*/
122-
functionstoreOption(longOption,optionValue,options,values){
132+
functionstoreOption(token,options,values,allowNegative){
133+
letlongOption=token.name;
134+
letoptionValue=token.value;
123135
if(longOption==='__proto__'){
124136
return;// No. Just no.
125137
}
126138

139+
if(allowNegative&&StringPrototypeStartsWith(longOption,'no-')&&optionValue===undefined){
140+
// Boolean option negation: --no-foo
141+
longOption=StringPrototypeSlice(longOption,3);
142+
token.name=longOption;
143+
optionValue=false;
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
constnewValue=optionValue??true;
@@ -290,15 +309,17 @@ const parseArgs = (config = kEmptyObject) => {
290309
conststrict=objectGetOwn(config,'strict')??true;
291310
constallowPositionals=objectGetOwn(config,'allowPositionals')??!strict;
292311
constreturnTokens=objectGetOwn(config,'tokens')??false;
312+
constallowNegative=objectGetOwn(config,'allowNegative')??false;
293313
constoptions=objectGetOwn(config,'options')??{__proto__: null};
294314
// Bundle these up for passing to strict-mode checks.
295-
constparseConfig={ args, strict, options, allowPositionals };
315+
constparseConfig={ args, strict, options, allowPositionals, allowNegative};
296316

297317
// Validate input configuration.
298318
validateArray(args,'args');
299319
validateBoolean(strict,'strict');
300320
validateBoolean(allowPositionals,'allowPositionals');
301321
validateBoolean(returnTokens,'tokens');
322+
validateBoolean(allowNegative,'allowNegative');
302323
validateObject(options,'options');
303324
ArrayPrototypeForEach(
304325
ObjectEntries(options),
@@ -360,7 +381,7 @@ const parseArgs = (config = kEmptyObject) => {
360381
checkOptionUsage(parseConfig,token);
361382
checkOptionLikeValue(token);
362383
}
363-
storeOption(token.name,token.value,options,result.values);
384+
storeOption(token,options,result.values,parseConfig.allowNegative);
364385
}elseif(token.kind==='positional'){
365386
if(!allowPositionals){
366387
thrownewERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);

‎test/parallel/test-parse-args.mjs‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,3 +992,73 @@ test('multiple as false should expect a String', () => {
992992
},/"options\.alpha\.default"propertymustbeoftypestring/
993993
);
994994
});
995+
996+
// Test negative options
997+
test('disable negative options and args are started with "--no-" prefix',()=>{
998+
constargs=['--no-alpha'];
999+
constoptions={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+
constargs=['--no-alpha','value'];
1009+
constoptions={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+
constargs=['--no-alpha'];
1019+
constoptions={alpha: {type: 'boolean'}};
1020+
constexpected={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+
constargs=['--no-alpha'];
1026+
constoptions={alpha: {type: 'boolean',default: true}};
1027+
constexpected={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+
constargs=['--no-alpha'];
1033+
constoptions={alpha: {type: 'boolean',default: false}};
1034+
constexpected={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+
constargs=['--no-alpha','--alpha','--no-alpha'];
1040+
constoptions={alpha: {type: 'boolean',multiple: true}};
1041+
constexpected={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+
constargs=['--no-alpha','--alpha'];
1047+
constoptions={alpha: {type: 'boolean'}};
1048+
constexpected={values: {__proto__: null,alpha: true},positionals: []};
1049+
assert.deepStrictEqual(parseArgs({ args, options,allowNegative: true}),expected);
1050+
});
1051+
1052+
test('auto-detect --no-foo as negated when strict:false and allowNegative',()=>{
1053+
constholdArgv=process.argv;
1054+
process.argv=[process.argv0,'script.js','--no-foo'];
1055+
constholdExecArgv=process.execArgv;
1056+
process.execArgv=[];
1057+
constresult=parseArgs({strict: false,allowNegative: true});
1058+
1059+
constexpected={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)