UNPKG

17.5 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11Object.defineProperty(exports, "__esModule", { value: true });
12const mri = require("mri");
13const os = require("os");
14const parameters_1 = require("./parameters");
15const ParserErrors_1 = require("./ParserErrors");
16const program_1 = require("./program");
17const utils_1 = require("./utils");
18class Parser {
19 constructor(argv = process.argv, commandPackageDefinition, runHandler, errorHandler, promptHandler, version, internalOptions = [], showPrompts = false) {
20 this.argv = argv;
21 this.runHandler = runHandler;
22 this.errorHandler = errorHandler;
23 this.promptHandler = promptHandler;
24 this.version = version;
25 this.internalOptions = internalOptions;
26 this.showPrompts = showPrompts;
27 this._definitionsMap = new Map();
28 this._argVals = [];
29 this._optionVals = {};
30 this._passedOptions = [];
31 this._parserErrors = new ParserErrors_1.ParserErrors();
32 this.findCommandDefinition = (name) => {
33 let cmdDef = this._definitionsMap.get(name);
34 if (!cmdDef) {
35 // if no exact match on command name, try to match on all lower case, or dash-case of command name
36 const cmdDefs = Array.from(this._definitionsMap.values());
37 cmdDef = cmdDefs.find(d => utils_1.dashCase(d.name).toLowerCase() === name.toLowerCase() || d.name.toLowerCase() === name.toLowerCase());
38 }
39 return cmdDef;
40 };
41 this.parseCommandLine = () => {
42 let offset = 2;
43 // parse using root and internal options
44 this._definitionOptions = this.internalOptions.concat(this._commandPackage.options || []);
45 let parsedMri = mri(this.argv.slice(offset), utils_1.getOptionsMriOpts(this._definitionOptions));
46 // Loop thru possible command(s)
47 let tryName;
48 let name = '';
49 let i = 1;
50 const len = parsedMri._.length + 1;
51 for (; i < len; i++) {
52 tryName = parsedMri._.slice(0, i).join(' ');
53 // if (this._definitionsMap.has(tryName)) {
54 if (this.findCommandDefinition(tryName)) {
55 name = tryName;
56 offset = i + 2; // argv slicer
57 }
58 }
59 let cmdDef = this.findCommandDefinition(name);
60 if (!cmdDef) {
61 if (name) {
62 this.pushError('command', `Unknown Command: '${name}'`);
63 }
64 else if (this._commandPackage.defaultCommandName) {
65 cmdDef = this._definitionsMap.get(this._commandPackage.defaultCommandName);
66 }
67 else if (this._commandPackage.commands.length > 0) {
68 this.pushError('command', `No Command Specified`);
69 }
70 }
71 // this._passedOptions = passedOptionsToNames(, ;
72 const rawPassedOptions = Object.keys(parsedMri).filter(v => v !== '_');
73 // if a command definition is identified, re-parse the command line with definition configurations
74 if (cmdDef) {
75 this._definitionOptions.push(...(cmdDef.options || []));
76 parsedMri = mri(this.argv.slice(offset), utils_1.getOptionsMriOpts(this._definitionOptions));
77 this._argVals = parsedMri._;
78 this._parsedCommand.command = cmdDef;
79 this._parsedCommand.parsedCommandName = name;
80 }
81 this._passedOptions = utils_1.passedOptionsToNames(rawPassedOptions, this._definitionOptions);
82 // Collect parsed Options
83 this._optionVals = Object.keys(parsedMri)
84 .filter(v => v !== '_')
85 .reduce((prev, curr) => {
86 prev[curr] = parsedMri[curr];
87 return prev;
88 }, {});
89 // Create a hash with all active options for final parsed output
90 this._parsedCommand.optionDefinitions = this._definitionOptions.reduce((prev, curr) => {
91 prev[curr.name] = curr;
92 return prev;
93 }, {});
94 };
95 this.parseArgs = () => {
96 const cmd = this.parsedCommand.command;
97 const defArgs = cmd ? cmd.arguments || [] : [];
98 const requiredDefArgs = defArgs.filter(a => !a.isOptional);
99 const isVariadic = defArgs.length > 0 && defArgs[defArgs.length - 1].isVariadic;
100 // check for too many args
101 if (this._argVals.length > defArgs.length && !isVariadic) {
102 this.pushError('argument', `Too many Arguments`);
103 }
104 // check for too few args
105 if (this._argVals.length < requiredDefArgs.length) {
106 // don't raise insufficient args if an option with a handler is set (ie help/version);
107 if (!this.getParsedOptionCommandName()) {
108 this.pushError('argument', `Insufficient arguments`);
109 }
110 }
111 const parsedArgs = {};
112 const validateArg = (val, arg) => {
113 const validation = parameters_1.validateCommandDefinitionParameterValue(val, arg);
114 if (validation !== true) {
115 const msg = typeof validation === 'string' ? validation : undefined;
116 this.pushError('argument', `Value for argument '${arg.name}' is invalid${msg ? `: ${msg}` : ''}`);
117 }
118 };
119 let argValIndex = 0;
120 // Loop through definition arguments, and assign each prop in definition to args value array
121 for (const defArg of defArgs) {
122 if (defArg.isVariadic) {
123 // variadic arg, must be last arg, assign remainder of command line
124 const vals = this._argVals.slice(argValIndex).map(a => parameters_1.getCommandDefinitionParameterValue(a, defArg));
125 // validate each value
126 vals.forEach(v => validateArg(v, defArg));
127 parsedArgs[defArg.name] = Object.assign(Object.assign({}, defArg), { wasPassed: true, value: vals });
128 argValIndex = this._argVals.length;
129 }
130 else if (this._argVals.length > argValIndex) {
131 // still arguments from command line, assign next arg to args arry
132 // assign value from command line to args array
133 const val = parameters_1.getCommandDefinitionParameterValue(this._argVals[argValIndex], defArg);
134 // validate
135 validateArg(val, defArg);
136 parsedArgs[defArg.name] = Object.assign(Object.assign({}, defArg), { wasPassed: true, value: val });
137 argValIndex++;
138 }
139 else {
140 // no more arguments on command line, assign default
141 parsedArgs[defArg.name] = Object.assign(Object.assign({}, defArg), { wasPassed: false, value: parameters_1.getCommandDefinitionParameterValue(undefined, defArg, false) });
142 argValIndex++;
143 }
144 }
145 // create args hash with just values
146 const args = Object.keys(parsedArgs).reduce((prev, curr) => {
147 prev[curr] = parsedArgs[curr].value;
148 return prev;
149 }, {});
150 const transformedArgs = utils_1.isFunction(cmd.transformArguments) ? cmd.transformArguments(args) : args;
151 // re-assign parsed arg value from transformed args (transform could add or remove entries, so not necessarily 1:1)
152 Object.keys(transformedArgs).forEach(ta => {
153 if (parsedArgs[ta]) {
154 parsedArgs[ta].value = transformedArgs[ta];
155 }
156 });
157 this.parsedCommand.parsedArguments = parsedArgs;
158 this.parsedCommand.arguments = transformedArgs;
159 };
160 this.parseOptions = () => {
161 const cmd = this._parsedCommand.command;
162 const dynamicOptions = {};
163 Object.keys(this._optionVals).forEach(o => {
164 if (!this._definitionOptions.find(co => co.name === o || co.flag === o || utils_1.dashCase(co.name) === o)) {
165 // if option not defined, add as dynamic if allowed, otherwise add as error
166 if (cmd && cmd.allowDynamicOptions) {
167 dynamicOptions[utils_1.camelCase(o)] = this._optionVals[o];
168 }
169 else {
170 this.pushError('option', `Invalid Option: ${o}`);
171 }
172 }
173 });
174 const parsedOptions = {};
175 this._definitionOptions.forEach(o => {
176 const wasPassed = this._passedOptions.indexOf(o.name) >= 0;
177 parsedOptions[o.name] = Object.assign(Object.assign({}, o), { wasPassed, value: parameters_1.getCommandDefinitionParameterValue(this._optionVals[o.name], o, wasPassed) });
178 if (wasPassed) {
179 // if option set on command line, and not a boolean option, make sure there is a value
180 if (o.valueType && o.valueType !== 'boolean' && !parsedOptions[o.name].value) {
181 this.pushError('option', `Value for option '${o.name}' not specified`);
182 }
183 else {
184 const validation = parameters_1.validateCommandDefinitionParameterValue(parsedOptions[o.name].value, o);
185 if (validation !== true) {
186 const msg = typeof validation === 'string' ? validation : undefined;
187 this.pushError('option', `Value for option '${o.name}' is invalid${msg ? `: ${msg}` : ''}`);
188 }
189 }
190 }
191 });
192 // add any dynamic options to parsedOptions
193 if (cmd && cmd.allowDynamicOptions) {
194 Object.keys(dynamicOptions).forEach(dyo => {
195 parsedOptions[dyo] = {
196 name: dyo,
197 wasPassed: true,
198 value: dynamicOptions[dyo],
199 };
200 });
201 }
202 // create options hash with just values
203 const options = Object.keys(parsedOptions).reduce((prev, curr) => {
204 prev[curr] = parsedOptions[curr].value;
205 return prev;
206 }, {});
207 const transformedOptions = cmd
208 ? utils_1.isFunction(cmd.transformOptions)
209 ? cmd.transformOptions(options)
210 : options
211 : options;
212 // re-assign parsed option value from transformed options (transform could add or remove entries, so not necessarily 1:1)
213 Object.keys(transformedOptions).forEach(to => {
214 if (parsedOptions[to]) {
215 parsedOptions[to].value = transformedOptions[to];
216 }
217 });
218 this._parsedCommand.parsedOptions = parsedOptions;
219 this._parsedCommand.options = transformedOptions;
220 this._parsedCommand.dynamicOptions = dynamicOptions;
221 };
222 this.pushError = (type, message) => {
223 this._parserErrors.push(type, message);
224 };
225 this.getEnv = () => {
226 const cmd = this.parsedCommand.command;
227 const env = {};
228 if (cmd && cmd.env) {
229 return cmd.env;
230 }
231 return env;
232 };
233 this.parseArgsAndOptions = () => {
234 this.parseOptions();
235 if (this._parsedCommand.command) {
236 this.parseArgs();
237 }
238 };
239 this.getParsedOptionCommandName = () => {
240 const parsed = this._parsedCommand;
241 return Object.keys(parsed.options)
242 .filter(o => typeof parsed.options[o] !== 'undefined')
243 .find(o => !!(parsed.optionDefinitions[o] && parsed.optionDefinitions[o].commandHandler));
244 };
245 this.setProcessEnvFromParsedCommand = () => {
246 Object.keys(this.parsedCommand.env).forEach(k => {
247 process.env[k] = this.parsedCommand.env[k];
248 });
249 };
250 this.promptArgsToArgVals = (promptArgs) => {
251 const cmd = this.parsedCommand.command;
252 const defArgs = cmd ? cmd.arguments || [] : [];
253 const argVals = [];
254 // Loop through definition arguments, and assign value from promptArgs Hash
255 for (const defArg of defArgs) {
256 const promptArgVal = promptArgs[defArg.name];
257 if (defArg.isVariadic) {
258 const promptArgValAry = Array.isArray(promptArgVal) ? promptArgVal : [promptArgVal];
259 argVals.push(...promptArgValAry);
260 }
261 else {
262 argVals.push(promptArgVal);
263 }
264 }
265 return argVals;
266 };
267 this.runAndParsePrompt = () => __awaiter(this, void 0, void 0, function* () {
268 if (this.showPrompts) {
269 const parsedCommand = this._parsedCommand;
270 // call out to prompt handler to get args and options
271 const promptResult = yield this.promptHandler(parsedCommand);
272 // reset arg/option errors
273 this._parserErrors.clearErrors('argument');
274 this._parserErrors.clearErrors('option');
275 // convert prompt arg values to command line array to re-parse them
276 this._argVals = this.promptArgsToArgVals(promptResult.arguments);
277 // assign prompt options to _optionVals to re-parse them
278 this._optionVals = promptResult.options;
279 // re-parse args and options from prompt values
280 this.parseArgsAndOptions();
281 }
282 });
283 this.runner = () => __awaiter(this, void 0, void 0, function* () {
284 const parsedCommand = this._parsedCommand;
285 // determine if an option is set to call a handler (in to over-ride the command def handler)
286 const commandOption = this.getParsedOptionCommandName();
287 let exitCode;
288 if (commandOption) {
289 // a selected option has it's own handler, set it up and run the option's handler (in lieu of the command's handler)
290 const commandOptionHandler = parsedCommand.optionDefinitions[commandOption].commandHandler;
291 exitCode = yield Promise.resolve(commandOptionHandler(parsedCommand));
292 }
293 else {
294 // delete options that are commands themselves (i.e. '--help', '--version')
295 Object.keys(parsedCommand.options)
296 .filter(o => !!(parsedCommand.optionDefinitions[o] && parsedCommand.optionDefinitions[o].commandHandler))
297 .forEach(o => {
298 delete parsedCommand.options[o];
299 });
300 yield this.runAndParsePrompt();
301 if (parsedCommand.errors.hasErrors) {
302 // parsed errors, call the errorHandler
303 exitCode = (yield Promise.resolve(this.errorHandler(parsedCommand))) || 1;
304 }
305 else {
306 // set process env from command env settings
307 this.setProcessEnvFromParsedCommand();
308 // call the handler with parsed command
309 exitCode = yield this.runHandler(parsedCommand);
310 }
311 }
312 return {
313 parsedCommand,
314 exitCode: typeof exitCode === 'number' ? exitCode : 0,
315 };
316 });
317 this._commandPackage = program_1.CliProgram.create(commandPackageDefinition, internalOptions);
318 if (this._commandPackage.validationErrors.length > 0) {
319 throw new Error(`Invalid Command Definition: ${this._commandPackage.validationErrors.join(os.EOL)}`);
320 }
321 this._definitionsMap = this._commandPackage.commandsMap;
322 }
323 get parsedCommand() {
324 if (!this._parsedCommand) {
325 this._parsedCommand = {};
326 this._parsedCommand.errors = this._parserErrors;
327 this._parsedCommand.program = this._commandPackage;
328 this._parsedCommand.run = this.runner;
329 this._parsedCommand.version = this.version;
330 this.parseCommandLine();
331 this.parseArgsAndOptions();
332 this._parsedCommand.env = this.getEnv();
333 }
334 return this._parsedCommand;
335 }
336}
337exports.Parser = Parser;
338//# sourceMappingURL=Parser.js.map
\No newline at end of file