UNPKG

16.7 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 Object.keys(this._optionVals).forEach(o => {
163 if (!this._definitionOptions.find(co => co.name === o || co.flag === o || utils_1.dashCase(co.name) === o)) {
164 this.pushError('option', `Invalid Option: ${o}`);
165 }
166 });
167 const parsedOptions = {};
168 this._definitionOptions.forEach(o => {
169 const wasPassed = this._passedOptions.indexOf(o.name) >= 0;
170 parsedOptions[o.name] = Object.assign(Object.assign({}, o), { wasPassed, value: parameters_1.getCommandDefinitionParameterValue(this._optionVals[o.name], o, wasPassed) });
171 if (wasPassed) {
172 // if option set on command line, and not a boolean option, make sure there is a value
173 if (o.valueType && o.valueType !== 'boolean' && !parsedOptions[o.name].value) {
174 this.pushError('option', `Value for option '${o.name}' not specified`);
175 }
176 else {
177 const validation = parameters_1.validateCommandDefinitionParameterValue(parsedOptions[o.name].value, o);
178 if (validation !== true) {
179 const msg = typeof validation === 'string' ? validation : undefined;
180 this.pushError('option', `Value for option '${o.name}' is invalid${msg ? `: ${msg}` : ''}`);
181 }
182 }
183 }
184 });
185 // create args hash with just values
186 const options = Object.keys(parsedOptions).reduce((prev, curr) => {
187 prev[curr] = parsedOptions[curr].value;
188 return prev;
189 }, {});
190 const transformedOptions = cmd
191 ? utils_1.isFunction(cmd.transformOptions)
192 ? cmd.transformOptions(options)
193 : options
194 : options;
195 // re-assign parsed option value from transformed options (transform could add or remove entries, so not necessarily 1:1)
196 Object.keys(transformedOptions).forEach(to => {
197 if (parsedOptions[to]) {
198 parsedOptions[to].value = transformedOptions[to];
199 }
200 });
201 this._parsedCommand.parsedOptions = parsedOptions;
202 this._parsedCommand.options = transformedOptions;
203 };
204 this.pushError = (type, message) => {
205 this._parserErrors.push(type, message);
206 };
207 this.getEnv = () => {
208 const cmd = this.parsedCommand.command;
209 const env = {};
210 if (cmd && cmd.env) {
211 return cmd.env;
212 }
213 return env;
214 };
215 this.parseArgsAndOptions = () => {
216 this.parseOptions();
217 if (this._parsedCommand.command) {
218 this.parseArgs();
219 }
220 };
221 this.getParsedOptionCommandName = () => {
222 const parsed = this._parsedCommand;
223 return Object.keys(parsed.options)
224 .filter(o => typeof parsed.options[o] !== 'undefined')
225 .find(o => !!(parsed.optionDefinitions[o] && parsed.optionDefinitions[o].commandHandler));
226 };
227 this.setProcessEnvFromParsedCommand = () => {
228 Object.keys(this.parsedCommand.env).forEach(k => {
229 process.env[k] = this.parsedCommand.env[k];
230 });
231 };
232 this.promptArgsToArgVals = (promptArgs) => {
233 const cmd = this.parsedCommand.command;
234 const defArgs = cmd ? cmd.arguments || [] : [];
235 const argVals = [];
236 // Loop through definition arguments, and assign value from promptArgs Hash
237 for (const defArg of defArgs) {
238 const promptArgVal = promptArgs[defArg.name];
239 if (defArg.isVariadic) {
240 const promptArgValAry = Array.isArray(promptArgVal) ? promptArgVal : [promptArgVal];
241 argVals.push(...promptArgValAry);
242 }
243 else {
244 argVals.push(promptArgVal);
245 }
246 }
247 return argVals;
248 };
249 this.runAndParsePrompt = () => __awaiter(this, void 0, void 0, function* () {
250 if (this.showPrompts) {
251 const parsedCommand = this._parsedCommand;
252 // call out to prompt handler to get args and options
253 const promptResult = yield this.promptHandler(parsedCommand);
254 // reset arg/option errors
255 this._parserErrors.clearErrors('argument');
256 this._parserErrors.clearErrors('option');
257 // convert prompt arg values to command line array to re-parse them
258 this._argVals = this.promptArgsToArgVals(promptResult.arguments);
259 // assign prompt options to _optionVals to re-parse them
260 this._optionVals = promptResult.options;
261 // re-parse args and options from prompt values
262 this.parseArgsAndOptions();
263 }
264 });
265 this.runner = () => __awaiter(this, void 0, void 0, function* () {
266 const parsedCommand = this._parsedCommand;
267 // determine if an option is set to call a handler (in to over-ride the command def handler)
268 const commandOption = this.getParsedOptionCommandName();
269 let exitCode;
270 if (commandOption) {
271 // a selected option has it's own handler, set it up and run the option's handler (in lieu of the command's handler)
272 const commandOptionHandler = parsedCommand.optionDefinitions[commandOption].commandHandler;
273 exitCode = yield Promise.resolve(commandOptionHandler(parsedCommand));
274 }
275 else {
276 // delete options that are commands themselves (i.e. '--help', '--version')
277 Object.keys(parsedCommand.options)
278 .filter(o => !!(parsedCommand.optionDefinitions[o] && parsedCommand.optionDefinitions[o].commandHandler))
279 .forEach(o => {
280 delete parsedCommand.options[o];
281 });
282 yield this.runAndParsePrompt();
283 if (parsedCommand.errors.hasErrors) {
284 // parsed errors, call the errorHandler
285 exitCode = (yield Promise.resolve(this.errorHandler(parsedCommand))) || 1;
286 }
287 else {
288 // set process env from command env settings
289 this.setProcessEnvFromParsedCommand();
290 // call the handler with parsed command
291 exitCode = yield this.runHandler(parsedCommand);
292 }
293 }
294 return {
295 parsedCommand,
296 exitCode: typeof exitCode === 'number' ? exitCode : 0,
297 };
298 });
299 this._commandPackage = program_1.CliProgram.create(commandPackageDefinition, internalOptions);
300 if (this._commandPackage.validationErrors.length > 0) {
301 throw new Error(`Invalid Command Definition: ${this._commandPackage.validationErrors.join(os.EOL)}`);
302 }
303 this._definitionsMap = this._commandPackage.commandsMap;
304 }
305 get parsedCommand() {
306 if (!this._parsedCommand) {
307 this._parsedCommand = {};
308 this._parsedCommand.errors = this._parserErrors;
309 this._parsedCommand.program = this._commandPackage;
310 this._parsedCommand.run = this.runner;
311 this._parsedCommand.version = this.version;
312 this.parseCommandLine();
313 this.parseArgsAndOptions();
314 this._parsedCommand.env = this.getEnv();
315 }
316 return this._parsedCommand;
317 }
318}
319exports.Parser = Parser;
320//# sourceMappingURL=Parser.js.map
\No newline at end of file