Spaces:
Sleeping
Sleeping
; | |
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} <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 {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, | |
}; | |