1 | "use strict";
|
2 |
|
3 | const GetterSetter = require('./utils').GetterSetter;
|
4 | const Option = require('./option');
|
5 | const Argument = require('./argument');
|
6 | const UnknownOptionError = require('./error/unknown-option');
|
7 | const InvalidOptionValueError = require('./error/invalid-option-value');
|
8 | const InvalidArgumentValueError = require('./error/invalid-argument-value');
|
9 | const MissingOptionError = require('./error/missing-option');
|
10 | const NoActionError = require('./error/no-action-error');
|
11 | const WrongNumberOfArgumentError = require('./error/wrong-num-of-arg');
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | class Command extends GetterSetter {
|
17 |
|
18 | |
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | constructor(name, description, program) {
|
25 | super();
|
26 | this._name = name;
|
27 | this._description = description;
|
28 | this._options = [];
|
29 | this._program = program;
|
30 | this._logger = this._program.logger();
|
31 | this._alias = null;
|
32 | this.description = this.makeGetterSetter('description');
|
33 | this._args = [];
|
34 | this._lastAddedArgOrOpt = null;
|
35 | this._setupLoggerMethods();
|
36 | }
|
37 |
|
38 |
|
39 |
|
40 | |
41 |
|
42 |
|
43 |
|
44 | name() {
|
45 | return this._name === '_default' ? '' : this._name;
|
46 | }
|
47 |
|
48 | |
49 |
|
50 |
|
51 |
|
52 | args(index) {
|
53 | return typeof index !== 'undefined' ? this._args[index] : this._args;
|
54 | }
|
55 |
|
56 | |
57 |
|
58 |
|
59 |
|
60 | options() {
|
61 | return this._options;
|
62 | }
|
63 |
|
64 | |
65 |
|
66 |
|
67 | getSynopsis() {
|
68 | return this.name() + ' ' + (this.args().map(a => a.synopsis()).join(' '));
|
69 | }
|
70 |
|
71 | |
72 |
|
73 |
|
74 |
|
75 | getAlias() {
|
76 | return this._alias;
|
77 | }
|
78 |
|
79 | |
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | argument(synopsis, description, validator, defaultValue) {
|
91 | const arg = new Argument(synopsis, description, validator, defaultValue, this._program);
|
92 | this._lastAddedArgOrOpt = arg;
|
93 | this._args.push(arg);
|
94 | return this;
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 | _requiredArgsCount() {
|
103 | return this.args().filter(a => a.isRequired()).length;
|
104 | }
|
105 |
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 | _optionalArgsCount() {
|
112 | return this.args().filter(a => a.isOptional()).length;
|
113 | }
|
114 |
|
115 | |
116 |
|
117 |
|
118 |
|
119 |
|
120 | _acceptedArgsRange() {
|
121 | const min = this._requiredArgsCount();
|
122 | const max = this._hasVariadicArguments() ? Infinity : (this._requiredArgsCount() + this._optionalArgsCount());
|
123 | return {min, max};
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | _findOption(optName) {
|
133 | return this._options.find(o => (o.getShortCleanName() === optName || o.getLongCleanName() === optName));
|
134 | }
|
135 |
|
136 |
|
137 | |
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | _findArgument(name) {
|
144 | return this._args.find(a => a.name() === name);
|
145 | }
|
146 |
|
147 | |
148 |
|
149 |
|
150 | _getLongOptions() {
|
151 | return this._options.map(opt => opt.getLongCleanName()).filter(o => typeof o !== 'undefined');
|
152 | }
|
153 |
|
154 | |
155 |
|
156 |
|
157 |
|
158 | command() {
|
159 | return this._program.command.apply(this._program, arguments);
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 | _hasVariadicArguments() {
|
167 | return this.args().find(a => a.isVariadic()) !== undefined;
|
168 | }
|
169 |
|
170 | |
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 | _argsArrayToObject(args) {
|
177 | return this.args().reduce((acc, arg, index) => {
|
178 | if (typeof args[index] !== 'undefined') {
|
179 | acc[arg.name()] = args[index];
|
180 | } else if(arg.hasDefault()) {
|
181 | acc[arg.name()] = arg.default();
|
182 | }
|
183 | return acc;
|
184 | }, {});
|
185 |
|
186 | }
|
187 |
|
188 | |
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | _splitArgs(args) {
|
195 | return this.args().reduce((acc, arg) => {
|
196 | if (arg.isVariadic()) {
|
197 | acc.push(args.slice());
|
198 | } else {
|
199 | acc.push(args.shift());
|
200 | }
|
201 | return acc;
|
202 | }, []);
|
203 | }
|
204 |
|
205 | |
206 |
|
207 |
|
208 |
|
209 |
|
210 | _checkArgsRange(argsArr) {
|
211 | const range = this._acceptedArgsRange();
|
212 | const argsCount = argsArr.length;
|
213 | if (argsCount < range.min || argsCount> range.max) {
|
214 | const expArgsStr = range.min === range.max ? `exactly ${range.min}.` : `between ${range.min} and ${range.max}.`;
|
215 | throw new WrongNumberOfArgumentError(
|
216 | "Wrong number of argument(s)" + (this.name() ? ' for command ' + this.name() : '') +
|
217 | `. Got ${argsCount}, expected ` + expArgsStr,
|
218 | {},
|
219 | this._program
|
220 | )
|
221 | }
|
222 | }
|
223 |
|
224 | |
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 | _validateArgs(args) {
|
231 | return Object.keys(args).reduce((acc, key) => {
|
232 | const arg = this._findArgument(key);
|
233 | const value = args[key];
|
234 | try {
|
235 | acc[key] = arg._validate(value);
|
236 | } catch(e) {
|
237 | throw new InvalidArgumentValueError(key, value, this, this._program);
|
238 | }
|
239 | return acc;
|
240 | }, {});
|
241 | }
|
242 |
|
243 | |
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 | _checkRequiredOptions(options) {
|
250 | return this._options.reduce((acc, opt) => {
|
251 | if (typeof acc[opt.getLongCleanName()] === 'undefined' && typeof acc[opt.getShortCleanName()] === 'undefined') {
|
252 | if (opt.hasDefault()) {
|
253 | acc[opt.getLongCleanName()] = opt.default();
|
254 | } else if (opt.isRequired()) {
|
255 | throw new MissingOptionError(opt.getLongOrShortCleanName(), this, this._program);
|
256 | }
|
257 | }
|
258 | return acc;
|
259 | }, options);
|
260 | }
|
261 |
|
262 | |
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | _validateOptions(options) {
|
269 | return Object.keys(options).reduce((acc, key) => {
|
270 | if (Command.NATIVE_OPTIONS.indexOf(key) !== -1) {
|
271 | return acc;
|
272 | }
|
273 | const value = acc[key];
|
274 | const opt = this._findOption(key);
|
275 | if (!opt) {
|
276 | throw new UnknownOptionError(key, this, this._program);
|
277 | }
|
278 | try {
|
279 | acc[key] = opt._validate(value);
|
280 | } catch(e) {
|
281 | throw new InvalidOptionValueError(key, value, this, e, this._program);
|
282 | }
|
283 | return acc;
|
284 | }, options);
|
285 | }
|
286 |
|
287 | |
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 | _addLongNotationToOptions(options) {
|
294 | return Object.keys(options).reduce((acc, key) => {
|
295 | if (key.length === 1) {
|
296 | const value = acc[key];
|
297 | const opt = this._findOption(key);
|
298 | if (opt && opt.getLongCleanName()) {
|
299 | acc[opt.getLongCleanName()] = value;
|
300 | }
|
301 | }
|
302 | return acc;
|
303 | }, options);
|
304 | }
|
305 |
|
306 | |
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 | _camelCaseOptions(options) {
|
313 | return this._options.reduce((acc, opt) => {
|
314 | if (typeof options[opt.getLongCleanName()] !== 'undefined') {
|
315 | acc[opt.name()] = options[opt.getLongCleanName()];
|
316 | }
|
317 | return acc;
|
318 | }, {});
|
319 | }
|
320 |
|
321 | |
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 | _validateCall(args, options) {
|
329 |
|
330 | this._checkArgsRange(args);
|
331 |
|
332 | args = this._splitArgs(args);
|
333 |
|
334 | args = this._argsArrayToObject(args);
|
335 |
|
336 | args = this._validateArgs(args);
|
337 |
|
338 | options = this._checkRequiredOptions(options);
|
339 |
|
340 | options = this._validateOptions(options);
|
341 |
|
342 | options = this._addLongNotationToOptions(options);
|
343 |
|
344 | options = this._camelCaseOptions(options);
|
345 | return {args, options};
|
346 | }
|
347 |
|
348 |
|
349 | |
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 | option(synopsis, description, validator, defaultValue, required) {
|
361 | const opt = new Option(synopsis, description, validator, defaultValue, required, this._program);
|
362 | this._lastAddedArgOrOpt = opt;
|
363 | this._options.push(opt);
|
364 | return this;
|
365 | }
|
366 |
|
367 | |
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 | action(action) {
|
375 | this._action = action;
|
376 | return this;
|
377 | }
|
378 |
|
379 | |
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | _run(args, options) {
|
388 | if (!this._action) {
|
389 | return this._program.fatalError(new NoActionError(
|
390 | "Caporal Setup Error: You don't have defined an action for you program/command. Use .action()",
|
391 | {},
|
392 | this._program
|
393 | ));
|
394 | }
|
395 | return this._action.apply(this, [args, options, this._logger]);
|
396 | }
|
397 |
|
398 | |
399 |
|
400 |
|
401 |
|
402 | _setupLoggerMethods() {
|
403 | ['error', 'warn', 'info', 'log', 'debug'].forEach(function(lev) {
|
404 | const overrideLevel = (lev === 'log') ? 'info' : lev;
|
405 | this[lev] = this._logger[overrideLevel].bind(this._logger);
|
406 | }, this);
|
407 | }
|
408 |
|
409 | |
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 |
|
416 | alias(alias) {
|
417 | this._alias = alias;
|
418 | return this;
|
419 | }
|
420 |
|
421 | |
422 |
|
423 |
|
424 | complete(callback) {
|
425 | this._program._autocomplete.registerCompletion(this._lastAddedArgOrOpt, callback);
|
426 | return this;
|
427 | }
|
428 |
|
429 | }
|
430 |
|
431 | Object.defineProperties(Command, {
|
432 | "NATIVE_OPTIONS": {
|
433 | value: ['h', 'help', 'V', 'version', 'no-color', 'quiet', 'silent', 'v', 'verbose', 'autocomplete']
|
434 | }
|
435 | });
|
436 |
|
437 | module.exports = Command;
|