UNPKG

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