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

feat: add parsed meta-data to returned properties #129

Merged
merged 69 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
52c3a2e
Proof of concept
shadowspawn May 14, 2022
5ec71b1
Return originalArgs and indices
shadowspawn May 14, 2022
f711187
Change short in AST to boolean
shadowspawn May 24, 2022
cce90bb
Store inlineValue rather than valueIndex
shadowspawn May 24, 2022
e4bc5fe
Rename symbol property in AST to kind
shadowspawn May 24, 2022
87624a1
Rename returned ast property to parseElements
shadowspawn May 24, 2022
2925639
Refactor to use parseElements to test for errors and store values
shadowspawn May 29, 2022
412287e
Merge remote-tracking branch 'upstream/main' into feature/ast
shadowspawn May 29, 2022
1897dac
Build positionals from parseElements
shadowspawn May 29, 2022
45d12e7
Replace .push with primordial
shadowspawn May 29, 2022
1d19fb2
forEach replaced with primordial
shadowspawn May 29, 2022
9d1267d
Swap isShort for optionUsed
shadowspawn May 30, 2022
c9c4d4c
Rework checkOptionLikeValue
shadowspawn May 30, 2022
ed4168d
Consistent property order and renames
shadowspawn May 30, 2022
16a9f51
Comment out new returned property until naming and tests ready
shadowspawn May 30, 2022
915e6c3
Refactor tokenize into own function
shadowspawn May 31, 2022
641197e
First test pass, get tests working by ignoring tokens in results
shadowspawn May 31, 2022
18470bd
Less magical check now checking kind
shadowspawn May 31, 2022
38d1d6c
Add some token tests
shadowspawn May 31, 2022
cfbd436
Fix index after space-separated option value
shadowspawn May 31, 2022
397dcf1
Make tokens opt-in
shadowspawn Jun 1, 2022
938bdb0
Simplify more tests with opt-in details
shadowspawn Jun 1, 2022
c5da345
Rename token.optionName to name, and restore longOption/shortOption …
shadowspawn Jun 1, 2022
4e1ec2d
Add example
shadowspawn Jun 1, 2022
bd3f574
Add side-note
shadowspawn Jun 2, 2022
b703ee8
Add example of #52, limit long syntax
shadowspawn Jun 2, 2022
82339f9
Add ordered example
shadowspawn Jun 2, 2022
fab1dc4
Add example for no repeated options
shadowspawn Jun 2, 2022
99165c9
Comment wording
shadowspawn Jun 2, 2022
9bd039d
Switch from details to tokens for configuration
shadowspawn Jun 3, 2022
6a3f637
Simplify example by removing "library" support
shadowspawn Jun 3, 2022
4cfbc29
Remove comments on how well-advised the examples goals are
shadowspawn Jun 3, 2022
9a9a740
Switch from optionUser to rawName
shadowspawn Jun 3, 2022
7cd685c
Merge remote-tracking branch 'upstream/main' into feature/ast
shadowspawn Jun 3, 2022
6f93632
Use modern syntax
shadowspawn Jun 4, 2022
642d8c0
Validate new input property
shadowspawn Jun 4, 2022
e70609a
Move strict check outside check routines
shadowspawn Jun 4, 2022
fdaa553
Tidy object setup
shadowspawn Jun 4, 2022
c293307
Simplify test
shadowspawn Jun 4, 2022
041459d
Indentation and match filename
shadowspawn Jun 4, 2022
7aafaf6
Rework with strict:true
shadowspawn Jun 4, 2022
1b6e585
Longer but simpler
shadowspawn Jun 4, 2022
20eacb0
Add textual description, and some fixes
shadowspawn Jun 4, 2022
81239d6
Add example output for tokens
shadowspawn Jun 4, 2022
84dde8e
Add example used in new documentation
shadowspawn Jun 4, 2022
40a6201
Refactor documentation
shadowspawn Jun 4, 2022
07fbb91
Update .editorconfig from Node.js
shadowspawn Jun 4, 2022
35c16f1
rework token property descriptions
shadowspawn Jun 4, 2022
038480a
Minor refactor of remaining arg processing
shadowspawn Jun 7, 2022
d4f96cc
Replace switch with if/else
shadowspawn Jun 14, 2022
122727e
Remove side-affect, per feedback
shadowspawn Jun 15, 2022
5b3518d
Add tricky case of short option group to token expansion
shadowspawn Jun 15, 2022
c8c2f84
Rework description of token.index after token example.
shadowspawn Jun 15, 2022
2b119b0
Be less clever with script naming in example
shadowspawn Jun 15, 2022
1b3c076
Remove superfluous colons in docs
shadowspawn Jun 17, 2022
e9e30a7
Upstream lint
shadowspawn Jun 17, 2022
46ab295
Merge branch 'main' into feature/ast
shadowspawn Jun 23, 2022
01baee5
Merge branch 'main' into feature/ast
shadowspawn Jun 24, 2022
46903af
Improve description
shadowspawn Jul 4, 2022
9b74c05
Merge branch 'feature/ast' of github.com:shadowspawn/parseargs into f…
shadowspawn Jul 4, 2022
89e4532
Use negate as example for tokens. Rework description a little.
shadowspawn Jul 4, 2022
3914949
Lint and feedback
shadowspawn Jul 4, 2022
d8126d1
Expand small token tests and remove uber tests
shadowspawn Jul 5, 2022
87bab7b
Use kEmptyObject per upstream suggestion. Move node lookalikes to int…
shadowspawn Jul 5, 2022
02ea585
Reworks tokens documentation for config
shadowspawn Jul 5, 2022
56fe4ca
Add version changes to YAML.
shadowspawn Jul 5, 2022
722f05e
Update README with upstream changes
shadowspawn Jul 5, 2022
ae9eca4
Update negate example calls to match documentation
shadowspawn Jul 16, 2022
f9dcc4f
Remove extra examples
shadowspawn Jul 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
# top-most EditorConfig file
root = true

# Copied from Node.js to ease compatibility in PR.
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
tab_width = 2
# trim_trailing_whitespace = true
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single
129 changes: 124 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@

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

Polyfill of proposal for `util.parseArgs()`
Polyfill of `util.parseArgs()`

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

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

> Stability: 1 - Experimental
Expand All @@ -25,18 +30,24 @@ added: REPLACEME
times. If `true`, all values will be collected in an array. If
`false`, values for the option are last-wins. **Default:** `false`.
* `short` {string} A single character alias for the option.
* `strict`: {boolean} Should an error be thrown when unknown arguments
* `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`.
**Default:** `true`.
* `allowPositionals`: {boolean} Whether this command accepts positional
* `allowPositionals` {boolean} Whether this command accepts positional
arguments.
**Default:** `false` if `strict` is `true`, otherwise `true`.
* `tokens` {boolean} Return the parsed tokens. This is useful for extending
the built-in behavior, from adding additional checks through to reprocessing
the tokens in different ways.
**Default:** `false`.

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

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

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

### `parseArgs` `tokens`

Detailed parse information is available for adding custom behaviours by
specifying `tokens: true` in the configuration.
The returned tokens have properties describing:

* all tokens
* `kind` {string} One of 'option', 'positional', or 'option-terminator'.
* `index` {number} Index of element in `args` containing token. So the
source argument for a token is `args[token.index]`.
* option tokens
Copy link
Collaborator

Choose a reason for hiding this comment

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

To improve the readability I would explain that we mean the kind=option token

Suggested change
* option tokens
* 'option' tokens

Copy link
Member

Choose a reason for hiding this comment

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

rather than quotes, which i find confusing, how about linking the term to the appropriate section?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, that works too.

I use the same syntax in the kind description to be homogeneous

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The compact POJO description is a bit subtle to read. How about an expanded version with all the properties listed?

Proposed expanded

A returned token has two properties which are always defined,
and some other properties which vary depending on the kind:

  • kind {string} One of 'option', 'positional', or 'option-terminator'.
  • index {number} Index of element in args containing token. So the
    source argument for a token is args[token.index].

An option token has additional parse details
for an option detected in the input args:

  • kind = 'option'
  • index {number} Index of element in args containing token.
  • name {string} Long name of option.
  • rawName {string} How option used in args, like -f of --foo.
  • value {string | undefined} Option value specified in args.
    Undefined for boolean options.
  • inlineValue {boolean | undefined} Whether option value specified inline,
    like --foo=bar.

A positional token has just one additional property with the positional value:

  • kind = 'positional'
  • index {number} Index of element in args containing token.
  • value {string} The value of the positional argument in args (i.e. args[index]).

An option-terminator token has only the base properties:

  • kind = 'option-terminator'
  • index {number} Index of element in args containing token.

Old compact

The returned tokens have properties describing:

  • all tokens
    • kind {string} One of 'option', 'positional', or 'option-terminator'.
    • index {number} Index of element in args containing token. So the
      source argument for a token is args[token.index].
  • option tokens
    • name {string} Long name of option.
    • rawName {string} How option used in args, like -f of --foo.
    • value {string | undefined} Option value specified in args.
      Undefined for boolean options.
    • inlineValue {boolean | undefined} Whether option value specified inline,
      like --foo=bar.
  • positional tokens
    • value {string} The value of the positional argument in args (i.e. args[index]).
  • option-terminator token

(Also asked in: nodejs/node#43459 (comment))

* `name` {string} Long name of option.
* `rawName` {string} How option used in args, like `-f` of `--foo`.
* `value` {string | undefined} Option value specified in args.
Undefined for boolean options.
* `inlineValue` {boolean | undefined} Whether option value specified inline,
like `--foo=bar`.
* positional tokens
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* positional tokens
* 'positional' tokens

* `value` {string} The value of the positional argument in args (i.e. `args[index]`).
* option-terminator token
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* option-terminator token
* 'option-terminator' token


The returned tokens are in the order encountered in the input args. Options
that appear more than once in args produce a token for each use. Short option
groups like `-xy` expand to a token for each option. So `-xxx` produces
three tokens.

For example to use the returned tokens to add support for a negated option
like `--no-color`, the tokens can be reprocessed to change the value stored
for the negated option.

```mjs
import { parseArgs } from 'node:util';

const options = {
'color': { type: 'boolean' },
'no-color': { type: 'boolean' },
'logfile': { type: 'string' },
'no-logfile': { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });

// Reprocess the option tokens and overwrite the returned values.
tokens
.filter((token) => token.kind === 'option')
.forEach((token) => {
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3);
values[positiveName] = false;
delete values[token.name];
} else {
// Resave value so last one wins if both --foo and --no-foo.
values[token.name] = token.value ?? true;
}
});

const color = values.color;
const logfile = values.logfile ?? 'default.log';

console.log({ logfile, color });
```

```cjs
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
const { parseArgs } = require('node:util');

const options = {
'color': { type: 'boolean' },
'no-color': { type: 'boolean' },
'logfile': { type: 'string' },
'no-logfile': { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });

// Reprocess the option tokens and overwrite the returned values.
tokens
.filter((token) => token.kind === 'option')
.forEach((token) => {
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3);
values[positiveName] = false;
delete values[token.name];
} else {
// Resave value so last one wins if both --foo and --no-foo.
values[token.name] = token.value ?? true;
}
});

const color = values.color;
const logfile = values.logfile ?? 'default.log';

console.log({ logfile, color });
```

Example usage showing negated options, and when an option is used
multiple ways then last one wins.

```console
$ node negate.js
{ logfile: 'default.log', color: undefined }
$ node negate.js --no-logfile --no-color
{ logfile: false, color: false }
$ node negate.js --logfile=test.log --color
{ logfile: 'test.log', color: true }
$ node negate.js --no-logfile --logfile=test.log --color --no-color
{ logfile: 'test.log', color: false }
```

-----

<!-- omit in toc -->
Expand Down
43 changes: 43 additions & 0 deletions examples/negate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

// This example is used in the documentation.

// How might I add my own support for --no-foo?

// 1. const { parseArgs } = require('node:util'); // from node
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
const { parseArgs } = require('..'); // in repo

const options = {
'color': { type: 'boolean' },
'no-color': { type: 'boolean' },
'logfile': { type: 'string' },
'no-logfile': { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });

// Reprocess the option tokens and overwrite the returned values.
tokens
.filter((token) => token.kind === 'option')
.forEach((token) => {
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3);
values[positiveName] = false;
delete values[token.name];
} else {
// Resave value so last one wins if both --foo and --no-foo.
values[token.name] = token.value ?? true;
}
});

const color = values.color;
const logfile = values.logfile ?? 'default.log';

console.log({ logfile, color });

// Try the following:
// node negate.js
// node negate.js --no-logfile --no-color
// negate.js --logfile=test.log --color
// node negate.js --no-logfile --logfile=test.log --color --no-color