diff options
Diffstat (limited to 'ext/node/polyfills/internal/util')
-rw-r--r-- | ext/node/polyfills/internal/util/parse_args/parse_args.js | 345 | ||||
-rw-r--r-- | ext/node/polyfills/internal/util/parse_args/utils.js | 187 |
2 files changed, 532 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal/util/parse_args/parse_args.js b/ext/node/polyfills/internal/util/parse_args/parse_args.js new file mode 100644 index 000000000..798039620 --- /dev/null +++ b/ext/node/polyfills/internal/util/parse_args/parse_args.js @@ -0,0 +1,345 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + ArrayPrototypePushApply, + ArrayPrototypeShift, + ArrayPrototypeSlice, + ArrayPrototypePush, + ArrayPrototypeUnshiftApply, + ObjectHasOwn, + ObjectEntries, + StringPrototypeCharAt, + StringPrototypeIndexOf, + StringPrototypeSlice, +} = primordials; + +import { + validateArray, + validateBoolean, + validateObject, + validateString, + validateUnion, +} from "ext:deno_node/internal/validators.mjs"; + +import { + findLongOptionForShort, + isLoneLongOption, + isLoneShortOption, + isLongOptionAndValue, + isOptionLikeValue, + isOptionValue, + isShortOptionAndValue, + isShortOptionGroup, + objectGetOwn, + optionsGetOwn, +} from "ext:deno_node/internal/util/parse_args/utils.js"; + +import { codes } from "ext:deno_node/internal/error_codes.ts"; +const { + ERR_INVALID_ARG_VALUE, + ERR_PARSE_ARGS_INVALID_OPTION_VALUE, + ERR_PARSE_ARGS_UNKNOWN_OPTION, + ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, +} = codes; + +function getMainArgs() { + // Work out where to slice process.argv for user supplied arguments. + + // Check node options for scenarios where user CLI args follow executable. + const execArgv = process.execArgv; + if ( + ArrayPrototypeIncludes(execArgv, "-e") || + ArrayPrototypeIncludes(execArgv, "--eval") || + ArrayPrototypeIncludes(execArgv, "-p") || + ArrayPrototypeIncludes(execArgv, "--print") + ) { + return ArrayPrototypeSlice(process.argv, 1); + } + + // Normally first two arguments are executable and script, then CLI arguments + return ArrayPrototypeSlice(process.argv, 2); +} + +/** + * 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 }) + */ +function checkOptionLikeValue(longOption, optionValue, shortOrLong, strict) { + if (strict && isOptionLikeValue(optionValue)) { + // 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}'? +To specify an option argument starting with a dash use ${example}.`; + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage); + } +} + +/** + * 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 }) + */ +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); + } + + const short = optionsGetOwn(options, longOption, "short"); + const shortAndLong = short ? `-${short}, --${longOption}` : `--${longOption}`; + const type = optionsGetOwn(options, longOption, "type"); + if (type === "string" && typeof optionValue !== "string") { + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE( + `Option '${shortAndLong} <value>' argument missing`, + ); + } + // (Idiomatic test for undefined||null, expecting undefined.) + if (type === "boolean" && optionValue != null) { + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE( + `Option '${shortAndLong}' does not take an argument`, + ); + } +} + +/** + * Store the option value in `values`. + * + * @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 {object} values - option values returned in `values` by parseArgs + */ +function storeOption(longOption, optionValue, options, values) { + if (longOption === "__proto__") { + return; // No. Just no. + } + + // We store based on the option value rather than option type, + // preserving the users intent for author to deal with. + const newValue = optionValue ?? true; + if (optionsGetOwn(options, longOption, "multiple")) { + // Always store value in array, including for boolean. + // values[longOption] starts out not present, + // first value is added as new array [newValue], + // subsequent values are pushed to existing array. + // (note: values has null prototype, so simpler usage) + if (values[longOption]) { + ArrayPrototypePush(values[longOption], newValue); + } else { + values[longOption] = [newValue]; + } + } else { + values[longOption] = newValue; + } +} + +export 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: [], + }; + + const remainingArgs = ArrayPrototypeSlice(args); + while (remainingArgs.length > 0) { + const arg = ArrayPrototypeShift(remainingArgs); + const nextArg = remainingArgs[0]; + + // 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. + ArrayPrototypePushApply( + result.positionals, + remainingArgs, + ); + break; // Finished processing args, leave while loop. + } + + if (isLoneShortOption(arg)) { + // e.g. '-f' + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + let optionValue; + if ( + optionsGetOwn(options, longOption, "type") === "string" && + isOptionValue(nextArg) + ) { + // e.g. '-f', 'bar' + optionValue = ArrayPrototypeShift(remainingArgs); + checkOptionLikeValue(longOption, optionValue, arg, strict); + } + checkOptionUsage( + longOption, + optionValue, + options, + arg, + strict, + allowPositionals, + ); + storeOption(longOption, optionValue, options, result.values); + continue; + } + + if (isShortOptionGroup(arg, options)) { + // Expand -fXzy to -f -X -z -y + const expanded = []; + for (let index = 1; index < arg.length; index++) { + const shortOption = StringPrototypeCharAt(arg, index); + const longOption = findLongOptionForShort(shortOption, options); + if ( + optionsGetOwn(options, longOption, "type") !== "string" || + index === arg.length - 1 + ) { + // Boolean option, or last short in group. Well formed. + ArrayPrototypePush(expanded, `-${shortOption}`); + } else { + // String option in middle. Yuck. + // Expand -abfFILE to -a -b -fFILE + ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`); + break; // finished short group + } + } + ArrayPrototypeUnshiftApply(remainingArgs, expanded); + continue; + } + + if (isShortOptionAndValue(arg, options)) { + // 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); + continue; + } + + if (isLoneLongOption(arg)) { + // e.g. '--foo' + const longOption = StringPrototypeSlice(arg, 2); + let optionValue; + if ( + optionsGetOwn(options, longOption, "type") === "string" && + isOptionValue(nextArg) + ) { + // e.g. '--foo', 'bar' + optionValue = ArrayPrototypeShift(remainingArgs); + checkOptionLikeValue(longOption, optionValue, arg, strict); + } + checkOptionUsage( + longOption, + optionValue, + options, + arg, + strict, + allowPositionals, + ); + storeOption(longOption, optionValue, options, result.values); + 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); + continue; + } + + // Anything left is a positional + if (!allowPositionals) { + throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(arg); + } + + ArrayPrototypePush(result.positionals, arg); + } + + return result; +}; diff --git a/ext/node/polyfills/internal/util/parse_args/utils.js b/ext/node/polyfills/internal/util/parse_args/utils.js new file mode 100644 index 000000000..d101dd3fd --- /dev/null +++ b/ext/node/polyfills/internal/util/parse_args/utils.js @@ -0,0 +1,187 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypeFind, + ObjectEntries, + ObjectHasOwn, + StringPrototypeCharAt, + StringPrototypeIncludes, + StringPrototypeStartsWith, +} = primordials; + +import { validateObject } from "ext:deno_node/internal/validators.mjs"; + +// These are internal utilities to make the parsing logic easier to read, and +// add lots of detail for the curious. They are in a separate file to allow +// unit testing, although that is not essential (this could be rolled into +// main file and just tested implicitly via API). +// +// These routines are for internal use, not for export to client. + +/** + * Return the named property, but only if it is an own property. + */ +function objectGetOwn(obj, prop) { + if (ObjectHasOwn(obj, prop)) { + return obj[prop]; + } +} + +/** + * Return the named options property, but only if it is an own property. + */ +function optionsGetOwn(options, longOption, prop) { + if (ObjectHasOwn(options, longOption)) { + return objectGetOwn(options[longOption], prop); + } +} + +/** + * Determines if the argument may be used as an option value. + * @example + * isOptionValue('V') // returns true + * isOptionValue('-v') // returns true (greedy) + * isOptionValue('--foo') // returns true (greedy) + * isOptionValue(undefined) // returns false + */ +function isOptionValue(value) { + if (value == null) return false; + + // Open Group Utility Conventions are that an option-argument + // is the argument after the option, and may start with a dash. + return true; // greedy! +} + +/** + * Detect whether there is possible confusion and user may have omitted + * the option argument, like `--port --verbose` when `port` of type:string. + * In strict mode we throw errors if value is option-like. + */ +function isOptionLikeValue(value) { + if (value == null) return false; + + return value.length > 1 && StringPrototypeCharAt(value, 0) === "-"; +} + +/** + * Determines if `arg` is just a short option. + * @example '-f' + */ +function isLoneShortOption(arg) { + return arg.length === 2 && + StringPrototypeCharAt(arg, 0) === "-" && + StringPrototypeCharAt(arg, 1) !== "-"; +} + +/** + * Determines if `arg` is a lone long option. + * @example + * isLoneLongOption('a') // returns false + * isLoneLongOption('-a') // returns false + * isLoneLongOption('--foo') // returns true + * isLoneLongOption('--foo=bar') // returns false + */ +function isLoneLongOption(arg) { + return arg.length > 2 && + StringPrototypeStartsWith(arg, "--") && + !StringPrototypeIncludes(arg, "=", 3); +} + +/** + * Determines if `arg` is a long option and value in the same argument. + * @example + * isLongOptionAndValue('--foo') // returns false + * isLongOptionAndValue('--foo=bar') // returns true + */ +function isLongOptionAndValue(arg) { + return arg.length > 2 && + StringPrototypeStartsWith(arg, "--") && + StringPrototypeIncludes(arg, "=", 3); +} + +/** + * Determines if `arg` is a short option group. + * + * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). + * One or more options without option-arguments, followed by at most one + * option that takes an option-argument, should be accepted when grouped + * behind one '-' delimiter. + * @example + * isShortOptionGroup('-a', {}) // returns false + * isShortOptionGroup('-ab', {}) // returns true + * // -fb is an option and a value, not a short option group + * isShortOptionGroup('-fb', { + * options: { f: { type: 'string' } } + * }) // returns false + * isShortOptionGroup('-bf', { + * options: { f: { type: 'string' } } + * }) // returns true + * // -bfb is an edge case, return true and caller sorts it out + * isShortOptionGroup('-bfb', { + * options: { f: { type: 'string' } } + * }) // returns true + */ +function isShortOptionGroup(arg, options) { + if (arg.length <= 2) return false; + if (StringPrototypeCharAt(arg, 0) !== "-") return false; + if (StringPrototypeCharAt(arg, 1) === "-") return false; + + const firstShort = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(firstShort, options); + return optionsGetOwn(options, longOption, "type") !== "string"; +} + +/** + * Determine if arg is a short string option followed by its value. + * @example + * isShortOptionAndValue('-a', {}); // returns false + * isShortOptionAndValue('-ab', {}); // returns false + * isShortOptionAndValue('-fFILE', { + * options: { foo: { short: 'f', type: 'string' }} + * }) // returns true + */ +function isShortOptionAndValue(arg, options) { + validateObject(options, "options"); + + if (arg.length <= 2) return false; + if (StringPrototypeCharAt(arg, 0) !== "-") return false; + if (StringPrototypeCharAt(arg, 1) === "-") return false; + + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + return optionsGetOwn(options, longOption, "type") === "string"; +} + +/** + * Find the long option associated with a short option. Looks for a configured + * `short` and returns the short option itself if a long option is not found. + * @example + * findLongOptionForShort('a', {}) // returns 'a' + * findLongOptionForShort('b', { + * options: { bar: { short: 'b' } } + * }) // returns 'bar' + */ +function findLongOptionForShort(shortOption, options) { + validateObject(options, "options"); + const longOptionEntry = ArrayPrototypeFind( + ObjectEntries(options), + ({ 1: optionConfig }) => + objectGetOwn(optionConfig, "short") === shortOption, + ); + return longOptionEntry?.[0] ?? shortOption; +} + +export { + findLongOptionForShort, + isLoneLongOption, + isLoneShortOption, + isLongOptionAndValue, + isOptionLikeValue, + isOptionValue, + isShortOptionAndValue, + isShortOptionGroup, + objectGetOwn, + optionsGetOwn, +}; |