Skip to content

Commit

Permalink
util: support --no- for argument with boolean type for parseArgs
Browse files Browse the repository at this point in the history
  • Loading branch information
kylo5aby committed May 22, 2024
1 parent 1b96527 commit 2b5871d
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 6 deletions.
4 changes: 4 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,10 @@ Provides a higher level API for command-line argument parsing than interacting
with `process.argv` directly. Takes a specification for the expected arguments
and returns a structured object with the parsed options and positionals.

for an argument with type `boolean`, If the argument is passed with
the `--no-` prefix, the value of the argument will be set to the opposite
of its default value.

```mjs
import { parseArgs } from 'node:util';
const args = ['-f', '--bar', 'b'];
Expand Down
31 changes: 25 additions & 6 deletions lib/internal/util/parse_args/parse_args.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,24 @@ To specify an option argument starting with a dash use ${example}.`;
* @param {object} token - from tokens as available from parseArgs
*/
function checkOptionUsage(config, token) {
if (!ObjectHasOwn(config.options, token.name)) {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
token.rawName, config.allowPositionals);
let tokenName = token.name;
if (!ObjectHasOwn(config.options, tokenName)) {
if (StringPrototypeStartsWith(tokenName, 'no-')) {
tokenName = StringPrototypeSlice(tokenName, 3);
if (!ObjectHasOwn(config.options, tokenName) ||
optionsGetOwn(config.options, tokenName, 'type') !== 'boolean') {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
token.rawName, config.allowPositionals);
}
} else {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
token.rawName, config.allowPositionals);
}
}

const short = optionsGetOwn(config.options, token.name, 'short');
const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
const type = optionsGetOwn(config.options, token.name, 'type');
const short = optionsGetOwn(config.options, tokenName, 'short');
const shortAndLong = `${short ? `-${short}, ` : ''}--${tokenName}`;
const type = optionsGetOwn(config.options, tokenName, 'type');
if (type === 'string' && typeof token.value !== 'string') {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
}
Expand All @@ -124,6 +134,15 @@ function storeOption(longOption, optionValue, options, values) {
return; // No. Just no.
}

if (StringPrototypeStartsWith(longOption, 'no-') && !ObjectHasOwn(options, longOption)) {
// Boolean option negation: --no-foo
const longOptionWithoutPrefixNo = StringPrototypeSlice(longOption, 3);
if (optionsGetOwn(options, longOptionWithoutPrefixNo, 'type') !== 'string') {
longOption = longOptionWithoutPrefixNo;
optionValue = optionsGetOwn(options, longOption, 'default') === false;
}
}

// We store based on the option value rather than option type,
// preserving the users intent for author to deal with.
const newValue = optionValue ?? true;
Expand Down
60 changes: 60 additions & 0 deletions test/parallel/test-parse-args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,63 @@ test('multiple as false should expect a String', () => {
}, /"options\.alpha\.default" property must be of type string/
);
});

// test "--no-" prefix
test('args are passed `type: "string"` and start with "--no-"', () => {
const args = ['--no-alpha'];
const options = { alpha: { type: 'string' } };
assert.throws(() => {
parseArgs({ args, options });
}, {
code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
});
});

test('args are passed `type: "boolean"` and start with "--no-"', () => {
const args = ['--no-alpha'];
const options = { alpha: { type: 'boolean' } };
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
assert.deepStrictEqual(parseArgs({ args, options }), expected);
});

test('args start with "--no-" and passed `default: "true"`', () => {
const args = ['--no-alpha'];
const options = { alpha: { type: 'boolean', default: true } };
const expected = { values: { __proto__: null, alpha: false }, positionals: [] };
assert.deepStrictEqual(parseArgs({ args, options }), expected);
});

test('args start with "--no-" and passed `default: "false"`', () => {
const args = ['--no-alpha'];
const options = { alpha: { type: 'boolean', default: false } };
const expected = { values: { __proto__: null, alpha: true }, positionals: [] };
assert.deepStrictEqual(parseArgs({ args, options }), expected);
});

test('args start with "--no-" and multiple as true', () => {
const args = ['--no-alpha', '--alpha', '--no-alpha'];
const options = { alpha: { type: 'boolean', multiple: true } };
const expected = { values: { __proto__: null, alpha: [false, true, false] }, positionals: [] };
assert.deepStrictEqual(parseArgs({ args, options }), expected);
});

test('args start with "--no-" and passed multiple arguments', () => {
const args = ['--alpha', '--no-alpha', '--alpha'];
const options = { alpha: { type: 'boolean' } };
const expected = { values: { __proto__: null, alpha: true }, positionals: [] };
assert.deepStrictEqual(parseArgs({ args, options }), expected);
});

test('correct default args with prefix "--no-" when normal arguments', () => {
const holdArgv = process.argv;
process.argv = [process.argv0, 'script.js', '--no-foo'];
const holdExecArgv = process.execArgv;
process.execArgv = [];
const result = parseArgs({ strict: false });

const expected = { values: { __proto__: null, foo: false },
positionals: [] };
assert.deepStrictEqual(result, expected);
process.argv = holdArgv;
process.execArgv = holdExecArgv;
});

0 comments on commit 2b5871d

Please sign in to comment.