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: add util.parseArgs() #35015

Closed
wants to merge 14 commits into from
120 changes: 120 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,124 @@ Otherwise, returns `false`.
See [`assert.deepStrictEqual()`][] for more information about deep strict
equality.

## `util.parseArgs([argv[, options]])`
<!-- YAML
added: REPLACEME
-->

* `argv` {Array<string>|Object} (Optional) Array of argument strings; defaults
to [`process.argv.slice(2)`](process_argv). If an Object, the default is used,
and this parameter is considered to be the `options` parameter.
* `options` {Object} (Optional) The `options` parameter, if present, is an
object supporting the following property:
* `optionsWithValue` {Array<string>|string} (Optional) One or more argument
strings which _expect a value_ when present
* `multiOptions` {Array<string>|string} (Optional) One or more argument
strings which can be appear multiple times in `argv` and will be
concatenated into an array
* Returns: {Object} An object having properties:
* `options`, an Object with properties and values corresponding to parsed
Options and Flags
* `positionals`, an Array containing containing Positionals

The `util.parseArgs` function parses command-line arguments from an array of
strings and returns an object representation.

Example using [`process.argv`][]:

```js
// script.js
// called via `node script.js --foo bar baz`
const argv = util.parseArgs();

// argv.foo === true
if (argv.foo) {
console.log(argv.positionals); // prints [ 'bar', 'baz' ]
}
```

Example using a custom `argv` and the `optionsWithValue` option:

```js
const argv = util.parseArgs(
['--foo', 'bar', 'baz'],
{ optionsWithValue: ['foo'] }
);

// argv.foo === 'bar'
if (argv.foo === 'bar') {
console.log(argv.positionals); // prints [ 'baz' ]
}
```

Example using custom `argv`, `optionsWithValue`, and the `multiOptions` option:

```js
const argv = util.parseArgs(
['--foo', 'bar', '--foo', 'baz'],
{ optionsWithValue: 'foo', multiOptions: 'foo' }
);

console.log(argv.options.bar); // prints [ 'bar', 'baz' ]
```

[`ERR_INVALID_ARG_TYPE`][] will be thrown if the `argv` parameter is not an
Array.

Arguments fall into one of three catgories:

* _Flags_, which begin with one or more dashes (`-`), and _do not_ have an
associated string value (e.g., `node app.js --verbose`)
* These will be parsed automatically; you do not need to "declare" them
* The Flag _name_ is the string following the prefix of one-or-more dashes,
e.g., the name of `--foo` is `foo`
* Flag names become property names in the returned object
* When appearing _once_ in the array, the value of the property will be `true`
* When _repeated_ in the array, the value of the property becomes a count of
repetitions (e.g., `['-v', '-v' '-v']` results in `{ v: 3 }`)
* _Options_, declared by `optionsWithValue`, which begin with one or more
dashes, and _do_ have an associated value (e.g., `node app.js --require
script.js`)
* Use the `optionsWithValue` option to `util.parseArgs` to declare Options
* The Option _name_ is the string following the prefix of one-or-more dashes,
e.g., the name of `--foo` is `foo`
* The Option _value_ is the next string following the name, e.g., the Option
value of `['--foo' 'bar']` is `bar`
* Option values may be provided _with or without_ a `=` separator (e.g.,
`['--require=script.js']` is equivalent to `['--require', 'script.js']`)
* An Option value is considered "missing" and is results in `true` when:
* A `=` does not separate the Option name from its value (e.g., `--foo bar`)
_and_ the following argument begins with a dash (e.g., `['--foo',
'--bar']`), _OR_
* The array ends with the Option name (e.g., `['--foo']`)
* When repeated, values are concatenated into an Array; unlike Flags, they _do
not_ become a numeric count
* When an Option name appears in the Array (or string) of `optionsWithValue`,
and does _not_ appear in the `argv` array, the resulting object _will not_
contain a property with this Option name (e.g., `util.parseArgs(['--bar'],
{ optionsWithValue: 'foo' })` will result in `{bar: true, _: [] }`
* _Positionals_ (or "positional arguments"), which _do not_ begin with one or
more dashes (e.g., `['script.js']`), _or_ every item in the `argv` Array
following a `--` (e.g., `['--', 'script.js']`)
* Positionals appear in the Array property `_` of the returned object
* The `_` property will _always_ be present and an Array, even if empty
* If present in the `argv` Array, `--` is discarded and is omitted from the
returned object
* Positionals will _always_ be parsed verbatim (e.g., `['--', '--foo']` will
result in an object of `{_: ['--foo']}`)

A Flag or Option having the name `_` will be ignored. If it was declared as an
Option (via the `optionsWithValue` option), its value will be ignored as well.

`util.parseArgs` does not consider "short" arguments (e.g., `-v`) to be
different than "long" arguments (e.g., `--verbose`). Furthermore, it does not
allow concatenation of short arguments (e.g., `-v -D` cannot be expressed as
`-vD`).

_No_ conversion to/from "camelCase" occurs; a Flag or Option name of `no-color`
results in an object with a `no-color` property. A Flag or Option name of
`noColor` results in an object with a `noColor` property.

## `util.promisify(original)`
<!-- YAML
added: v8.0.0
Expand Down Expand Up @@ -2497,8 +2615,10 @@ util.log('Timestamped message.');
[compare function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters
[constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor
[default sort]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
[`ERR_INVALID_ARG_TYPE`]: errors.html#ERR_INVALID_ARG_TYPE
[global symbol registry]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for
[list of deprecated APIS]: deprecations.html#deprecations_list_of_deprecated_apis
[`napi_create_external()`]: n-api.html#n_api_napi_create_external
[`process.argv`]: process.html#process_process_argv
[semantically incompatible]: https://github.com/nodejs/node/issues/4179
[util.inspect.custom]: #util_util_inspect_custom
143 changes: 143 additions & 0 deletions lib/internal/util/parse_args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict';

const {
ArrayIsArray,
ArrayPrototypePush,
ArrayPrototypeSlice,
SafeSet,
StringPrototypeReplace,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;

/**
* Returns an Object representation of command-line arguments.
*
* Default behavior:
* - All arguments are considered "boolean flags"; in the `options` property of
* returned object, the key is the argument (if present), and the value will
* be `true`.
* - Uses `process.argv.slice(2)`, but can accept an explicit array of strings.
* - Argument(s) specified in `opts.optionsWithValue` will have `string` values
* instead of `true`; the subsequent item in the `argv` array will be consumed
* if a `=` is not used
* - "Bare" arguments are those which do not begin with a `-` or `--` and those
* after a bare `--`; these will be returned as items of the `positionals`
* array
* - The `positionals` array will always be present, even if empty.
* - The `options` Object will always be present, even if empty.
* @param {string[]} [argv=process.argv.slice(2)] - Array of script arguments as
* strings
* @param {Object} [options] - Options
* @param {string[]|string} [opts.optionsWithValue] - This argument (or
* arguments) expect a value
* @param {string[]|string} [opts.multiOptions] - This argument (or arguments)
boneskull marked this conversation as resolved.
Show resolved Hide resolved
* can be specified multiple times and its values will be concatenated into an
* array
* @returns {{options: Object, positionals: string[]}} Parsed arguments
* @example
* parseArgs(['--foo', '--bar'])
* // {options: { foo: true, bar: true }, positionals: []}
* parseArgs(['--foo', '-b'])
* // {options: { foo: true, b: true }, positionals: []}
* parseArgs(['---foo'])
* // {options: { foo: true }, positionals: []}
* parseArgs(['--foo=bar'])
* // {options: { foo: true }, positionals: []}
* parseArgs([--foo', 'bar'])
* // {options: {foo: true}, positionals: ['bar']}
* parseArgs(['--foo', 'bar'], {optionsWithValue: 'foo'})
* // {options: {foo: 'bar'}, positionals: []}
* parseArgs(['foo'])
* // {options: {}, positionals: ['foo']}
* parseArgs(['--foo', '--', '--bar'])
* // {options: {foo: true}, positionals: ['--bar']}
* parseArgs(['--foo=bar', '--foo=baz'])
* // {options: {foo: true}, positionals: []}
* parseArgs(['--foo=bar', '--foo=baz'], {optionsWithValue: 'foo'})
* // {options: {foo: 'baz'}, positionals: []}
* parseArgs(['--foo=bar', '--foo=baz'], {
* optionsWithValue: 'foo', multiOptions: 'foo'
* }) // {options: {foo: ['bar', 'baz']}, positionals: []}
* parseArgs(['--foo', '--foo'])
* // {options: {foo: true}, positionals: []}
* parseArgs(['--foo', '--foo'], {multiOptions: ['foo']})
* // {options: {foo: [true, true]}, positionals: []}
*/
const parseArgs = (
argv = ArrayPrototypeSlice(process.argv, 2),
options = { optionsWithValue: [] }
) => {
if (!ArrayIsArray(argv)) {
options = argv;
argv = ArrayPrototypeSlice(process.argv, 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to process.argv.slice(2) is fine but I think the implementation should somewhat also consider the eval usecase, right now if I try running node -p|-e I get a very unhelpful error:

$ node -p 'require('util').parseArgs()' foo bar
internal/validators.js:122
    throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
    ^

TypeError [ERR_INVALID_ARG_TYPE]: The "id" argument must be of type string. Received an instance of Object
    at new NodeError (internal/errors.js:253:15)
    at validateString (internal/validators.js:122:11)
    at Module.require (internal/modules/cjs/loader.js:972:3)
    at require (internal/modules/cjs/helpers.js:88:18)
    at [eval]:1:1
    at Script.runInThisContext (vm.js:132:18)
    at Object.runInThisContext (vm.js:309:38)
    at internal/process/execution.js:77:19
    at [eval]-wrapper:6:22
    at evalScript (internal/process/execution.js:76:60) {
  code: 'ERR_INVALID_ARG_TYPE'
}

In the past I've done something like process.argv.slice(require.main ? 2 : 1) in order to support it (though there might be better ways to handle the check in core).

IMO parseArgs should either handle eval/script properly OR at least throw an useful error instead 😊

}
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE(
'options',
'object',
options);
}
if (typeof options.optionsWithValue === 'string') {
options.optionsWithValue = [options.optionsWithValue];
}
if (typeof options.multiOptions === 'string') {
options.multiOptions = [options.multiOptions];
}
Comment on lines +88 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my request for changes still stands on these

const optionsWithValue = new SafeSet(options.optionsWithValue || []);
const multiOptions = new SafeSet(options.multiOptions || []);
const result = { positionals: [], options: {} };

let pos = 0;
while (true) {
const arg = argv[pos];
if (arg === undefined) {
boneskull marked this conversation as resolved.
Show resolved Hide resolved
return result;
}
boneskull marked this conversation as resolved.
Show resolved Hide resolved
if (StringPrototypeStartsWith(arg, '-')) {
// Everything after a bare '--' is considered a positional argument
// and is returned verbatim
if (arg === '--') {
ArrayPrototypePush(
result.positionals, ...ArrayPrototypeSlice(argv, ++pos)
);
return result;
}
// Any number of leading dashes are allowed
const argParts = StringPrototypeSplit(StringPrototypeReplace(arg, /^-+/, ''), '=');
const optionName = argParts[0];
let optionValue = argParts[1];

// Consume the next item in the array if `=` was not used
// and the next item is not itself a flag or option
if (optionsWithValue.has(optionName)) {
if (optionValue === undefined) {
optionValue = StringPrototypeStartsWith(argv[pos + 1], '-') ||
boneskull marked this conversation as resolved.
Show resolved Hide resolved
argv[++pos];
}
} else {
optionValue = true;
}

if (multiOptions.has(optionName)) {
// Consume the next item in the array if `=` was not used
// and the next item is not itself a flag or option
if (result.options[optionName] === undefined) {
result.options[optionName] = [optionValue];
} else {
ArrayPrototypePush(result.options[optionName], optionValue);
}
} else {
result.options[optionName] = optionValue;
}
} else {
ArrayPrototypePush(result.positionals, arg);
}
pos++;
}
};

module.exports = {
parseArgs
};
2 changes: 2 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const { validateNumber } = require('internal/validators');
const { TextDecoder, TextEncoder } = require('internal/encoding');
const { isBuffer } = require('buffer').Buffer;
const types = require('internal/util/types');
const { parseArgs } = require('internal/util/parse_args');

const {
deprecate,
Expand Down Expand Up @@ -270,6 +271,7 @@ module.exports = {
isFunction,
isPrimitive,
log,
parseArgs,
promisify,
TextDecoder,
TextEncoder,
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
'lib/internal/util/debuglog.js',
'lib/internal/util/inspect.js',
'lib/internal/util/inspector.js',
'lib/internal/util/parse_args.js',
'lib/internal/util/types.js',
'lib/internal/http2/core.js',
'lib/internal/http2/compat.js',
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-bootstrap-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const expectedModules = new Set([
'NativeModule internal/util',
'NativeModule internal/util/debuglog',
'NativeModule internal/util/inspect',
'NativeModule internal/util/parse_args',
'NativeModule internal/util/types',
'NativeModule internal/validators',
'NativeModule internal/vm/module',
Expand Down
Loading