From 1060df006c76819f440a704cd85ccaebc07e0f1b Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 17 Jun 2022 22:29:32 +1200 Subject: [PATCH 01/13] util: add tokens to parseArgs Offer additional meta-data for building custom and additional behaviour on top of parseArgs. --- doc/api/util.md | 74 ++++++ lib/internal/util/parse_args/parse_args.js | 246 +++++++++++-------- test/parallel/test-parse-args.mjs | 273 +++++++++++++++++++++ 3 files changed, 491 insertions(+), 102 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 4f5f10957a8192..d410896e34e8f5 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1101,6 +1101,80 @@ console.log(values, positionals); // Prints: [Object: null prototype] { foo: true, bar: 'b' } [] ``` +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. +* 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 } Positional value (i.e. `args[index]`). +* option-terminator token + +For example, assuming the following script which uses +automatic detection of options and no error checking: + +```mjs +import { parseArgs } from 'node:util'; +console.log(parseArgs({ strict: false, tokens: true })); +``` + +```cjs +const { parseArgs } = require('node:util'); +console.log(parseArgs({ strict: false, tokens: true })); +``` + +This call shows the three kinds of token and their properties: + +```console +$ node tokens.cjs -xy --foo=BAR -- file.txt +{ + values: [Object: null prototype] { d: true, foo: 'BAR' }, + positionals: [ 'file.txt' ], + tokens: [ + { + kind: 'option', + name: 'x', + rawName: '-x', + index: 0, + value: undefined, + inlineValue: undefined + }, + { + kind: 'option', + name: 'y', + rawName: '-y', + index: 0, + value: undefined, + inlineValue: undefined + }, + { + kind: 'option', + name: 'foo', + rawName: '--foo', + index: 1, + value: 'BAR', + inlineValue: true + }, + { kind: 'option-terminator', index: 2 }, + { kind: 'positional', index: 3, value: 'file.txt' } + ] +} +``` + +The source argument for a token is `args[token.index]`. +Short option groups like `-xy` expand to a token for each option. +The `x` and `y` tokens above have the same index, since +they come from the same argument. + `util.parseArgs` is experimental and behavior may change. Join the conversation in [pkgjs/parseargs][] to contribute to the design. diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index f27cd0d74d12d2..32a7681ab0f96e 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -3,16 +3,18 @@ const { ArrayPrototypeForEach, ArrayPrototypeIncludes, + ArrayPrototypeMap, + ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeShift, ArrayPrototypeSlice, - ArrayPrototypePush, ArrayPrototypeUnshiftApply, - ObjectPrototypeHasOwnProperty: ObjectHasOwn, ObjectEntries, + ObjectPrototypeHasOwnProperty: ObjectHasOwn, StringPrototypeCharAt, StringPrototypeIndexOf, StringPrototypeSlice, + StringPrototypeStartsWith, } = primordials; const { @@ -64,19 +66,16 @@ function getMainArgs() { /** * In strict mode, throw for possible usage errors like --foo --bar * - * @param {string} longOption - long option name e.g. 'foo' - * @param {string|undefined} optionValue - value from user args - * @param {string} shortOrLong - option used, with dashes e.g. `-l` or `--long` - * @param {boolean} strict - show errors, from parseArgs({ strict }) + * @param {object} token - from tokens as available from parseArgs */ -function checkOptionLikeValue(longOption, optionValue, shortOrLong, strict) { - if (strict && isOptionLikeValue(optionValue)) { +function checkOptionLikeValue(token) { + if (!token.inlineValue && isOptionLikeValue(token.value)) { // Only show short example if user used short option. - const example = (shortOrLong.length === 2) ? - `'--${longOption}=-XYZ' or '${shortOrLong}-XYZ'` : - `'--${longOption}=-XYZ'`; - const errorMessage = `Option '${shortOrLong}' argument is ambiguous. -Did you forget to specify the option argument for '${shortOrLong}'? + const example = StringPrototypeStartsWith(token.rawName, '--') ? + `'${token.rawName}=-XYZ'` : + `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`; + const errorMessage = `Option '${token.rawName}' argument is ambiguous. +Did you forget to specify the option argument for '${token.rawName}'? To specify an option argument starting with a dash use ${example}.`; throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage); } @@ -85,34 +84,28 @@ To specify an option argument starting with a dash use ${example}.`; /** * In strict mode, throw for usage errors. * - * @param {string} longOption - long option name e.g. 'foo' - * @param {string|undefined} optionValue - value from user args - * @param {object} options - option configs, from parseArgs({ options }) - * @param {string} shortOrLong - option used, with dashes e.g. `-l` or `--long` - * @param {boolean} strict - show errors, from parseArgs({ strict }) - * @param {boolean} allowPositionals - from parseArgs({ allowPositionals }) + * @param {object} config - from config passed to parseArgs + * @param {object} token - from tokens as available from parseArgs */ -function checkOptionUsage(longOption, optionValue, options, - shortOrLong, strict, allowPositionals) { - // Strict and options are used from local context. - if (!strict) return; - - if (!ObjectHasOwn(options, longOption)) { - throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOrLong, allowPositionals); +function checkOptionUsage(config, token) { + if (!ObjectHasOwn(config.options, token.name)) { + throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( + token.rawName, config.allowPositionals); } - const short = optionsGetOwn(options, longOption, 'short'); - const shortAndLong = short ? `-${short}, --${longOption}` : `--${longOption}`; - const type = optionsGetOwn(options, longOption, 'type'); - if (type === 'string' && typeof optionValue !== 'string') { + const short = optionsGetOwn(config.options, token.name, 'short'); + const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`; + const type = optionsGetOwn(config.options, token.name, 'type'); + if (type === 'string' && typeof token.value !== 'string') { throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} ' argument missing`); } // (Idiomatic test for undefined||null, expecting undefined.) - if (type === 'boolean' && optionValue != null) { + if (type === 'boolean' && token.value != null) { throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`); } } + /** * Store the option value in `values`. * @@ -145,64 +138,38 @@ function storeOption(longOption, optionValue, options, values) { } } -const parseArgs = (config = { __proto__: null }) => { - const args = objectGetOwn(config, 'args') ?? getMainArgs(); - const strict = objectGetOwn(config, 'strict') ?? true; - const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; - const options = objectGetOwn(config, 'options') ?? { __proto__: null }; - - // Validate input configuration. - validateArray(args, 'args'); - validateBoolean(strict, 'strict'); - validateBoolean(allowPositionals, 'allowPositionals'); - validateObject(options, 'options'); - ArrayPrototypeForEach( - ObjectEntries(options), - ({ 0: longOption, 1: optionConfig }) => { - validateObject(optionConfig, `options.${longOption}`); - - // type is required - validateUnion(objectGetOwn(optionConfig, 'type'), `options.${longOption}.type`, ['string', 'boolean']); - - if (ObjectHasOwn(optionConfig, 'short')) { - const shortOption = optionConfig.short; - validateString(shortOption, `options.${longOption}.short`); - if (shortOption.length !== 1) { - throw new ERR_INVALID_ARG_VALUE( - `options.${longOption}.short`, - shortOption, - 'must be a single character' - ); - } - } - - if (ObjectHasOwn(optionConfig, 'multiple')) { - validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`); - } - } - ); - - const result = { - values: { __proto__: null }, - positionals: [] - }; +/** + * Process args and turn into identified tokens: + * - option (along with value, if any) + * - positional + * - option-terminator + * + * @param {string[]} args - from parseArgs({ args }) or mainArgs + * @param {object} options - option configs, from parseArgs({ options }) + */ +function argsToTokens(args, options) { + const tokens = []; + let index = -1; + let groupCount = 0; const remainingArgs = ArrayPrototypeSlice(args); while (remainingArgs.length > 0) { const arg = ArrayPrototypeShift(remainingArgs); const nextArg = remainingArgs[0]; + if (groupCount > 0) + groupCount--; + else + index++; // Check if `arg` is an options terminator. // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html if (arg === '--') { - if (!allowPositionals && remainingArgs.length > 0) { - throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(nextArg); - } - // Everything after a bare '--' is considered a positional argument. + ArrayPrototypePush(tokens, { kind: 'option-terminator', index }); ArrayPrototypePushApply( - result.positionals, - remainingArgs + tokens, ArrayPrototypeMap(remainingArgs, (arg) => { + return { kind: 'positional', index: ++index, value: arg }; + }) ); break; // Finished processing args, leave while loop. } @@ -211,16 +178,19 @@ const parseArgs = (config = { __proto__: null }) => { // e.g. '-f' const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); - let optionValue; + let value; + let inlineValue; if (optionsGetOwn(options, longOption, 'type') === 'string' && isOptionValue(nextArg)) { // e.g. '-f', 'bar' - optionValue = ArrayPrototypeShift(remainingArgs); - checkOptionLikeValue(longOption, optionValue, arg, strict); + value = ArrayPrototypeShift(remainingArgs); + inlineValue = false; } - checkOptionUsage(longOption, optionValue, options, - arg, strict, allowPositionals); - storeOption(longOption, optionValue, options, result.values); + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: arg, + index, value, inlineValue }); + if (value != null) ++index; continue; } @@ -242,6 +212,7 @@ const parseArgs = (config = { __proto__: null }) => { } } ArrayPrototypeUnshiftApply(remainingArgs, expanded); + groupCount = expanded.length; continue; } @@ -249,45 +220,116 @@ const parseArgs = (config = { __proto__: null }) => { // e.g. -fFILE const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); - const optionValue = StringPrototypeSlice(arg, 2); - checkOptionUsage(longOption, optionValue, options, `-${shortOption}`, strict, allowPositionals); - storeOption(longOption, optionValue, options, result.values); + const value = StringPrototypeSlice(arg, 2); + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: `-${shortOption}`, + index, value, inlineValue: true }); continue; } if (isLoneLongOption(arg)) { // e.g. '--foo' const longOption = StringPrototypeSlice(arg, 2); - let optionValue; + let value; + let inlineValue; if (optionsGetOwn(options, longOption, 'type') === 'string' && isOptionValue(nextArg)) { // e.g. '--foo', 'bar' - optionValue = ArrayPrototypeShift(remainingArgs); - checkOptionLikeValue(longOption, optionValue, arg, strict); + value = ArrayPrototypeShift(remainingArgs); + inlineValue = false; } - checkOptionUsage(longOption, optionValue, options, - arg, strict, allowPositionals); - storeOption(longOption, optionValue, options, result.values); + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: arg, + index, value, inlineValue }); + if (value != null) ++index; continue; } if (isLongOptionAndValue(arg)) { // e.g. --foo=bar - const index = StringPrototypeIndexOf(arg, '='); - const longOption = StringPrototypeSlice(arg, 2, index); - const optionValue = StringPrototypeSlice(arg, index + 1); - checkOptionUsage(longOption, optionValue, options, `--${longOption}`, strict, allowPositionals); - storeOption(longOption, optionValue, options, result.values); + const equalIndex = StringPrototypeIndexOf(arg, '='); + const longOption = StringPrototypeSlice(arg, 2, equalIndex); + const value = StringPrototypeSlice(arg, equalIndex + 1); + ArrayPrototypePush( + tokens, + { kind: 'option', name: longOption, rawName: `--${longOption}`, + index, value, inlineValue: true }); continue; } - // Anything left is a positional - if (!allowPositionals) { - throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(arg); + ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg }); + } + return tokens; +} + +const parseArgs = (config = { __proto__: null }) => { + const args = objectGetOwn(config, 'args') ?? getMainArgs(); + const strict = objectGetOwn(config, 'strict') ?? true; + const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; + const returnTokens = objectGetOwn(config, 'tokens') ?? false; + const options = objectGetOwn(config, 'options') ?? { __proto__: null }; + // Bundle these up for passing to strict-mode checks. + const parseConfig = { args, strict, options, allowPositionals }; + + // Validate input configuration. + validateArray(args, 'args'); + validateBoolean(strict, 'strict'); + validateBoolean(allowPositionals, 'allowPositionals'); + validateBoolean(returnTokens, 'tokens'); + validateObject(options, 'options'); + ArrayPrototypeForEach( + ObjectEntries(options), + ({ 0: longOption, 1: optionConfig }) => { + validateObject(optionConfig, `options.${longOption}`); + + // type is required + validateUnion(objectGetOwn(optionConfig, 'type'), `options.${longOption}.type`, ['string', 'boolean']); + + if (ObjectHasOwn(optionConfig, 'short')) { + const shortOption = optionConfig.short; + validateString(shortOption, `options.${longOption}.short`); + if (shortOption.length !== 1) { + throw new ERR_INVALID_ARG_VALUE( + `options.${longOption}.short`, + shortOption, + 'must be a single character' + ); + } + } + + if (ObjectHasOwn(optionConfig, 'multiple')) { + validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`); + } } + ); + + // Phase 1: identify tokens + const tokens = argsToTokens(args, options); - ArrayPrototypePush(result.positionals, arg); + // Phase 2: process tokens into parsed option values and positionals + const result = { + values: { __proto__: null }, + positionals: [], + }; + if (returnTokens) { + result.tokens = tokens; } + ArrayPrototypeForEach(tokens, (token) => { + if (token.kind === 'option') { + if (strict) { + checkOptionUsage(parseConfig, token); + checkOptionLikeValue(token); + } + storeOption(token.name, token.value, options, result.values); + } else if (token.kind === 'positional') { + if (!allowPositionals) { + throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value); + } + ArrayPrototypePush(result.positionals, token.value); + } + }); return result; }; diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index ef0ac006282bd7..1c49dcba770cab 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -578,3 +578,276 @@ test('strict: when long option and suspect value then throws with whole expected }, /To specify an option argument starting with a dash use '--with=-XYZ'/ ); }); + +test('tokens: positional', () => { + const args = ['one']; + const expectedTokens = [ + { kind: 'positional', index: 0, value: 'one' }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: -- followed by option-like', () => { + const args = ['--', '--foo']; + const expectedTokens = [ + { kind: 'option-terminator', index: 0 }, + { kind: 'positional', index: 1, value: '--foo' }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true boolean short', () => { + const args = ['-f']; + const options = { + file: { short: 'f', type: 'boolean' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '-f', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true boolean long', () => { + const args = ['--file']; + const options = { + file: { short: 'f', type: 'boolean' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false boolean short', () => { + const args = ['-f']; + const expectedTokens = [ + { kind: 'option', name: 'f', rawName: '-f', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false boolean long', () => { + const args = ['--file']; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false boolean option group', () => { + const args = ['-ab']; + const expectedTokens = [ + { kind: 'option', name: 'a', rawName: '-a', + index: 0, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'b', rawName: '-b', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true string short with value after space', () => { + // Also positional to check index correct after out-of-line. + const args = ['-f', 'bar', 'ppp']; + const options = { + file: { short: 'f', type: 'string' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '-f', + index: 0, value: 'bar', inlineValue: false }, + { kind: 'positional', index: 2, value: 'ppp' }, + ]; + const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true string short with value inline', () => { + const args = ['-fBAR']; + const options = { + file: { short: 'f', type: 'string' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '-f', + index: 0, value: 'BAR', inlineValue: true }, + ]; + const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false string short missing value', () => { + const args = ['-f']; + const options = { + file: { short: 'f', type: 'string' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '-f', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: false, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true string long with value after space', () => { + // Also positional to check index correct after out-of-line. + const args = ['--file', 'bar', 'ppp']; + const options = { + file: { short: 'f', type: 'string' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: 'bar', inlineValue: false }, + { kind: 'positional', index: 2, value: 'ppp' }, + ]; + const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true string long with value inline', () => { + const args = ['--file=bar']; + const options = { + file: { short: 'f', type: 'string' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: 'bar', inlineValue: true }, + ]; + const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false string long with value inline', () => { + const args = ['--file=bar']; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: 'bar', inlineValue: true }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false string long missing value', () => { + const args = ['--file']; + const options = { + file: { short: 'f', type: 'string' } + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: undefined, inlineValue: undefined }, + ]; + const { tokens } = parseArgs({ strict: false, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true complex option group with value after space', () => { + const args = ['-ab', 'c']; + const options = { + alpha: { short: 'a', type: 'boolean' }, + beta: { short: 'b', type: 'string' }, + }; + const expectedTokens = [ + { kind: 'option', name: 'alpha', rawName: '-a', + index: 0, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'beta', rawName: '-b', + index: 0, value: 'c', inlineValue: false }, + ]; + const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true complex option group with inline value', () => { + const args = ['-abc']; + const options = { + alpha: { short: 'a', type: 'boolean' }, + beta: { short: 'b', type: 'string' }, + }; + const expectedTokens = [ + { kind: 'option', name: 'alpha', rawName: '-a', + index: 0, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'beta', rawName: '-b', + index: 0, value: 'c', inlineValue: true }, + ]; + const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false variety', () => { + const args = ['-a', '1', '-bc', '2', '--ddd', '--eee=fff', '--', '3']; + const expectedTokens = [ + { kind: 'option', name: 'a', rawName: '-a', index: 0, value: undefined, inlineValue: undefined }, + { kind: 'positional', index: 1, value: '1' }, + { kind: 'option', name: 'b', rawName: '-b', index: 2, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'c', rawName: '-c', index: 2, value: undefined, inlineValue: undefined }, + { kind: 'positional', index: 3, value: '2' }, + { kind: 'option', name: 'ddd', rawName: '--ddd', index: 4, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'eee', rawName: '--eee', index: 5, value: 'fff', inlineValue: true }, + { kind: 'option-terminator', index: 6 }, + { kind: 'positional', index: 7, value: '3' }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:true variety', () => { + const args = ['-a', '1', '-bc', '-dDDD', '-e', 'EEE', '2', + '--fff=FFF', '--ggg', 'GGG', '--hhh', '--', '3']; + const options = { + alpha: { short: 'a', type: 'boolean' }, + beta: { short: 'b', type: 'boolean' }, + cat: { short: 'c', type: 'boolean' }, + delta: { short: 'd', type: 'string' }, + epsilon: { short: 'e', type: 'string' }, + fff: { type: 'string' }, + ggg: { type: 'string' }, + hhh: { type: 'boolean' }, + }; + const expectedTokens = [ + { kind: 'option', name: 'alpha', rawName: '-a', index: 0, value: undefined, inlineValue: undefined }, + { kind: 'positional', index: 1, value: '1' }, + { kind: 'option', name: 'beta', rawName: '-b', index: 2, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'cat', rawName: '-c', index: 2, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'delta', rawName: '-d', index: 3, value: 'DDD', inlineValue: true }, + { kind: 'option', name: 'epsilon', rawName: '-e', index: 4, value: 'EEE', inlineValue: false }, + { kind: 'positional', index: 6, value: '2' }, + { kind: 'option', name: 'fff', rawName: '--fff', index: 7, value: 'FFF', inlineValue: true }, + { kind: 'option', name: 'ggg', rawName: '--ggg', index: 8, value: 'GGG', inlineValue: false }, + { kind: 'option', name: 'hhh', rawName: '--hhh', index: 10, value: undefined, inlineValue: undefined }, + { kind: 'option-terminator', index: 11 }, + { kind: 'positional', index: 12, value: '3' }, + ]; + const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false with single dashes', () => { + const args = ['--file', '-', '-']; + const options = { + file: { short: 'f', type: 'string' }, + }; + const expectedTokens = [ + { kind: 'option', name: 'file', rawName: '--file', + index: 0, value: '-', inlineValue: false }, + { kind: 'positional', index: 2, value: '-' }, + ]; + const { tokens } = parseArgs({ strict: false, args, options, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + +test('tokens: strict:false with -- --', () => { + const args = ['--', '--']; + const expectedTokens = [ + { kind: 'option-terminator', index: 0 }, + { kind: 'positional', index: 1, value: '--' }, + ]; + const { tokens } = parseArgs({ strict: false, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); From ed54c30c9cf4f309162518e5e840e15f727d8765 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 08:50:22 +1200 Subject: [PATCH 02/13] Improve wording --- doc/api/util.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/util.md b/doc/api/util.md index d410896e34e8f5..da97defe3379b5 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1116,7 +1116,7 @@ properties describing: * `inlineValue` { boolean | undefined } Whether option value specified inline, like `--foo=bar`. * positional tokens - * `value` { string } Positional value (i.e. `args[index]`). + * `value` { string } the value of the positional argument in args (i.e. `args[index]`). * option-terminator token For example, assuming the following script which uses From fb2cd00dc457c7459615fd3babe018c55bc7daf3 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 09:51:56 +1200 Subject: [PATCH 03/13] Use negate as example for tokens. Rework description a little. --- doc/api/util.md | 126 ++++++++++++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index da97defe3379b5..06d00a521b58be 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1102,79 +1102,109 @@ console.log(values, positionals); ``` Detailed parse information is available for adding custom behaviours by -specifying `tokens: true` in the configuration. The returned tokens have +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. + * `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. +Undefined for boolean options. * `inlineValue` { boolean | undefined } Whether option value specified inline, - like `--foo=bar`. +like `--foo=bar`. * positional tokens - * `value` { string } the value of the positional argument in args (i.e. `args[index]`). + * `value` { string } The value of the positional argument in args (i.e. `args[index]`). * option-terminator token -For example, assuming the following script which uses -automatic detection of options and no error checking: +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'; -console.log(parseArgs({ strict: false, tokens: true })); + +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 const { parseArgs } = require('node:util'); -console.log(parseArgs({ strict: false, tokens: true })); + +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 }); ``` -This call shows the three kinds of token and their properties: +Example usage showing negated options, and when option use multiple times then last one wins. ```console -$ node tokens.cjs -xy --foo=BAR -- file.txt -{ - values: [Object: null prototype] { d: true, foo: 'BAR' }, - positionals: [ 'file.txt' ], - tokens: [ - { - kind: 'option', - name: 'x', - rawName: '-x', - index: 0, - value: undefined, - inlineValue: undefined - }, - { - kind: 'option', - name: 'y', - rawName: '-y', - index: 0, - value: undefined, - inlineValue: undefined - }, - { - kind: 'option', - name: 'foo', - rawName: '--foo', - index: 1, - value: 'BAR', - inlineValue: true - }, - { kind: 'option-terminator', index: 2 }, - { kind: 'positional', index: 3, value: 'file.txt' } - ] -} +$ 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 } ``` -The source argument for a token is `args[token.index]`. -Short option groups like `-xy` expand to a token for each option. -The `x` and `y` tokens above have the same index, since -they come from the same argument. - `util.parseArgs` is experimental and behavior may change. Join the conversation in [pkgjs/parseargs][] to contribute to the design. From 257004785fcbb1542a507f5c60cbb8c9be01b4e4 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 10:08:17 +1200 Subject: [PATCH 04/13] Lint and feedback --- doc/api/util.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 06d00a521b58be..9b0a8835d8ed78 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1108,34 +1108,36 @@ 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]`. + * `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. + Undefined for boolean options. * `inlineValue` { boolean | undefined } Whether option value specified inline, -like `--foo=bar`. + like `--foo=bar`. * positional tokens * `value` { string } The value of the positional argument in args (i.e. `args[index]`). * 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 +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. +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' }, + 'color': { type: 'boolean' }, + 'no-color': { type: 'boolean' }, + 'logfile': { type: 'string' }, + 'no-logfile': { type: 'boolean' }, }; const { values, tokens } = parseArgs({ options, tokens: true }); @@ -1164,10 +1166,10 @@ console.log({ logfile, color }); const { parseArgs } = require('node:util'); const options = { - ['color']: { type: 'boolean' }, - ['no-color']: { type: 'boolean' }, - ['logfile']: { type: 'string' }, - ['no-logfile']: { type: 'boolean' }, + 'color': { type: 'boolean' }, + 'no-color': { type: 'boolean' }, + 'logfile': { type: 'string' }, + 'no-logfile': { type: 'boolean' }, }; const { values, tokens } = parseArgs({ options, tokens: true }); @@ -1192,7 +1194,8 @@ const logfile = values.logfile ?? 'default.log'; console.log({ logfile, color }); ``` -Example usage showing negated options, and when option use multiple times then last one wins. +Example usage showing negated options, and when an option is used +multiple ways then last one wins. ```console $ node negate.js From 33dbe57bbdf6f291801b2958b6a92a509c60dff2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 19:05:01 +1200 Subject: [PATCH 05/13] Expand short token tests and remove unwieldy tests --- test/parallel/test-parse-args.mjs | 78 ++++++++++--------------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 1c49dcba770cab..98cf9403743a41 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -656,6 +656,20 @@ test('tokens: strict:false boolean option group', () => { assert.deepStrictEqual(tokens, expectedTokens); }); +test('tokens: strict:false boolean option group with repeated option', () => { + // Also positional to check index correct after grouop + const args = ['-aa', 'pos']; + const expectedTokens = [ + { kind: 'option', name: 'a', rawName: '-a', + index: 0, value: undefined, inlineValue: undefined }, + { kind: 'option', name: 'a', rawName: '-a', + index: 0, value: undefined, inlineValue: undefined }, + { kind: 'positional', index: 1, value: 'pos' }, + ]; + const { tokens } = parseArgs({ strict: false, allowPositionals: true, args, tokens: true }); + assert.deepStrictEqual(tokens, expectedTokens); +}); + test('tokens: strict:true string short with value after space', () => { // Also positional to check index correct after out-of-line. const args = ['-f', 'bar', 'ppp']; @@ -713,15 +727,17 @@ test('tokens: strict:true string long with value after space', () => { }); test('tokens: strict:true string long with value inline', () => { - const args = ['--file=bar']; + // Also positional to check index correct after out-of-line. + const args = ['--file=bar', 'pos']; const options = { file: { short: 'f', type: 'string' } }; const expectedTokens = [ { kind: 'option', name: 'file', rawName: '--file', index: 0, value: 'bar', inlineValue: true }, + { kind: 'positional', index: 1, value: 'pos' }, ]; - const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); assert.deepStrictEqual(tokens, expectedTokens); }); @@ -749,7 +765,8 @@ test('tokens: strict:false string long missing value', () => { }); test('tokens: strict:true complex option group with value after space', () => { - const args = ['-ab', 'c']; + // Also positional to check index correct afterwards. + const args = ['-ab', 'c', 'pos']; const options = { alpha: { short: 'a', type: 'boolean' }, beta: { short: 'b', type: 'string' }, @@ -759,13 +776,15 @@ test('tokens: strict:true complex option group with value after space', () => { index: 0, value: undefined, inlineValue: undefined }, { kind: 'option', name: 'beta', rawName: '-b', index: 0, value: 'c', inlineValue: false }, + { kind: 'positional', index: 2, value: 'pos' }, ]; - const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); + const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); assert.deepStrictEqual(tokens, expectedTokens); }); test('tokens: strict:true complex option group with inline value', () => { - const args = ['-abc']; + // Also positional to check index correct afterwards. + const args = ['-abc', 'pos']; const options = { alpha: { short: 'a', type: 'boolean' }, beta: { short: 'b', type: 'string' }, @@ -775,54 +794,7 @@ test('tokens: strict:true complex option group with inline value', () => { index: 0, value: undefined, inlineValue: undefined }, { kind: 'option', name: 'beta', rawName: '-b', index: 0, value: 'c', inlineValue: true }, - ]; - const { tokens } = parseArgs({ strict: true, args, options, tokens: true }); - assert.deepStrictEqual(tokens, expectedTokens); -}); - -test('tokens: strict:false variety', () => { - const args = ['-a', '1', '-bc', '2', '--ddd', '--eee=fff', '--', '3']; - const expectedTokens = [ - { kind: 'option', name: 'a', rawName: '-a', index: 0, value: undefined, inlineValue: undefined }, - { kind: 'positional', index: 1, value: '1' }, - { kind: 'option', name: 'b', rawName: '-b', index: 2, value: undefined, inlineValue: undefined }, - { kind: 'option', name: 'c', rawName: '-c', index: 2, value: undefined, inlineValue: undefined }, - { kind: 'positional', index: 3, value: '2' }, - { kind: 'option', name: 'ddd', rawName: '--ddd', index: 4, value: undefined, inlineValue: undefined }, - { kind: 'option', name: 'eee', rawName: '--eee', index: 5, value: 'fff', inlineValue: true }, - { kind: 'option-terminator', index: 6 }, - { kind: 'positional', index: 7, value: '3' }, - ]; - const { tokens } = parseArgs({ strict: false, args, tokens: true }); - assert.deepStrictEqual(tokens, expectedTokens); -}); - -test('tokens: strict:true variety', () => { - const args = ['-a', '1', '-bc', '-dDDD', '-e', 'EEE', '2', - '--fff=FFF', '--ggg', 'GGG', '--hhh', '--', '3']; - const options = { - alpha: { short: 'a', type: 'boolean' }, - beta: { short: 'b', type: 'boolean' }, - cat: { short: 'c', type: 'boolean' }, - delta: { short: 'd', type: 'string' }, - epsilon: { short: 'e', type: 'string' }, - fff: { type: 'string' }, - ggg: { type: 'string' }, - hhh: { type: 'boolean' }, - }; - const expectedTokens = [ - { kind: 'option', name: 'alpha', rawName: '-a', index: 0, value: undefined, inlineValue: undefined }, - { kind: 'positional', index: 1, value: '1' }, - { kind: 'option', name: 'beta', rawName: '-b', index: 2, value: undefined, inlineValue: undefined }, - { kind: 'option', name: 'cat', rawName: '-c', index: 2, value: undefined, inlineValue: undefined }, - { kind: 'option', name: 'delta', rawName: '-d', index: 3, value: 'DDD', inlineValue: true }, - { kind: 'option', name: 'epsilon', rawName: '-e', index: 4, value: 'EEE', inlineValue: false }, - { kind: 'positional', index: 6, value: '2' }, - { kind: 'option', name: 'fff', rawName: '--fff', index: 7, value: 'FFF', inlineValue: true }, - { kind: 'option', name: 'ggg', rawName: '--ggg', index: 8, value: 'GGG', inlineValue: false }, - { kind: 'option', name: 'hhh', rawName: '--hhh', index: 10, value: undefined, inlineValue: undefined }, - { kind: 'option-terminator', index: 11 }, - { kind: 'positional', index: 12, value: '3' }, + { kind: 'positional', index: 1, value: 'pos' }, ]; const { tokens } = parseArgs({ strict: true, allowPositionals: true, args, options, tokens: true }); assert.deepStrictEqual(tokens, expectedTokens); From 27d9e4d6ee1bdd1351bdaa91b1251586e8bd0464 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 21:13:18 +1200 Subject: [PATCH 06/13] Add suggested change Co-authored-by: Antoine du Hamel --- lib/internal/util/parse_args/parse_args.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 32a7681ab0f96e..59742cc13bd1ca 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -264,7 +264,7 @@ function argsToTokens(args, options) { return tokens; } -const parseArgs = (config = { __proto__: null }) => { +const parseArgs = (config = kEmptyObject) => { const args = objectGetOwn(config, 'args') ?? getMainArgs(); const strict = objectGetOwn(config, 'strict') ?? true; const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; From 454bd33ae72b43251198f256dd86cf6ee22a8874 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 21:22:14 +1200 Subject: [PATCH 07/13] Add missing require --- lib/internal/util/parse_args/parse_args.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 59742cc13bd1ca..05f4c6cffbdbd7 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -47,6 +47,11 @@ const { }, } = require('internal/errors'); +const { + kEmptyObject, +} = require('internal/util'); + + function getMainArgs() { // Work out where to slice process.argv for user supplied arguments. From baccf9b3b5e9b6f55a4d6d552fd8b9c0b4c9f6b5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 21:47:44 +1200 Subject: [PATCH 08/13] Add documentation for tokens to config docs --- doc/api/util.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 9b0a8835d8ed78..7c8d24de2fb1a6 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1044,18 +1044,25 @@ added: v18.3.0 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 behaviour, + 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} [tokens] Detailed parse information (see below for details). + 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 From d51dcb72ec9f14fe83018e95f54beeedec4c9ecf Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 5 Jul 2022 22:05:59 +1200 Subject: [PATCH 09/13] Add version changes to YAML. --- doc/api/util.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 7c8d24de2fb1a6..b903f3552f123e 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1028,6 +1028,11 @@ equality. > Stability: 1 - Experimental @@ -1061,8 +1066,8 @@ added: v18.3.0 * `values` {Object} A mapping of parsed option names with their {string} or {boolean} values. * `positionals` {string\[]} Positional arguments. - * `tokens` {Object} [tokens] Detailed parse information (see below for details). - Only returned if `config` includes `tokens: true`. + * `tokens` {Object} Array of parsed tokens with detailed information + (see below for more). 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 From f8bc77d9224993f2c1dab03583bd76a230c6a0e2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 6 Jul 2022 09:09:15 +1200 Subject: [PATCH 10/13] Apply suggestions from code review Co-authored-by: Antoine du Hamel --- doc/api/util.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index b903f3552f123e..b5c3e225bc42f0 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1056,18 +1056,16 @@ changes: * `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 behaviour, - from adding additional checks through to reprocessing the tokens - in different ways. + * `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} Array of parsed tokens with detailed information - (see below for more). Only returned if `config` includes `tokens: true`. + * `tokens` {Object\[]|undefined} See [`parseArgs`' `tokens`][] section. Provides a higher level API for command-line argument parsing than interacting with `process.argv` directly. Takes a specification for the expected arguments @@ -1113,10 +1111,11 @@ console.log(values, positionals); // Prints: [Object: null prototype] { foo: true, bar: 'b' } [] ``` +### `parseArgs`' `tokens` + Detailed parse information is available for adding custom behaviours by specifying `tokens: true` in the configuration. -The returned tokens have -properties describing: +The returned tokens have properties describing: * all tokens * `kind` { string } One of 'option', 'positional', or 'option-terminator'. From 0bd9da63c6a436f93b3beb6c35d0044ee4b245cb Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 6 Jul 2022 09:23:39 +1200 Subject: [PATCH 11/13] Fix reference --- doc/api/util.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index b5c3e225bc42f0..50169435455b24 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1065,7 +1065,7 @@ changes: * `values` {Object} A mapping of parsed option names with their {string} or {boolean} values. * `positionals` {string\[]} Positional arguments. - * `tokens` {Object\[]|undefined} See [`parseArgs`' `tokens`][] section. + * `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens) section. Provides a higher level API for command-line argument parsing than interacting with `process.argv` directly. Takes a specification for the expected arguments @@ -1111,7 +1111,7 @@ console.log(values, positionals); // Prints: [Object: null prototype] { foo: true, bar: 'b' } [] ``` -### `parseArgs`' `tokens` +### `parseArgs` `tokens` Detailed parse information is available for adding custom behaviours by specifying `tokens: true` in the configuration. From 25cb98c2d64b2e94e64f5aa46540cc376f64588b Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 6 Jul 2022 09:31:26 +1200 Subject: [PATCH 12/13] Whitespace and other minor improvements to documentstion --- doc/api/util.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 50169435455b24..5e4565df590efe 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1066,6 +1066,7 @@ changes: 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 @@ -1111,6 +1112,9 @@ console.log(values, positionals); // 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 @@ -1118,18 +1122,18 @@ 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 + * `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. + * `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, + * `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]`). + * `value` {string} The value of the positional argument in args (i.e. `args[index]`). * option-terminator token The returned tokens are in the order encountered in the input args. Options @@ -1219,9 +1223,6 @@ $ node negate.js --no-logfile --logfile=test.log --color --no-color { logfile: 'test.log', color: false } ``` -`util.parseArgs` is experimental and behavior may change. Join the -conversation in [pkgjs/parseargs][] to contribute to the design. - ## `util.promisify(original)`