123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- '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} <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,
- };
|