'use strict'; const { ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypeUnshiftApply, ObjectEntries, ObjectPrototypeHasOwnProperty: ObjectHasOwn, StringPrototypeCharAt, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeStartsWith, } = require('./internal/primordials'); const { validateArray, validateBoolean, validateBooleanArray, validateObject, validateString, validateStringArray, validateUnion, } = require('./internal/validators'); const { kEmptyObject, } = require('./internal/util'); const { findLongOptionForShort, isLoneLongOption, isLoneShortOption, isLongOptionAndValue, isOptionValue, isOptionLikeValue, isShortOptionAndValue, isShortOptionGroup, useDefaultValueOption, objectGetOwn, optionsGetOwn, } = require('./utils'); const { codes: { ERR_INVALID_ARG_VALUE, ERR_PARSE_ARGS_INVALID_OPTION_VALUE, ERR_PARSE_ARGS_UNKNOWN_OPTION, ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, }, } = require('./internal/errors'); 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 {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 = 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); } } /** * 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) { if (!ObjectHasOwn(config.options, token.name)) { throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( token.rawName, config.allowPositionals); } 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' && 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 {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; } } /** * 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 = 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 === '--') { // Everything after a bare '--' is considered a positional argument. ArrayPrototypePush(tokens, { kind: 'option-terminator', index }); ArrayPrototypePushApply( tokens, ArrayPrototypeMap(remainingArgs, (arg) => { return { kind: 'positional', index: ++index, value: arg }; }) ); break; // Finished processing args, leave while loop. } if (isLoneShortOption(arg)) { // e.g. '-f' const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); let value; let inlineValue; if (optionsGetOwn(options, longOption, 'type') === 'string' && isOptionValue(nextArg)) { // e.g. '-f', 'bar' value = ArrayPrototypeShift(remainingArgs); inlineValue = false; } ArrayPrototypePush( 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 = 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); groupCount = expanded.length; continue; } if (isShortOptionAndValue(arg, options)) { // e.g. -fFILE const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); 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 value; let inlineValue; if (optionsGetOwn(options, longOption, 'type') === 'string' && isOptionValue(nextArg)) { // e.g. '--foo', 'bar' value = ArrayPrototypeShift(remainingArgs); inlineValue = false; } ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: arg, index, value, inlineValue }); if (value != null) ++index; continue; } if (isLongOptionAndValue(arg)) { // e.g. --foo=bar 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; } ArrayPrototypePush(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 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 const optionType = objectGetOwn(optionConfig, 'type'); validateUnion(optionType, `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' ); } } const multipleOption = objectGetOwn(optionConfig, 'multiple'); if (ObjectHasOwn(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; } 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); } }); // Phase 3: fill in default values for missing args ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { const mustSetDefault = useDefaultValueOption(longOption, optionConfig, result.values); if (mustSetDefault) { storeDefaultOption(longOption, objectGetOwn(optionConfig, 'default'), result.values); } }); return result; }; module.exports = { parseArgs, };