1'use strict';
2
3const {
4  ArrayPrototypeForEach,
5  ArrayPrototypeIncludes,
6  ArrayPrototypeMap,
7  ArrayPrototypePush,
8  ArrayPrototypePushApply,
9  ArrayPrototypeShift,
10  ArrayPrototypeSlice,
11  ArrayPrototypeUnshiftApply,
12  ObjectEntries,
13  ObjectPrototypeHasOwnProperty: ObjectHasOwn,
14  StringPrototypeCharAt,
15  StringPrototypeIndexOf,
16  StringPrototypeSlice,
17  StringPrototypeStartsWith,
18} = require('./internal/primordials');
19
20const {
21  validateArray,
22  validateBoolean,
23  validateBooleanArray,
24  validateObject,
25  validateString,
26  validateStringArray,
27  validateUnion,
28} = require('./internal/validators');
29
30const {
31  kEmptyObject,
32} = require('./internal/util');
33
34const {
35  findLongOptionForShort,
36  isLoneLongOption,
37  isLoneShortOption,
38  isLongOptionAndValue,
39  isOptionValue,
40  isOptionLikeValue,
41  isShortOptionAndValue,
42  isShortOptionGroup,
43  useDefaultValueOption,
44  objectGetOwn,
45  optionsGetOwn,
46} = require('./utils');
47
48const {
49  codes: {
50    ERR_INVALID_ARG_VALUE,
51    ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
52    ERR_PARSE_ARGS_UNKNOWN_OPTION,
53    ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
54  },
55} = require('./internal/errors');
56
57function getMainArgs() {
58  // Work out where to slice process.argv for user supplied arguments.
59
60  // Check node options for scenarios where user CLI args follow executable.
61  const execArgv = process.execArgv;
62  if (ArrayPrototypeIncludes(execArgv, '-e') ||
63      ArrayPrototypeIncludes(execArgv, '--eval') ||
64      ArrayPrototypeIncludes(execArgv, '-p') ||
65      ArrayPrototypeIncludes(execArgv, '--print')) {
66    return ArrayPrototypeSlice(process.argv, 1);
67  }
68
69  // Normally first two arguments are executable and script, then CLI arguments
70  return ArrayPrototypeSlice(process.argv, 2);
71}
72
73/**
74 * In strict mode, throw for possible usage errors like --foo --bar
75 *
76 * @param {object} token - from tokens as available from parseArgs
77 */
78function checkOptionLikeValue(token) {
79  if (!token.inlineValue && isOptionLikeValue(token.value)) {
80    // Only show short example if user used short option.
81    const example = StringPrototypeStartsWith(token.rawName, '--') ?
82      `'${token.rawName}=-XYZ'` :
83      `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`;
84    const errorMessage = `Option '${token.rawName}' argument is ambiguous.
85Did you forget to specify the option argument for '${token.rawName}'?
86To specify an option argument starting with a dash use ${example}.`;
87    throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage);
88  }
89}
90
91/**
92 * In strict mode, throw for usage errors.
93 *
94 * @param {object} config - from config passed to parseArgs
95 * @param {object} token - from tokens as available from parseArgs
96 */
97function checkOptionUsage(config, token) {
98  if (!ObjectHasOwn(config.options, token.name)) {
99    throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
100      token.rawName, config.allowPositionals);
101  }
102
103  const short = optionsGetOwn(config.options, token.name, 'short');
104  const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
105  const type = optionsGetOwn(config.options, token.name, 'type');
106  if (type === 'string' && typeof token.value !== 'string') {
107    throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
108  }
109  // (Idiomatic test for undefined||null, expecting undefined.)
110  if (type === 'boolean' && token.value != null) {
111    throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`);
112  }
113}
114
115
116/**
117 * Store the option value in `values`.
118 *
119 * @param {string} longOption - long option name e.g. 'foo'
120 * @param {string|undefined} optionValue - value from user args
121 * @param {object} options - option configs, from parseArgs({ options })
122 * @param {object} values - option values returned in `values` by parseArgs
123 */
124function storeOption(longOption, optionValue, options, values) {
125  if (longOption === '__proto__') {
126    return; // No. Just no.
127  }
128
129  // We store based on the option value rather than option type,
130  // preserving the users intent for author to deal with.
131  const newValue = optionValue ?? true;
132  if (optionsGetOwn(options, longOption, 'multiple')) {
133    // Always store value in array, including for boolean.
134    // values[longOption] starts out not present,
135    // first value is added as new array [newValue],
136    // subsequent values are pushed to existing array.
137    // (note: values has null prototype, so simpler usage)
138    if (values[longOption]) {
139      ArrayPrototypePush(values[longOption], newValue);
140    } else {
141      values[longOption] = [newValue];
142    }
143  } else {
144    values[longOption] = newValue;
145  }
146}
147
148/**
149 * Store the default option value in `values`.
150 *
151 * @param {string} longOption - long option name e.g. 'foo'
152 * @param {string
153 *         | boolean
154 *         | string[]
155 *         | boolean[]} optionValue - default value from option config
156 * @param {object} values - option values returned in `values` by parseArgs
157 */
158function storeDefaultOption(longOption, optionValue, values) {
159  if (longOption === '__proto__') {
160    return; // No. Just no.
161  }
162
163  values[longOption] = optionValue;
164}
165
166/**
167 * Process args and turn into identified tokens:
168 * - option (along with value, if any)
169 * - positional
170 * - option-terminator
171 *
172 * @param {string[]} args - from parseArgs({ args }) or mainArgs
173 * @param {object} options - option configs, from parseArgs({ options })
174 */
175function argsToTokens(args, options) {
176  const tokens = [];
177  let index = -1;
178  let groupCount = 0;
179
180  const remainingArgs = ArrayPrototypeSlice(args);
181  while (remainingArgs.length > 0) {
182    const arg = ArrayPrototypeShift(remainingArgs);
183    const nextArg = remainingArgs[0];
184    if (groupCount > 0)
185      groupCount--;
186    else
187      index++;
188
189    // Check if `arg` is an options terminator.
190    // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
191    if (arg === '--') {
192      // Everything after a bare '--' is considered a positional argument.
193      ArrayPrototypePush(tokens, { kind: 'option-terminator', index });
194      ArrayPrototypePushApply(
195        tokens, ArrayPrototypeMap(remainingArgs, (arg) => {
196          return { kind: 'positional', index: ++index, value: arg };
197        })
198      );
199      break; // Finished processing args, leave while loop.
200    }
201
202    if (isLoneShortOption(arg)) {
203      // e.g. '-f'
204      const shortOption = StringPrototypeCharAt(arg, 1);
205      const longOption = findLongOptionForShort(shortOption, options);
206      let value;
207      let inlineValue;
208      if (optionsGetOwn(options, longOption, 'type') === 'string' &&
209          isOptionValue(nextArg)) {
210        // e.g. '-f', 'bar'
211        value = ArrayPrototypeShift(remainingArgs);
212        inlineValue = false;
213      }
214      ArrayPrototypePush(
215        tokens,
216        { kind: 'option', name: longOption, rawName: arg,
217          index, value, inlineValue });
218      if (value != null) ++index;
219      continue;
220    }
221
222    if (isShortOptionGroup(arg, options)) {
223      // Expand -fXzy to -f -X -z -y
224      const expanded = [];
225      for (let index = 1; index < arg.length; index++) {
226        const shortOption = StringPrototypeCharAt(arg, index);
227        const longOption = findLongOptionForShort(shortOption, options);
228        if (optionsGetOwn(options, longOption, 'type') !== 'string' ||
229          index === arg.length - 1) {
230          // Boolean option, or last short in group. Well formed.
231          ArrayPrototypePush(expanded, `-${shortOption}`);
232        } else {
233          // String option in middle. Yuck.
234          // Expand -abfFILE to -a -b -fFILE
235          ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`);
236          break; // finished short group
237        }
238      }
239      ArrayPrototypeUnshiftApply(remainingArgs, expanded);
240      groupCount = expanded.length;
241      continue;
242    }
243
244    if (isShortOptionAndValue(arg, options)) {
245      // e.g. -fFILE
246      const shortOption = StringPrototypeCharAt(arg, 1);
247      const longOption = findLongOptionForShort(shortOption, options);
248      const value = StringPrototypeSlice(arg, 2);
249      ArrayPrototypePush(
250        tokens,
251        { kind: 'option', name: longOption, rawName: `-${shortOption}`,
252          index, value, inlineValue: true });
253      continue;
254    }
255
256    if (isLoneLongOption(arg)) {
257      // e.g. '--foo'
258      const longOption = StringPrototypeSlice(arg, 2);
259      let value;
260      let inlineValue;
261      if (optionsGetOwn(options, longOption, 'type') === 'string' &&
262          isOptionValue(nextArg)) {
263        // e.g. '--foo', 'bar'
264        value = ArrayPrototypeShift(remainingArgs);
265        inlineValue = false;
266      }
267      ArrayPrototypePush(
268        tokens,
269        { kind: 'option', name: longOption, rawName: arg,
270          index, value, inlineValue });
271      if (value != null) ++index;
272      continue;
273    }
274
275    if (isLongOptionAndValue(arg)) {
276      // e.g. --foo=bar
277      const equalIndex = StringPrototypeIndexOf(arg, '=');
278      const longOption = StringPrototypeSlice(arg, 2, equalIndex);
279      const value = StringPrototypeSlice(arg, equalIndex + 1);
280      ArrayPrototypePush(
281        tokens,
282        { kind: 'option', name: longOption, rawName: `--${longOption}`,
283          index, value, inlineValue: true });
284      continue;
285    }
286
287    ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg });
288  }
289
290  return tokens;
291}
292
293const parseArgs = (config = kEmptyObject) => {
294  const args = objectGetOwn(config, 'args') ?? getMainArgs();
295  const strict = objectGetOwn(config, 'strict') ?? true;
296  const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
297  const returnTokens = objectGetOwn(config, 'tokens') ?? false;
298  const options = objectGetOwn(config, 'options') ?? { __proto__: null };
299  // Bundle these up for passing to strict-mode checks.
300  const parseConfig = { args, strict, options, allowPositionals };
301
302  // Validate input configuration.
303  validateArray(args, 'args');
304  validateBoolean(strict, 'strict');
305  validateBoolean(allowPositionals, 'allowPositionals');
306  validateBoolean(returnTokens, 'tokens');
307  validateObject(options, 'options');
308  ArrayPrototypeForEach(
309    ObjectEntries(options),
310    ({ 0: longOption, 1: optionConfig }) => {
311      validateObject(optionConfig, `options.${longOption}`);
312
313      // type is required
314      const optionType = objectGetOwn(optionConfig, 'type');
315      validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']);
316
317      if (ObjectHasOwn(optionConfig, 'short')) {
318        const shortOption = optionConfig.short;
319        validateString(shortOption, `options.${longOption}.short`);
320        if (shortOption.length !== 1) {
321          throw new ERR_INVALID_ARG_VALUE(
322            `options.${longOption}.short`,
323            shortOption,
324            'must be a single character'
325          );
326        }
327      }
328
329      const multipleOption = objectGetOwn(optionConfig, 'multiple');
330      if (ObjectHasOwn(optionConfig, 'multiple')) {
331        validateBoolean(multipleOption, `options.${longOption}.multiple`);
332      }
333
334      const defaultValue = objectGetOwn(optionConfig, 'default');
335      if (defaultValue !== undefined) {
336        let validator;
337        switch (optionType) {
338          case 'string':
339            validator = multipleOption ? validateStringArray : validateString;
340            break;
341
342          case 'boolean':
343            validator = multipleOption ? validateBooleanArray : validateBoolean;
344            break;
345        }
346        validator(defaultValue, `options.${longOption}.default`);
347      }
348    }
349  );
350
351  // Phase 1: identify tokens
352  const tokens = argsToTokens(args, options);
353
354  // Phase 2: process tokens into parsed option values and positionals
355  const result = {
356    values: { __proto__: null },
357    positionals: [],
358  };
359  if (returnTokens) {
360    result.tokens = tokens;
361  }
362  ArrayPrototypeForEach(tokens, (token) => {
363    if (token.kind === 'option') {
364      if (strict) {
365        checkOptionUsage(parseConfig, token);
366        checkOptionLikeValue(token);
367      }
368      storeOption(token.name, token.value, options, result.values);
369    } else if (token.kind === 'positional') {
370      if (!allowPositionals) {
371        throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);
372      }
373      ArrayPrototypePush(result.positionals, token.value);
374    }
375  });
376
377  // Phase 3: fill in default values for missing args
378  ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption,
379                                                   1: optionConfig }) => {
380    const mustSetDefault = useDefaultValueOption(longOption,
381                                                 optionConfig,
382                                                 result.values);
383    if (mustSetDefault) {
384      storeDefaultOption(longOption,
385                         objectGetOwn(optionConfig, 'default'),
386                         result.values);
387    }
388  });
389
390
391  return result;
392};
393
394module.exports = {
395  parseArgs,
396};
397