UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

449 lines (414 loc) 14.1 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/util/parse_args/parse_args.js import { validateArray, validateBoolean, validateBooleanArray, validateObject, validateString, validateStringArray, validateUnion, } from "nstdlib/lib/internal/validators"; import { findLongOptionForShort, isLoneLongOption, isLoneShortOption, isLongOptionAndValue, isOptionValue, isOptionLikeValue, isShortOptionAndValue, isShortOptionGroup, useDefaultValueOption, objectGetOwn, optionsGetOwn, } from "nstdlib/lib/internal/util/parse_args/utils"; import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { kEmptyObject } from "nstdlib/stub/internal/util/parse_args/.."; const { ERR_INVALID_ARG_VALUE, ERR_PARSE_ARGS_INVALID_OPTION_VALUE, ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, ERR_PARSE_ARGS_UNKNOWN_OPTION, } = __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 ( Array.prototype.includes.call(execArgv, "-e") || Array.prototype.includes.call(execArgv, "--eval") || Array.prototype.includes.call(execArgv, "-p") || Array.prototype.includes.call(execArgv, "--print") ) { return Array.prototype.slice.call(process.argv, 1); } // Normally first two arguments are executable and script, then CLI arguments return Array.prototype.slice.call(process.argv, 2); } /** * In strict mode, throw for possible usage errors like --foo --bar * @param {object} token - from tokens as available from parseArgs */ function checkOptionLikeValue(token) { if (!token.inlineValue && isOptionLikeValue(token.value)) { // Only show short example if user used short option. const example = String.prototype.startsWith.call(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); } } /** * In strict mode, throw for usage errors. * @param {object} config - from config passed to parseArgs * @param {object} token - from tokens as available from parseArgs */ function checkOptionUsage(config, token) { let tokenName = token.name; if (!Object.hasOwn(config.options, tokenName)) { // Check for negated boolean option. if ( config.allowNegative && String.prototype.startsWith.call(tokenName, "no-") ) { tokenName = String.prototype.slice.call(tokenName, 3); if ( !Object.hasOwn(config.options, tokenName) || optionsGetOwn(config.options, tokenName, "type") !== "boolean" ) { throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( token.rawName, config.allowPositionals, ); } } else { throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( token.rawName, config.allowPositionals, ); } } const short = optionsGetOwn(config.options, tokenName, "short"); const shortAndLong = `${short ? `-${short}, ` : ""}--${tokenName}`; const type = optionsGetOwn(config.options, tokenName, "type"); if (type === "string" && typeof token.value !== "string") { throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE( `Option '${shortAndLong} <value>' argument missing`, ); } // (Idiomatic test for undefined||null, expecting undefined.) 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`. * @param {object} token - from tokens as available from parseArgs * @param {object} options - option configs, from parseArgs({ options }) * @param {object} values - option values returned in `values` by parseArgs * @param {boolean} allowNegative - allow negative optinons if true */ function storeOption(token, options, values, allowNegative) { let longOption = token.name; let optionValue = token.value; if (longOption === "__proto__") { return; // No. Just no. } if ( allowNegative && String.prototype.startsWith.call(longOption, "no-") && optionValue === undefined ) { // Boolean option negation: --no-foo longOption = String.prototype.slice.call(longOption, 3); token.name = longOption; optionValue = false; } // 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]) { Array.prototype.push.call(values[longOption], newValue); } else { values[longOption] = [newValue]; } } else { values[longOption] = newValue; } } /** * Store the default option value in `values`. * @param {string} longOption - long option name e.g. 'foo' * @param {string * | boolean * | string[] * | boolean[]} optionValue - default value from option config * @param {object} values - option values returned in `values` by parseArgs */ function storeDefaultOption(longOption, optionValue, values) { if (longOption === "__proto__") { return; // No. Just no. } values[longOption] = optionValue; } /** * 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 = Array.prototype.slice.call(args); while (remainingArgs.length > 0) { const arg = Array.prototype.shift.call(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 === "--") { // Everything after a bare '--' is considered a positional argument. Array.prototype.push.call(tokens, { kind: "option-terminator", index }); Array.prototype.push.apply( tokens, Array.prototype.map.call(remainingArgs, (arg) => { return { kind: "positional", index: ++index, value: arg }; }), ); break; // Finished processing args, leave while loop. } if (isLoneShortOption(arg)) { // e.g. '-f' const shortOption = String.prototype.charAt.call(arg, 1); const longOption = findLongOptionForShort(shortOption, options); let value; let inlineValue; if ( optionsGetOwn(options, longOption, "type") === "string" && isOptionValue(nextArg) ) { // e.g. '-f', 'bar' value = Array.prototype.shift.call(remainingArgs); inlineValue = false; } Array.prototype.push.call(tokens, { kind: "option", name: longOption, rawName: arg, index, value, inlineValue, }); if (value != null) ++index; continue; } if (isShortOptionGroup(arg, options)) { // Expand -fXzy to -f -X -z -y const expanded = []; for (let index = 1; index < arg.length; index++) { const shortOption = String.prototype.charAt.call(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. Array.prototype.push.call(expanded, `-${shortOption}`); } else { // String option in middle. Yuck. // Expand -abfFILE to -a -b -fFILE Array.prototype.push.call( expanded, `-${String.prototype.slice.call(arg, index)}`, ); break; // finished short group } } Array.prototype.unshift.apply(remainingArgs, expanded); groupCount = expanded.length; continue; } if (isShortOptionAndValue(arg, options)) { // e.g. -fFILE const shortOption = String.prototype.charAt.call(arg, 1); const longOption = findLongOptionForShort(shortOption, options); const value = String.prototype.slice.call(arg, 2); Array.prototype.push.call(tokens, { kind: "option", name: longOption, rawName: `-${shortOption}`, index, value, inlineValue: true, }); continue; } if (isLoneLongOption(arg)) { // e.g. '--foo' const longOption = String.prototype.slice.call(arg, 2); let value; let inlineValue; if ( optionsGetOwn(options, longOption, "type") === "string" && isOptionValue(nextArg) ) { // e.g. '--foo', 'bar' value = Array.prototype.shift.call(remainingArgs); inlineValue = false; } Array.prototype.push.call(tokens, { kind: "option", name: longOption, rawName: arg, index, value, inlineValue, }); if (value != null) ++index; continue; } if (isLongOptionAndValue(arg)) { // e.g. --foo=bar const equalIndex = String.prototype.indexOf.call(arg, "="); const longOption = String.prototype.slice.call(arg, 2, equalIndex); const value = String.prototype.slice.call(arg, equalIndex + 1); Array.prototype.push.call(tokens, { kind: "option", name: longOption, rawName: `--${longOption}`, index, value, inlineValue: true, }); continue; } Array.prototype.push.call(tokens, { kind: "positional", index, value: arg, }); } return tokens; } const parseArgs = (config = kEmptyObject) => { 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 allowNegative = objectGetOwn(config, "allowNegative") ?? false; const options = objectGetOwn(config, "options") ?? { __proto__: null }; // Bundle these up for passing to strict-mode checks. const parseConfig = { args, strict, options, allowPositionals, allowNegative, }; // Validate input configuration. validateArray(args, "args"); validateBoolean(strict, "strict"); validateBoolean(allowPositionals, "allowPositionals"); validateBoolean(returnTokens, "tokens"); validateBoolean(allowNegative, "allowNegative"); validateObject(options, "options"); Array.prototype.forEach.call( Object.entries(options), ({ 0: longOption, 1: optionConfig }) => { validateObject(optionConfig, `options.${longOption}`); // type is required const optionType = objectGetOwn(optionConfig, "type"); validateUnion(optionType, `options.${longOption}.type`, [ "string", "boolean", ]); if (Object.hasOwn(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", ); } } const multipleOption = objectGetOwn(optionConfig, "multiple"); if (Object.hasOwn(optionConfig, "multiple")) { validateBoolean(multipleOption, `options.${longOption}.multiple`); } const defaultValue = objectGetOwn(optionConfig, "default"); if (defaultValue !== undefined) { let validator; switch (optionType) { case "string": validator = multipleOption ? validateStringArray : validateString; break; case "boolean": validator = multipleOption ? validateBooleanArray : validateBoolean; break; } validator(defaultValue, `options.${longOption}.default`); } }, ); // Phase 1: identify tokens const tokens = argsToTokens(args, options); // Phase 2: process tokens into parsed option values and positionals const result = { values: { __proto__: null }, positionals: [], }; if (returnTokens) { result.tokens = tokens; } Array.prototype.forEach.call(tokens, (token) => { if (token.kind === "option") { if (strict) { checkOptionUsage(parseConfig, token); checkOptionLikeValue(token); } storeOption(token, options, result.values, parseConfig.allowNegative); } else if (token.kind === "positional") { if (!allowPositionals) { throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value); } Array.prototype.push.call(result.positionals, token.value); } }); // Phase 3: fill in default values for missing args Array.prototype.forEach.call( Object.entries(options), ({ 0: longOption, 1: optionConfig }) => { const mustSetDefault = useDefaultValueOption( longOption, optionConfig, result.values, ); if (mustSetDefault) { storeDefaultOption( longOption, objectGetOwn(optionConfig, "default"), result.values, ); } }, ); return result; }; export { parseArgs };