index.js 13 KB

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