Skip to content

Commit bb2c69c

Browse files
bakkotkylo5aby
andauthored
Add support for allowNegative ("--no-foo") (#163)
Co-authored-by: Zhenwei Jin <[email protected]>
1 parent 8c4e0b9 commit bb2c69c

File tree

3 files changed

+116
-16
lines changed

3 files changed

+116
-16
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ changes:
1616
using `tokens` in input `config` and returned properties.
1717
-->
1818

19-
> Stability: 1 - Experimental
19+
> Stability: 2 - Stable
2020
2121
* `config` {Object} Used to provide arguments for parsing and to configure
2222
the parser. `config` supports the following properties:
@@ -40,6 +40,9 @@ changes:
4040
* `allowPositionals` {boolean} Whether this command accepts positional
4141
arguments.
4242
**Default:** `false` if `strict` is `true`, otherwise `true`.
43+
* `allowNegative` {boolean} If `true`, allows explicitly setting boolean
44+
options to `false` by prefixing the option name with `--no-`.
45+
**Default:** `false`.
4346
* `tokens` {boolean} Return the parsed tokens. This is useful for extending
4447
the built-in behavior, from adding additional checks through to reprocessing
4548
the tokens in different ways.
@@ -125,9 +128,9 @@ that appear more than once in args produce a token for each use. Short option
125128
groups like `-xy` expand to a token for each option. So `-xxx` produces
126129
three tokens.
127130

128-
For example to use the returned tokens to add support for a negated option
129-
like `--no-color`, the tokens can be reprocessed to change the value stored
130-
for the negated option.
131+
For example, to add support for a negated option like `--no-color` (which
132+
`allowNegative` supports when the option is of `boolean` type), the returned
133+
tokens can be reprocessed to change the value stored for the negated option.
131134

132135
```mjs
133136
import { parseArgs } from 'node:util';

index.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
/* eslint max-len: ["error", {"code": 120}], */
4+
35
const {
46
ArrayPrototypeForEach,
57
ArrayPrototypeIncludes,
@@ -95,14 +97,24 @@ To specify an option argument starting with a dash use ${example}.`;
9597
* @param {object} token - from tokens as available from parseArgs
9698
*/
9799
function checkOptionUsage(config, token) {
98-
if (!ObjectHasOwn(config.options, token.name)) {
99-
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
100-
token.rawName, config.allowPositionals);
100+
let tokenName = token.name;
101+
if (!ObjectHasOwn(config.options, tokenName)) {
102+
// Check for negated boolean option.
103+
if (config.allowNegative && StringPrototypeStartsWith(tokenName, 'no-')) {
104+
tokenName = StringPrototypeSlice(tokenName, 3);
105+
if (!ObjectHasOwn(config.options, tokenName) || optionsGetOwn(config.options, tokenName, 'type') !== 'boolean') {
106+
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
107+
token.rawName, config.allowPositionals);
108+
}
109+
} else {
110+
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
111+
token.rawName, config.allowPositionals);
112+
}
101113
}
102114

103-
const short = optionsGetOwn(config.options, token.name, 'short');
104-
const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
105-
const type = optionsGetOwn(config.options, token.name, 'type');
115+
const short = optionsGetOwn(config.options, tokenName, 'short');
116+
const shortAndLong = `${short ? `-${short}, ` : ''}--${tokenName}`;
117+
const type = optionsGetOwn(config.options, tokenName, 'type');
106118
if (type === 'string' && typeof token.value !== 'string') {
107119
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
108120
}
@@ -115,17 +127,25 @@ function checkOptionUsage(config, token) {
115127

116128
/**
117129
* Store the option value in `values`.
118-
*
119-
* @param {string} longOption - long option name e.g. 'foo'
120-
* @param {string|undefined} optionValue - value from user args
130+
* @param {object} token - from tokens as available from parseArgs
121131
* @param {object} options - option configs, from parseArgs({ options })
122132
* @param {object} values - option values returned in `values` by parseArgs
133+
* @param {boolean} allowNegative - allow negative optinons if true
123134
*/
124-
function storeOption(longOption, optionValue, options, values) {
135+
function storeOption(token, options, values, allowNegative) {
136+
let longOption = token.name;
137+
let optionValue = token.value;
125138
if (longOption === '__proto__') {
126139
return; // No. Just no.
127140
}
128141

142+
if (allowNegative && StringPrototypeStartsWith(longOption, 'no-') && optionValue === undefined) {
143+
// Boolean option negation: --no-foo
144+
longOption = StringPrototypeSlice(longOption, 3);
145+
token.name = longOption;
146+
optionValue = false;
147+
}
148+
129149
// We store based on the option value rather than option type,
130150
// preserving the users intent for author to deal with.
131151
const newValue = optionValue ?? true;
@@ -295,15 +315,17 @@ const parseArgs = (config = kEmptyObject) => {
295315
const strict = objectGetOwn(config, 'strict') ?? true;
296316
const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
297317
const returnTokens = objectGetOwn(config, 'tokens') ?? false;
318+
const allowNegative = objectGetOwn(config, 'allowNegative') ?? false;
298319
const options = objectGetOwn(config, 'options') ?? { __proto__: null };
299320
// Bundle these up for passing to strict-mode checks.
300-
const parseConfig = { args, strict, options, allowPositionals };
321+
const parseConfig = { args, strict, options, allowPositionals, allowNegative };
301322

302323
// Validate input configuration.
303324
validateArray(args, 'args');
304325
validateBoolean(strict, 'strict');
305326
validateBoolean(allowPositionals, 'allowPositionals');
306327
validateBoolean(returnTokens, 'tokens');
328+
validateBoolean(allowNegative, 'allowNegative');
307329
validateObject(options, 'options');
308330
ArrayPrototypeForEach(
309331
ObjectEntries(options),
@@ -365,7 +387,7 @@ const parseArgs = (config = kEmptyObject) => {
365387
checkOptionUsage(parseConfig, token);
366388
checkOptionLikeValue(token);
367389
}
368-
storeOption(token.name, token.value, options, result.values);
390+
storeOption(token, options, result.values, parseConfig.allowNegative);
369391
} else if (token.kind === 'positional') {
370392
if (!allowPositionals) {
371393
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);

test/allow-negative.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* global assert */
2+
/* eslint max-len: 0 */
3+
'use strict';
4+
5+
const { test } = require('./utils');
6+
const { parseArgs } = require('../index');
7+
8+
test('disable negative options and args are started with "--no-" prefix', () => {
9+
const args = ['--no-alpha'];
10+
const options = { alpha: { type: 'boolean' } };
11+
assert.throws(() => {
12+
parseArgs({ args, options });
13+
}, {
14+
code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
15+
});
16+
});
17+
18+
test('args are passed `type: "string"` and allow negative options', () => {
19+
const args = ['--no-alpha', 'value'];
20+
const options = { alpha: { type: 'string' } };
21+
assert.throws(() => {
22+
parseArgs({ args, options, allowNegative: true });
23+
}, {
24+
code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
25+
});
26+
});
27+
28+
test('args are passed `type: "boolean"` and allow negative options', () => {
29+
const args = ['--no-alpha'];
30+
const options = { alpha: { type: 'boolean' } };
31+
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
32+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
33+
});
34+
35+
test('args are passed `default: "true"` and allow negative options', () => {
36+
const args = ['--no-alpha'];
37+
const options = { alpha: { type: 'boolean', default: true } };
38+
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
39+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
40+
});
41+
42+
test('args are passed `default: "false" and allow negative options', () => {
43+
const args = ['--no-alpha'];
44+
const options = { alpha: { type: 'boolean', default: false } };
45+
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
46+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
47+
});
48+
49+
test('allow negative options and multiple as true', () => {
50+
const args = ['--no-alpha', '--alpha', '--no-alpha'];
51+
const options = { alpha: { type: 'boolean', multiple: true } };
52+
const expected = { values: { __proto__: null, alpha: [false, true, false] }, positionals: [] };
53+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
54+
});
55+
56+
test('allow negative options and passed multiple arguments', () => {
57+
const args = ['--no-alpha', '--alpha'];
58+
const options = { alpha: { type: 'boolean' } };
59+
const expected = { values: { __proto__: null, alpha: true }, positionals: [] };
60+
assert.deepStrictEqual(parseArgs({ args, options, allowNegative: true }), expected);
61+
});
62+
63+
test('auto-detect --no-foo as negated when strict:false and allowNegative', () => {
64+
const holdArgv = process.argv;
65+
process.argv = [process.argv0, 'script.js', '--no-foo'];
66+
const holdExecArgv = process.execArgv;
67+
process.execArgv = [];
68+
const result = parseArgs({ strict: false, allowNegative: true });
69+
70+
const expected = { values: { __proto__: null, foo: false },
71+
positionals: [] };
72+
assert.deepStrictEqual(result, expected);
73+
process.argv = holdArgv;
74+
process.execArgv = holdExecArgv;
75+
});

0 commit comments

Comments
 (0)