Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: support more config options for parseArgs #53434

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,10 @@ added:
- v18.3.0
- v16.17.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/53434
description: Add support for `required` and `placeholder`
options.
- version:
- v20.0.0
pr-url: https://github.com/nodejs/node/pull/46718
Expand Down Expand Up @@ -1422,6 +1426,9 @@ changes:
* `default` {string | boolean | string\[] | boolean\[]} The default option
value when it is not set by args. It must be of the same type as the
`type` property. When `multiple` is `true`, it must be an array.
* `required` {boolean} Whether the argument must be supplied. **Default:** `false`.
* `placeholder` {string | boolean} The default value to supply when an
RedYetiDev marked this conversation as resolved.
Show resolved Hide resolved
empty argument is supplied.
* `strict` {boolean} Should an error be thrown when unknown arguments
are encountered, or when arguments are passed that do not match the
`type` configured in `options`.
Expand Down
45 changes: 38 additions & 7 deletions lib/internal/util/parse_args/parse_args.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,13 @@ function checkOptionUsage(config, token) {
const short = optionsGetOwn(config.options, token.name, 'short');
const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
const type = optionsGetOwn(config.options, token.name, 'type');
const placeholder = optionsGetOwn(config.options, token.name, 'placeholder');
if (type === 'string' && typeof token.value !== 'string') {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
if (placeholder !== undefined) {
token.value = placeholder;
} else {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
}
}
// (Idiomatic test for undefined||null, expecting undefined.)
if (type === 'boolean' && token.value != null) {
Expand Down Expand Up @@ -327,18 +332,39 @@ const parseArgs = (config = kEmptyObject) => {
}

const defaultValue = objectGetOwn(optionConfig, 'default');
if (defaultValue !== undefined) {
let validator;
const placeholderValue = objectGetOwn(optionConfig, 'placeholder');

if (ObjectHasOwn(optionConfig, 'required')) {
const required = optionConfig.required;
validateBoolean(required, `options.${longOption}.required`);
if (required && defaultValue !== undefined) {
throw new ERR_INVALID_ARG_VALUE(
`options.${longOption}.required`,
required,
'cannot be true when default value is provided',
);
}
}

if (defaultValue !== undefined || placeholderValue !== undefined) {
let defaultValidator;
let placeholderValidator;
switch (optionType) {
case 'string':
validator = multipleOption ? validateStringArray : validateString;
defaultValidator = multipleOption ? validateStringArray : validateString;
placeholderValidator = validateString;
break;

case 'boolean':
validator = multipleOption ? validateBooleanArray : validateBoolean;
break;
defaultValidator = multipleOption ? validateBooleanArray : validateBoolean;
placeholderValidator = validateBoolean;
}
if (defaultValue !== undefined) {
defaultValidator(defaultValue, `options.${longOption}.default`);
}
if (placeholderValue !== undefined) {
placeholderValidator(placeholderValue, `options.${longOption}.placeholder`);
}
validator(defaultValue, `options.${longOption}.default`);
}
},
);
Expand Down Expand Up @@ -372,6 +398,11 @@ const parseArgs = (config = kEmptyObject) => {
// Phase 3: fill in default values for missing args
ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption,
1: optionConfig }) => {
const required = objectGetOwn(optionConfig, 'required') ?? false;
if (required && result.values[longOption] === undefined) {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(
`Option '${longOption}' is required`);
}
const mustSetDefault = useDefaultValueOption(longOption,
optionConfig,
result.values);
Expand Down
71 changes: 71 additions & 0 deletions test/parallel/test-parse-args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,74 @@ test('multiple as false should expect a String', () => {
}, /"options\.alpha\.default" property must be of type string/
);
});

// Required
test('required must be a boolean', () => {
const args = [];
const options = { alpha: { type: 'string', required: 'not a boolean' } };
assert.throws(() => {
parseArgs({ args, options });
}, /"options\.alpha\.required" property must be of type boolean/
);
});

test('required cannot be true when default is set', () => {
const args = [];
const options = { alpha: { type: 'string', required: true, default: 'HELLO' } };
assert.throws(() => {
parseArgs({ args, options });
}, /'options\.alpha\.required' cannot be true when default value is provided/
);
});

test('required option must be passed', () => {
const args = [];
const options = { alpha: { type: 'string', required: true } };
assert.throws(() => {
parseArgs({ args, options });
}, /Option 'alpha' is required/
);
});

test('required option with no value throws', () => {
const args = ['--alpha'];
const options = { alpha: { type: 'string', required: true } };
assert.throws(() => {
parseArgs({ args, options });
}, /Option '--alpha <value>' argument missing/
);
});

// Placeholder
test('placeholder must be the same type as the option (except array)', () => {
const args = [];
const options = { alpha: { type: 'string', placeholder: true } };
assert.throws(() => {
parseArgs({ args, options });
}, /"options\.alpha\.placeholder" property must be of type string/
);
});

test('placeholder must be the same type as the option, even when multiple', () => {
const args = ['--alpha'];
const options = { alpha: { type: 'string', multiple: true, placeholder: 'hello' } };
const expected = { values: { __proto__: null, alpha: ['hello'] }, positionals: [] };
const result = parseArgs({ args, options });
assert.deepStrictEqual(result, expected);
});

test('placeholder will be used when option is supplied with no value', () => {
const args = ['--alpha'];
const options = { alpha: { type: 'string', placeholder: 'HELLO', default: 'WORLD' } };
const expected = { values: { __proto__: null, alpha: 'HELLO' }, positionals: [] };
const result = parseArgs({ args, options });
assert.deepStrictEqual(result, expected);
});

test('placeholder can be used with required option', () => {
const args = ['--alpha'];
const options = { alpha: { type: 'string', placeholder: 'HELLO', required: true } };
const expected = { values: { __proto__: null, alpha: 'HELLO' }, positionals: [] };
const result = parseArgs({ args, options });
assert.deepStrictEqual(result, expected);
});