UNPKG

10.6 kBJavaScriptView Raw
1"use strict";
2
3const GetterSetter = require('./utils').GetterSetter;
4const Option = require('./option');
5const Argument = require('./argument');
6const UnknownOptionError = require('./error/unknown-option');
7const InvalidOptionValueError = require('./error/invalid-option-value');
8const InvalidArgumentValueError = require('./error/invalid-argument-value');
9const MissingOptionError = require('./error/missing-option');
10const NoActionError = require('./error/no-action-error');
11const WrongNumberOfArgumentError = require('./error/wrong-num-of-arg');
12
13/**
14 * Command class
15 */
16class Command extends GetterSetter {
17
18 /**
19 *
20 * @param {String|null} name - Command name.
21 * @param {String} description - Command description
22 * @param {Program} program Program instance
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 * @private
42 * @returns {Command.command|null|Program.command|string|*}
43 */
44 name() {
45 return this._name === '_default' ? '' : this._name;
46 }
47
48 /**
49 * @private
50 * @returns {Argument[]}
51 */
52 args(index) {
53 return typeof index !== 'undefined' ? this._args[index] : this._args;
54 }
55
56 /**
57 * @private
58 * @returns {Option[]}
59 */
60 options() {
61 return this._options;
62 }
63
64 /**
65 * @private
66 */
67 getSynopsis() {
68 return this.name() + ' ' + (this.args().map(a => a.synopsis()).join(' '));
69 }
70
71 /**
72 * @private
73 * @returns {null|String}
74 */
75 getAlias() {
76 return this._alias;
77 }
78
79 /**
80 * Add command argument
81 *
82 * @param {String} synopsis - Argument synopsis like `<my-argument>` or `[my-argument]`.
83 * Angled brackets (e.g. `<item>`) indicate required input. Square brackets (e.g. `[env]`) indicate optional input.
84 * @param {String} description - Option description
85 * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting
86 * @param {*} [defaultValue] - Default value
87 * @public
88 * @returns {Command}
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 * @returns {Number}
100 * @private
101 */
102 _requiredArgsCount() {
103 return this.args().filter(a => a.isRequired()).length;
104 }
105
106 /**
107 *
108 * @returns {Number}
109 * @private
110 */
111 _optionalArgsCount() {
112 return this.args().filter(a => a.isOptional()).length;
113 }
114
115 /**
116 *
117 * @returns {{min: Number, max: *}}
118 * @private
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 * @param optName
129 * @returns {Option|undefined}
130 * @private
131 */
132 _findOption(optName) {
133 return this._options.find(o => (o.getShortCleanName() === optName || o.getLongCleanName() === optName));
134 }
135
136
137 /**
138 *
139 * @param {String} name - Argument name
140 * @returns {Argument|undefined}
141 * @private
142 */
143 _findArgument(name) {
144 return this._args.find(a => a.name() === name);
145 }
146
147 /**
148 * @private
149 */
150 _getLongOptions() {
151 return this._options.map(opt => opt.getLongCleanName()).filter(o => typeof o !== 'undefined');
152 }
153
154 /**
155 * Allow chaining command() with other .command()
156 * @returns {Command}
157 */
158 command() {
159 return this._program.command.apply(this._program, arguments);
160 }
161
162 /**
163 * @private
164 * @returns {boolean}
165 */
166 _hasVariadicArguments() {
167 return this.args().find(a => a.isVariadic()) !== undefined;
168 }
169
170 /**
171 *
172 * @param {Array} args
173 * @return {Object}
174 * @private
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 * @param {Array} args
191 * @returns {Array}
192 * @private
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 * @param {Array} argsArr
208 * @private
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 * @param args
227 * @returns {*}
228 * @private
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 * @param options
246 * @returns {*}
247 * @private
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 * @param options
265 * @returns {*}
266 * @private
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 * @param options
290 * @returns {*}
291 * @private
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 * @param options
309 * @returns {*}
310 * @private
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 * @param args
324 * @param options
325 * @returns {*}
326 * @private
327 */
328 _validateCall(args, options) {
329 // check min & max arguments accepted
330 this._checkArgsRange(args);
331 // split args
332 args = this._splitArgs(args);
333 // transfrom args array to object, and set defaults for arguments not passed
334 args = this._argsArrayToObject(args);
335 // arguments validation
336 args = this._validateArgs(args);
337 // check required options
338 options = this._checkRequiredOptions(options);
339 // options validation
340 options = this._validateOptions(options);
341 // add long notation if exists
342 options = this._addLongNotationToOptions(options);
343 // camelcase options
344 options = this._camelCaseOptions(options);
345 return {args, options};
346 }
347
348
349 /**
350 * Add an option
351 *
352 * @param {String} synopsis - Option synopsis like '-f, --force', or '-f, --file <file>', or '--with-openssl [path]'
353 * @param {String} description - Option description
354 * @param {String|RegExp|Function|Number} [validator] - Option validator, used for checking or casting
355 * @param {*} [defaultValue] - Default value
356 * @param {Boolean} [required] - Is the option itself required
357 * @public
358 * @returns {Command}
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 * Set the corresponding action to execute for this command
369 *
370 * @param {Function} action - Action to execute
371 * @returns {Command}
372 * @public
373 */
374 action(action) {
375 this._action = action;
376 return this;
377 }
378
379 /**
380 * Run the command's action
381 *
382 * @param {Object} args - Arguments
383 * @param {Object} options - Options
384 * @returns {*}
385 * @private
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 * @private
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 * Set an alias for this command. Only one alias can be set up for a command
411 *
412 * @param {String} alias - Alias
413 * @returns {Command}
414 * @public
415 */
416 alias(alias) {
417 this._alias = alias;
418 return this;
419 }
420
421 /**
422 * Autocomplete callabck
423 */
424 complete(callback) {
425 this._program._autocomplete.registerCompletion(this._lastAddedArgOrOpt, callback);
426 return this;
427 }
428
429}
430
431Object.defineProperties(Command, {
432 "NATIVE_OPTIONS": {
433 value: ['h', 'help', 'V', 'version', 'no-color', 'quiet', 'silent', 'v', 'verbose', 'autocomplete']
434 }
435});
436
437module.exports = Command;