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