Skip to content
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)
* 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) {
return result;
}
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], '-') ||
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