Skip to content

Commit 91bfb4d

Browse files
authored
feat: add parsed meta-data to returned properties (#129)
1 parent 6a7e969 commit 91bfb4d

File tree

10 files changed

+597
-134
lines changed

10 files changed

+597
-134
lines changed

.editorconfig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# top-most EditorConfig file
44
root = true
55

6+
# Copied from Node.js to ease compatibility in PR.
67
[*]
8+
charset = utf-8
79
end_of_line = lf
8-
insert_final_newline = true
9-
indent_style = space
1010
indent_size = 2
11-
tab_width = 2
12-
# trim_trailing_whitespace = true
11+
indent_style = space
12+
insert_final_newline = true
13+
trim_trailing_whitespace = true
14+
quote_type = single

README.md

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33

44
[![Coverage][coverage-image]][coverage-url]
55

6-
Polyfill of proposal for `util.parseArgs()`
6+
Polyfill of `util.parseArgs()`
77

88
## `util.parseArgs([config])`
99

1010
<!-- YAML
11-
added: REPLACEME
11+
added: v18.3.0
12+
changes:
13+
- version: REPLACEME
14+
pr-url: https://github.com/nodejs/node/pull/43459
15+
description: add support for returning detailed parse information
16+
using `tokens` in input `config` and returned properties.
1217
-->
1318

1419
> Stability: 1 - Experimental
@@ -25,18 +30,24 @@ added: REPLACEME
2530
times. If `true`, all values will be collected in an array. If
2631
`false`, values for the option are last-wins. **Default:** `false`.
2732
* `short` {string} A single character alias for the option.
28-
* `strict`: {boolean} Should an error be thrown when unknown arguments
33+
* `strict` {boolean} Should an error be thrown when unknown arguments
2934
are encountered, or when arguments are passed that do not match the
3035
`type` configured in `options`.
3136
**Default:** `true`.
32-
* `allowPositionals`: {boolean} Whether this command accepts positional
37+
* `allowPositionals` {boolean} Whether this command accepts positional
3338
arguments.
3439
**Default:** `false` if `strict` is `true`, otherwise `true`.
40+
* `tokens` {boolean} Return the parsed tokens. This is useful for extending
41+
the built-in behavior, from adding additional checks through to reprocessing
42+
the tokens in different ways.
43+
**Default:** `false`.
3544

3645
* Returns: {Object} The parsed command line arguments:
3746
* `values` {Object} A mapping of parsed option names with their {string}
3847
or {boolean} values.
3948
* `positionals` {string\[]} Positional arguments.
49+
* `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens)
50+
section. Only returned if `config` includes `tokens: true`.
4051

4152
Provides a higher level API for command-line argument parsing than interacting
4253
with `process.argv` directly. Takes a specification for the expected arguments
@@ -79,12 +90,120 @@ const {
7990
positionals
8091
} = parseArgs({ args, options });
8192
console.log(values, positionals);
82-
// Prints: [Object: null prototype] { foo: true, bar: 'b' } []ss
93+
// Prints: [Object: null prototype] { foo: true, bar: 'b' } []
8394
```
8495

8596
`util.parseArgs` is experimental and behavior may change. Join the
8697
conversation in [pkgjs/parseargs][] to contribute to the design.
8798

99+
### `parseArgs` `tokens`
100+
101+
Detailed parse information is available for adding custom behaviours by
102+
specifying `tokens: true` in the configuration.
103+
The returned tokens have properties describing:
104+
105+
* all tokens
106+
* `kind` {string} One of 'option', 'positional', or 'option-terminator'.
107+
* `index` {number} Index of element in `args` containing token. So the
108+
source argument for a token is `args[token.index]`.
109+
* option tokens
110+
* `name` {string} Long name of option.
111+
* `rawName` {string} How option used in args, like `-f` of `--foo`.
112+
* `value` {string | undefined} Option value specified in args.
113+
Undefined for boolean options.
114+
* `inlineValue` {boolean | undefined} Whether option value specified inline,
115+
like `--foo=bar`.
116+
* positional tokens
117+
* `value` {string} The value of the positional argument in args (i.e. `args[index]`).
118+
* option-terminator token
119+
120+
The returned tokens are in the order encountered in the input args. Options
121+
that appear more than once in args produce a token for each use. Short option
122+
groups like `-xy` expand to a token for each option. So `-xxx` produces
123+
three tokens.
124+
125+
For example to use the returned tokens to add support for a negated option
126+
like `--no-color`, the tokens can be reprocessed to change the value stored
127+
for the negated option.
128+
129+
```mjs
130+
import { parseArgs } from 'node:util';
131+
132+
const options = {
133+
'color': { type: 'boolean' },
134+
'no-color': { type: 'boolean' },
135+
'logfile': { type: 'string' },
136+
'no-logfile': { type: 'boolean' },
137+
};
138+
const { values, tokens } = parseArgs({ options, tokens: true });
139+
140+
// Reprocess the option tokens and overwrite the returned values.
141+
tokens
142+
.filter((token) => token.kind === 'option')
143+
.forEach((token) => {
144+
if (token.name.startsWith('no-')) {
145+
// Store foo:false for --no-foo
146+
const positiveName = token.name.slice(3);
147+
values[positiveName] = false;
148+
delete values[token.name];
149+
} else {
150+
// Resave value so last one wins if both --foo and --no-foo.
151+
values[token.name] = token.value ?? true;
152+
}
153+
});
154+
155+
const color = values.color;
156+
const logfile = values.logfile ?? 'default.log';
157+
158+
console.log({ logfile, color });
159+
```
160+
161+
```cjs
162+
const { parseArgs } = require('node:util');
163+
164+
const options = {
165+
'color': { type: 'boolean' },
166+
'no-color': { type: 'boolean' },
167+
'logfile': { type: 'string' },
168+
'no-logfile': { type: 'boolean' },
169+
};
170+
const { values, tokens } = parseArgs({ options, tokens: true });
171+
172+
// Reprocess the option tokens and overwrite the returned values.
173+
tokens
174+
.filter((token) => token.kind === 'option')
175+
.forEach((token) => {
176+
if (token.name.startsWith('no-')) {
177+
// Store foo:false for --no-foo
178+
const positiveName = token.name.slice(3);
179+
values[positiveName] = false;
180+
delete values[token.name];
181+
} else {
182+
// Resave value so last one wins if both --foo and --no-foo.
183+
values[token.name] = token.value ?? true;
184+
}
185+
});
186+
187+
const color = values.color;
188+
const logfile = values.logfile ?? 'default.log';
189+
190+
console.log({ logfile, color });
191+
```
192+
193+
Example usage showing negated options, and when an option is used
194+
multiple ways then last one wins.
195+
196+
```console
197+
$ node negate.js
198+
{ logfile: 'default.log', color: undefined }
199+
$ node negate.js --no-logfile --no-color
200+
{ logfile: false, color: false }
201+
$ node negate.js --logfile=test.log --color
202+
{ logfile: 'test.log', color: true }
203+
$ node negate.js --no-logfile --logfile=test.log --color --no-color
204+
{ logfile: 'test.log', color: false }
205+
```
206+
88207
-----
89208
90209
<!-- omit in toc -->

examples/negate.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
// This example is used in the documentation.
4+
5+
// How might I add my own support for --no-foo?
6+
7+
// 1. const { parseArgs } = require('node:util'); // from node
8+
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
9+
const { parseArgs } = require('..'); // in repo
10+
11+
const options = {
12+
'color': { type: 'boolean' },
13+
'no-color': { type: 'boolean' },
14+
'logfile': { type: 'string' },
15+
'no-logfile': { type: 'boolean' },
16+
};
17+
const { values, tokens } = parseArgs({ options, tokens: true });
18+
19+
// Reprocess the option tokens and overwrite the returned values.
20+
tokens
21+
.filter((token) => token.kind === 'option')
22+
.forEach((token) => {
23+
if (token.name.startsWith('no-')) {
24+
// Store foo:false for --no-foo
25+
const positiveName = token.name.slice(3);
26+
values[positiveName] = false;
27+
delete values[token.name];
28+
} else {
29+
// Resave value so last one wins if both --foo and --no-foo.
30+
values[token.name] = token.value ?? true;
31+
}
32+
});
33+
34+
const color = values.color;
35+
const logfile = values.logfile ?? 'default.log';
36+
37+
console.log({ logfile, color });
38+
39+
// Try the following:
40+
// node negate.js
41+
// node negate.js --no-logfile --no-color
42+
// negate.js --logfile=test.log --color
43+
// node negate.js --no-logfile --logfile=test.log --color --no-color

0 commit comments

Comments
 (0)