diff options
author | Yoshiya Hinosawa <stibium121@gmail.com> | 2023-11-29 15:42:58 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-29 15:42:58 +0900 |
commit | e332fa4a83d36605771844745738c001f276b390 (patch) | |
tree | 9d21beacdcad06bb49cc31539ed3d8374e27506f /ext/node/polyfills/internal/util/parse_args/parse_args.js | |
parent | 75ec650f080ac66e98d8b848118dc2349ca70aa8 (diff) |
fix(ext/node): add util.parseArgs (#21342)
Diffstat (limited to 'ext/node/polyfills/internal/util/parse_args/parse_args.js')
-rw-r--r-- | ext/node/polyfills/internal/util/parse_args/parse_args.js | 345 |
1 files changed, 345 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; +}; |