argsParser.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. function throwError(exitCode, message, helpText) {
  8. const error = new Error(
  9. helpText ? `${message}\n\n---\n\n${helpText}` : message
  10. );
  11. error.exitCode = exitCode;
  12. throw error;
  13. }
  14. function formatOption(option) {
  15. let text = ' ';
  16. text += option.abbr ? `-${option.abbr}, ` : ' ';
  17. text += `--${option.flag ? '(no-)' : ''}${option.full}`;
  18. if (option.choices) {
  19. text += `=${option.choices.join('|')}`;
  20. } else if (option.metavar) {
  21. text += `=${option.metavar}`;
  22. }
  23. if (option.list) {
  24. text += ' ...';
  25. }
  26. if (option.defaultHelp || option.default !== undefined || option.help) {
  27. text += ' ';
  28. if (text.length < 32) {
  29. text += ' '.repeat(32 - text.length);
  30. }
  31. const textLength = text.length;
  32. if (option.help) {
  33. text += option.help;
  34. }
  35. if (option.defaultHelp || option.default !== undefined) {
  36. if (option.help) {
  37. text += '\n';
  38. }
  39. text += `${' '.repeat(textLength)}(default: ${option.defaultHelp || option.default})`;
  40. }
  41. }
  42. return text;
  43. }
  44. function getHelpText(options) {
  45. const opts = Object.keys(options)
  46. .map(k => options[k])
  47. .sort((a,b) => a.full.localeCompare(b.full));
  48. const text = `
  49. Usage: jscodeshift [OPTION]... PATH...
  50. or: jscodeshift [OPTION]... -t TRANSFORM_PATH PATH...
  51. or: jscodeshift [OPTION]... -t URL PATH...
  52. or: jscodeshift [OPTION]... --stdin < file_list.txt
  53. Apply transform logic in TRANSFORM_PATH (recursively) to every PATH.
  54. If --stdin is set, each line of the standard input is used as a path.
  55. Options:
  56. "..." behind an option means that it can be supplied multiple times.
  57. All options are also passed to the transformer, which means you can supply custom options that are not listed here.
  58. ${opts.map(formatOption).join('\n')}
  59. `;
  60. return text.trimLeft();
  61. }
  62. function validateOptions(parsedOptions, options) {
  63. const errors = [];
  64. for (const optionName in options) {
  65. const option = options[optionName];
  66. if (option.choices && !option.choices.includes(parsedOptions[optionName])) {
  67. errors.push(
  68. `Error: --${option.full} must be one of the values ${option.choices.join(',')}`
  69. );
  70. }
  71. }
  72. if (errors.length > 0) {
  73. throwError(
  74. 1,
  75. errors.join('\n'),
  76. getHelpText(options)
  77. );
  78. }
  79. }
  80. function prepareOptions(options) {
  81. options.help = {
  82. abbr: 'h',
  83. help: 'print this help and exit',
  84. callback() {
  85. return getHelpText(options);
  86. },
  87. };
  88. const preparedOptions = {};
  89. for (const optionName of Object.keys(options)) {
  90. const option = options[optionName];
  91. if (!option.full) {
  92. option.full = optionName;
  93. }
  94. option.key = optionName;
  95. preparedOptions['--'+option.full] = option;
  96. if (option.abbr) {
  97. preparedOptions['-'+option.abbr] = option;
  98. }
  99. if (option.flag) {
  100. preparedOptions['--no-'+option.full] = option;
  101. }
  102. }
  103. return preparedOptions;
  104. }
  105. function isOption(value) {
  106. return /^--?/.test(value);
  107. }
  108. function parse(options, args=process.argv.slice(2)) {
  109. const missingValue = Symbol();
  110. const preparedOptions = prepareOptions(options);
  111. const parsedOptions = {};
  112. const positionalArguments = [];
  113. for (const optionName in options) {
  114. const option = options[optionName];
  115. if (option.default !== undefined) {
  116. parsedOptions[optionName] = option.default;
  117. } else if (option.list) {
  118. parsedOptions[optionName] = [];
  119. }
  120. }
  121. for (let i = 0; i < args.length; i++) {
  122. const arg = args[i];
  123. if (isOption(arg)) {
  124. let optionName = arg;
  125. let value = null;
  126. let option = null;
  127. if (optionName.includes('=')) {
  128. const index = arg.indexOf('=');
  129. optionName = arg.slice(0, index);
  130. value = arg.slice(index+1);
  131. }
  132. if (preparedOptions.hasOwnProperty(optionName)) {
  133. option = preparedOptions[optionName];
  134. } else {
  135. // Unknown options are just "passed along".
  136. // The logic is as follows:
  137. // - If an option is encountered without a value, it's treated
  138. // as a flag
  139. // - If the option has a value, it's initialized with that value
  140. // - If the option has been seen before, it's converted to a list
  141. // If the previous value was true (i.e. a flag), that value is
  142. // discarded.
  143. const realOptionName = optionName.replace(/^--?(no-)?/, '');
  144. const isList = parsedOptions.hasOwnProperty(realOptionName) &&
  145. parsedOptions[realOptionName] !== true;
  146. option = {
  147. key: realOptionName,
  148. full: realOptionName,
  149. flag: !parsedOptions.hasOwnProperty(realOptionName) &&
  150. value === null &&
  151. isOption(args[i+1]),
  152. list: isList,
  153. process(value) {
  154. // Try to parse values as JSON to be compatible with nomnom
  155. try {
  156. return JSON.parse(value);
  157. } catch(_e) {}
  158. return value;
  159. },
  160. };
  161. if (isList) {
  162. const currentValue = parsedOptions[realOptionName];
  163. if (!Array.isArray(currentValue)) {
  164. parsedOptions[realOptionName] = currentValue === true ?
  165. [] :
  166. [currentValue];
  167. }
  168. }
  169. }
  170. if (option.callback) {
  171. throwError(0, option.callback());
  172. } else if (option.flag) {
  173. if (optionName.startsWith('--no-')) {
  174. value = false;
  175. } else if (value !== null) {
  176. value = value === '1';
  177. } else {
  178. value = true;
  179. }
  180. parsedOptions[option.key] = value;
  181. } else {
  182. if (value === null && i < args.length - 1 && !isOption(args[i+1])) {
  183. // consume next value
  184. value = args[i+1];
  185. i += 1;
  186. }
  187. if (value !== null) {
  188. if (option.process) {
  189. value = option.process(value);
  190. }
  191. if (option.list) {
  192. parsedOptions[option.key].push(value);
  193. } else {
  194. parsedOptions[option.key] = value;
  195. }
  196. } else {
  197. parsedOptions[option.key] = missingValue;
  198. }
  199. }
  200. } else {
  201. positionalArguments.push(/^\d+$/.test(arg) ? Number(arg) : arg);
  202. }
  203. }
  204. for (const optionName in parsedOptions) {
  205. if (parsedOptions[optionName] === missingValue) {
  206. throwError(
  207. 1,
  208. `Missing value: --${options[optionName].full} requires a value`,
  209. getHelpText(options)
  210. );
  211. }
  212. }
  213. const result = {
  214. positionalArguments,
  215. options: parsedOptions,
  216. };
  217. validateOptions(parsedOptions, options);
  218. return result;
  219. }
  220. module.exports = {
  221. /**
  222. * `options` is an object of objects. Each option can have the following
  223. * properties:
  224. *
  225. * - full: The name of the option to be used in the command line (if
  226. * different than the property name.
  227. * - abbr: The short version of the option, a single character
  228. * - flag: Whether the option takes an argument or not.
  229. * - default: The default value to use if option is not supplied
  230. * - choices: Restrict possible values to these values
  231. * - help: The help text to print
  232. * - metavar: Value placeholder to use in the help
  233. * - callback: If option is supplied, call this function and exit
  234. * - process: Pre-process value before returning it
  235. */
  236. options(options) {
  237. return {
  238. parse(args) {
  239. return parse(options, args);
  240. },
  241. getHelpText() {
  242. return getHelpText(options);
  243. },
  244. };
  245. },
  246. };