parser.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. /**
  4. * @license
  5. * Copyright Google Inc. All Rights Reserved.
  6. *
  7. * Use of this source code is governed by an MIT-style license that can be
  8. * found in the LICENSE file at https://angular.io/license
  9. *
  10. */
  11. const core_1 = require("@angular-devkit/core");
  12. const interface_1 = require("./interface");
  13. class ParseArgumentException extends core_1.BaseException {
  14. constructor(comments, parsed, ignored) {
  15. super(`One or more errors occurred while parsing arguments:\n ${comments.join('\n ')}`);
  16. this.comments = comments;
  17. this.parsed = parsed;
  18. this.ignored = ignored;
  19. }
  20. }
  21. exports.ParseArgumentException = ParseArgumentException;
  22. function _coerceType(str, type, v) {
  23. switch (type) {
  24. case interface_1.OptionType.Any:
  25. if (Array.isArray(v)) {
  26. return v.concat(str || '');
  27. }
  28. return _coerceType(str, interface_1.OptionType.Boolean, v) !== undefined
  29. ? _coerceType(str, interface_1.OptionType.Boolean, v)
  30. : _coerceType(str, interface_1.OptionType.Number, v) !== undefined
  31. ? _coerceType(str, interface_1.OptionType.Number, v)
  32. : _coerceType(str, interface_1.OptionType.String, v);
  33. case interface_1.OptionType.String:
  34. return str || '';
  35. case interface_1.OptionType.Boolean:
  36. switch (str) {
  37. case 'false':
  38. return false;
  39. case undefined:
  40. case '':
  41. case 'true':
  42. return true;
  43. default:
  44. return undefined;
  45. }
  46. case interface_1.OptionType.Number:
  47. if (str === undefined) {
  48. return 0;
  49. }
  50. else if (str === '') {
  51. return undefined;
  52. }
  53. else if (Number.isFinite(+str)) {
  54. return +str;
  55. }
  56. else {
  57. return undefined;
  58. }
  59. case interface_1.OptionType.Array:
  60. return Array.isArray(v)
  61. ? v.concat(str || '')
  62. : v === undefined
  63. ? [str || '']
  64. : [v + '', str || ''];
  65. default:
  66. return undefined;
  67. }
  68. }
  69. function _coerce(str, o, v) {
  70. if (!o) {
  71. return _coerceType(str, interface_1.OptionType.Any, v);
  72. }
  73. else {
  74. const types = o.types || [o.type];
  75. // Try all the types one by one and pick the first one that returns a value contained in the
  76. // enum. If there's no enum, just return the first one that matches.
  77. for (const type of types) {
  78. const maybeResult = _coerceType(str, type, v);
  79. if (maybeResult !== undefined) {
  80. if (!o.enum || o.enum.includes(maybeResult)) {
  81. return maybeResult;
  82. }
  83. }
  84. }
  85. return undefined;
  86. }
  87. }
  88. function _getOptionFromName(name, options) {
  89. const camelName = /(-|_)/.test(name)
  90. ? core_1.strings.camelize(name)
  91. : name;
  92. for (const option of options) {
  93. if (option.name === name || option.name === camelName) {
  94. return option;
  95. }
  96. if (option.aliases.some(x => x === name || x === camelName)) {
  97. return option;
  98. }
  99. }
  100. return undefined;
  101. }
  102. function _removeLeadingDashes(key) {
  103. const from = key.startsWith('--') ? 2 : key.startsWith('-') ? 1 : 0;
  104. return key.substr(from);
  105. }
  106. function _assignOption(arg, nextArg, { options, parsedOptions, leftovers, ignored, errors, warnings }) {
  107. const from = arg.startsWith('--') ? 2 : 1;
  108. let consumedNextArg = false;
  109. let key = arg.substr(from);
  110. let option = null;
  111. let value = '';
  112. const i = arg.indexOf('=');
  113. // If flag is --no-abc AND there's no equal sign.
  114. if (i == -1) {
  115. if (key.startsWith('no')) {
  116. // Only use this key if the option matching the rest is a boolean.
  117. const from = key.startsWith('no-') ? 3 : 2;
  118. const maybeOption = _getOptionFromName(core_1.strings.camelize(key.substr(from)), options);
  119. if (maybeOption && maybeOption.type == 'boolean') {
  120. value = 'false';
  121. option = maybeOption;
  122. }
  123. }
  124. if (option === null) {
  125. // Set it to true if it's a boolean and the next argument doesn't match true/false.
  126. const maybeOption = _getOptionFromName(key, options);
  127. if (maybeOption) {
  128. value = nextArg;
  129. let shouldShift = true;
  130. if (value && value.startsWith('-')) {
  131. // Verify if not having a value results in a correct parse, if so don't shift.
  132. if (_coerce(undefined, maybeOption) !== undefined) {
  133. shouldShift = false;
  134. }
  135. }
  136. // Only absorb it if it leads to a better value.
  137. if (shouldShift && _coerce(value, maybeOption) !== undefined) {
  138. consumedNextArg = true;
  139. }
  140. else {
  141. value = '';
  142. }
  143. option = maybeOption;
  144. }
  145. }
  146. }
  147. else {
  148. key = arg.substring(0, i);
  149. option = _getOptionFromName(_removeLeadingDashes(key), options) || null;
  150. if (option) {
  151. value = arg.substring(i + 1);
  152. }
  153. }
  154. if (option === null) {
  155. if (nextArg && !nextArg.startsWith('-')) {
  156. leftovers.push(arg, nextArg);
  157. consumedNextArg = true;
  158. }
  159. else {
  160. leftovers.push(arg);
  161. }
  162. }
  163. else {
  164. const v = _coerce(value, option, parsedOptions[option.name]);
  165. if (v !== undefined) {
  166. if (parsedOptions[option.name] !== v) {
  167. if (parsedOptions[option.name] !== undefined && option.type !== interface_1.OptionType.Array) {
  168. warnings.push(`Option ${JSON.stringify(option.name)} was already specified with value `
  169. + `${JSON.stringify(parsedOptions[option.name])}. The new value ${JSON.stringify(v)} `
  170. + `will override it.`);
  171. }
  172. parsedOptions[option.name] = v;
  173. if (option.deprecated !== undefined && option.deprecated !== false) {
  174. warnings.push(`Option ${JSON.stringify(option.name)} is deprecated${typeof option.deprecated == 'string' ? ': ' + option.deprecated : '.'}`);
  175. }
  176. }
  177. }
  178. else {
  179. let error = `Argument ${key} could not be parsed using value ${JSON.stringify(value)}.`;
  180. if (option.enum) {
  181. error += ` Valid values are: ${option.enum.map(x => JSON.stringify(x)).join(', ')}.`;
  182. }
  183. else {
  184. error += `Valid type(s) is: ${(option.types || [option.type]).join(', ')}`;
  185. }
  186. errors.push(error);
  187. ignored.push(arg);
  188. }
  189. }
  190. return consumedNextArg;
  191. }
  192. /**
  193. * Parse the arguments in a consistent way, but without having any option definition. This tries
  194. * to assess what the user wants in a free form. For example, using `--name=false` will set the
  195. * name properties to a boolean type.
  196. * This should only be used when there's no schema available or if a schema is "true" (anything is
  197. * valid).
  198. *
  199. * @param args Argument list to parse.
  200. * @returns An object that contains a property per flags from the args.
  201. */
  202. function parseFreeFormArguments(args) {
  203. const parsedOptions = {};
  204. const leftovers = [];
  205. for (let arg = args.shift(); arg !== undefined; arg = args.shift()) {
  206. if (arg == '--') {
  207. leftovers.push(...args);
  208. break;
  209. }
  210. if (arg.startsWith('--')) {
  211. const eqSign = arg.indexOf('=');
  212. let name;
  213. let value;
  214. if (eqSign !== -1) {
  215. name = arg.substring(2, eqSign);
  216. value = arg.substring(eqSign + 1);
  217. }
  218. else {
  219. name = arg.substr(2);
  220. value = args.shift();
  221. }
  222. const v = _coerce(value, null, parsedOptions[name]);
  223. if (v !== undefined) {
  224. parsedOptions[name] = v;
  225. }
  226. }
  227. else if (arg.startsWith('-')) {
  228. arg.split('').forEach(x => parsedOptions[x] = true);
  229. }
  230. else {
  231. leftovers.push(arg);
  232. }
  233. }
  234. if (leftovers.length) {
  235. parsedOptions['--'] = leftovers;
  236. }
  237. return parsedOptions;
  238. }
  239. exports.parseFreeFormArguments = parseFreeFormArguments;
  240. /**
  241. * Parse the arguments in a consistent way, from a list of standardized options.
  242. * The result object will have a key per option name, with the `_` key reserved for positional
  243. * arguments, and `--` will contain everything that did not match. Any key that don't have an
  244. * option will be pushed back in `--` and removed from the object. If you need to validate that
  245. * there's no additionalProperties, you need to check the `--` key.
  246. *
  247. * @param args The argument array to parse.
  248. * @param options List of supported options. {@see Option}.
  249. * @param logger Logger to use to warn users.
  250. * @returns An object that contains a property per option.
  251. */
  252. function parseArguments(args, options, logger) {
  253. if (options === null) {
  254. options = [];
  255. }
  256. const leftovers = [];
  257. const positionals = [];
  258. const parsedOptions = {};
  259. const ignored = [];
  260. const errors = [];
  261. const warnings = [];
  262. const state = { options, parsedOptions, positionals, leftovers, ignored, errors, warnings };
  263. for (let argIndex = 0; argIndex < args.length; argIndex++) {
  264. const arg = args[argIndex];
  265. let consumedNextArg = false;
  266. if (arg == '--') {
  267. // If we find a --, we're done.
  268. leftovers.push(...args.slice(argIndex + 1));
  269. break;
  270. }
  271. if (arg.startsWith('--')) {
  272. consumedNextArg = _assignOption(arg, args[argIndex + 1], state);
  273. }
  274. else if (arg.startsWith('-')) {
  275. // Argument is of form -abcdef. Starts at 1 because we skip the `-`.
  276. for (let i = 1; i < arg.length; i++) {
  277. const flag = arg[i];
  278. // If the next character is an '=', treat it as a long flag.
  279. if (arg[i + 1] == '=') {
  280. const f = '-' + flag + arg.slice(i + 1);
  281. consumedNextArg = _assignOption(f, args[argIndex + 1], state);
  282. break;
  283. }
  284. // Treat the last flag as `--a` (as if full flag but just one letter). We do this in
  285. // the loop because it saves us a check to see if the arg is just `-`.
  286. if (i == arg.length - 1) {
  287. const arg = '-' + flag;
  288. consumedNextArg = _assignOption(arg, args[argIndex + 1], state);
  289. }
  290. else {
  291. const maybeOption = _getOptionFromName(flag, options);
  292. if (maybeOption) {
  293. const v = _coerce(undefined, maybeOption, parsedOptions[maybeOption.name]);
  294. if (v !== undefined) {
  295. parsedOptions[maybeOption.name] = v;
  296. }
  297. }
  298. }
  299. }
  300. }
  301. else {
  302. positionals.push(arg);
  303. }
  304. if (consumedNextArg) {
  305. argIndex++;
  306. }
  307. }
  308. // Deal with positionals.
  309. // TODO(hansl): this is by far the most complex piece of code in this file. Try to refactor it
  310. // simpler.
  311. if (positionals.length > 0) {
  312. let pos = 0;
  313. for (let i = 0; i < positionals.length;) {
  314. let found = false;
  315. let incrementPos = false;
  316. let incrementI = true;
  317. // We do this with a found flag because more than 1 option could have the same positional.
  318. for (const option of options) {
  319. // If any option has this positional and no value, AND fit the type, we need to remove it.
  320. if (option.positional === pos) {
  321. const coercedValue = _coerce(positionals[i], option, parsedOptions[option.name]);
  322. if (parsedOptions[option.name] === undefined && coercedValue !== undefined) {
  323. parsedOptions[option.name] = coercedValue;
  324. found = true;
  325. }
  326. else {
  327. incrementI = false;
  328. }
  329. incrementPos = true;
  330. }
  331. }
  332. if (found) {
  333. positionals.splice(i--, 1);
  334. }
  335. if (incrementPos) {
  336. pos++;
  337. }
  338. if (incrementI) {
  339. i++;
  340. }
  341. }
  342. }
  343. if (positionals.length > 0 || leftovers.length > 0) {
  344. parsedOptions['--'] = [...positionals, ...leftovers];
  345. }
  346. if (warnings.length > 0 && logger) {
  347. warnings.forEach(message => logger.warn(message));
  348. }
  349. if (errors.length > 0) {
  350. throw new ParseArgumentException(errors, parsedOptions, ignored);
  351. }
  352. return parsedOptions;
  353. }
  354. exports.parseArguments = parseArguments;