-
Notifications
You must be signed in to change notification settings - Fork 278
/
util.js
executable file
·464 lines (411 loc) · 13.4 KB
/
util.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
'use strict';
/**
* Module dependencies.
*/
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _ = require('lodash');
var minimist = require('minimist');
var strip = require('strip-ansi');
var util = {
/**
* Parses command arguments from multiple
* sources.
*
* @param {String} str
* @param {Object} opts
* @return {Array}
* @api private
*/
parseArgs: function parseArgs(str, opts) {
var reg = /"(.*?)"|'(.*?)'|`(.*?)`|([^\s"]+)/gi;
var arr = [];
var match = void 0;
do {
match = reg.exec(str);
if (match !== null) {
arr.push(match[1] || match[2] || match[3] || match[4]);
}
} while (match !== null);
arr = minimist(arr, opts);
arr._ = arr._ || [];
return arr;
},
/**
* Prepares a command and all its parts for execution.
*
* @param {String} command
* @param {Array} commands
* @return {Object}
* @api public
*/
parseCommand: function parseCommand(command, commands) {
var self = this;
var pipes = [];
var match = void 0;
var matchArgs = void 0;
var matchParts = void 0;
function parsePipes() {
// First, split the command by pipes naively.
// This will split command arguments in half when the argument contains a pipe character.
// For example, say "(Vorpal|vorpal)" will be split into ['say "(Vorpal', 'vorpal)'] which isn't good.
var naivePipes = String(command).trim().split('|');
// Contruct empty array to place correctly split commands into.
var newPipes = [];
// We will look for pipe characters within these quotes to rejoin together.
var quoteChars = ['"', '\'', '`'];
// This will expand to contain one boolean key for each type of quote.
// The value keyed by the quote is toggled off and on as quote type is opened and closed.
// Example { "`": true, "'": false } would mean that there is an open angle quote.
var quoteTracker = {};
// The current command piece before being rejoined with it's over half.
// Since it's not common for pipes to occur in commands, this is usually the entire command pipe.
var commandPart = '';
// Loop through each naive pipe.
for (var key in naivePipes) {
// It's possible/likely that this naive pipe is the whole pipe if it doesn't contain an unfinished quote.
var possiblePipe = naivePipes[key];
commandPart += possiblePipe;
// Loop through each individual character in the possible pipe tracking the opening and closing of quotes.
for (var i = 0; i < possiblePipe.length; i++) {
var char = possiblePipe[i];
if (quoteChars.indexOf(char) !== -1) {
quoteTracker[char] = !quoteTracker[char];
}
}
// Does the pipe end on an unfinished quote?
var inQuote = _.some(quoteChars, function (quoteChar) {
return quoteTracker[quoteChar];
});
// If the quotes have all been closed or this is the last possible pipe in the array, add as pipe.
if (!inQuote || key * 1 === naivePipes.length - 1) {
newPipes.push(commandPart.trim());
commandPart = '';
} else {
// Quote was left open. The pipe character was previously removed when the array was split.
commandPart += '|';
}
}
// Set the first pipe to command and the rest to pipes.
command = newPipes.shift();
pipes = pipes.concat(newPipes);
}
function parseMatch() {
matchParts = self.matchCommand(command, commands);
match = matchParts.command;
matchArgs = matchParts.args;
}
parsePipes();
parseMatch();
if (match && _.isFunction(match._parse)) {
command = match._parse(command, matchParts.args);
parsePipes();
parseMatch();
}
return {
command: command,
match: match,
matchArgs: matchArgs,
pipes: pipes
};
},
/**
* Run a raw command string, e.g. foo -bar
* against a given list of commands,
* and if there is a match, parse the
* results.
*
* @param {String} cmd
* @param {Array} cmds
* @return {Object}
* @api public
*/
matchCommand: function matchCommand(cmd, cmds) {
var parts = String(cmd).trim().split(' ');
var match = void 0;
var matchArgs = void 0;
for (var i = 0; i < parts.length; ++i) {
var subcommand = String(parts.slice(0, parts.length - i).join(' ')).trim();
match = _.find(cmds, { _name: subcommand }) || match;
if (!match) {
for (var key in cmds) {
var _cmd = cmds[key];
var idx = _cmd._aliases.indexOf(subcommand);
match = idx > -1 ? _cmd : match;
}
}
if (match) {
matchArgs = parts.slice(parts.length - i, parts.length).join(' ');
break;
}
}
// If there's no command match, check if the
// there's a `catch` command, which catches all
// missed commands.
if (!match) {
match = _.find(cmds, { _catch: true });
// If there is one, we still need to make sure we aren't
// partially matching command groups, such as `do things` when
// there is a command `do things well`. If we match partially,
// we still want to show the help menu for that command group.
if (match) {
var allCommands = _.map(cmds, '_name');
var wordMatch = false;
for (var _key in allCommands) {
var _cmd2 = allCommands[_key];
var parts2 = String(_cmd2).split(' ');
var cmdParts = String(match.command).split(' ');
var matchAll = true;
for (var k = 0; k < cmdParts.length; ++k) {
if (parts2[k] !== cmdParts[k]) {
matchAll = false;
break;
}
}
if (matchAll) {
wordMatch = true;
break;
}
}
if (wordMatch) {
match = undefined;
} else {
matchArgs = cmd;
}
}
}
return {
command: match,
args: matchArgs
};
},
buildCommandArgs: function buildCommandArgs(passedArgs, cmd, execCommand, isCommandArgKeyPairNormalized) {
var args = { options: {} };
if (isCommandArgKeyPairNormalized) {
// Normalize all foo="bar" with "foo='bar'"
// This helps implement unix-like key value pairs.
var reg = /(['"]?)(\w+)=(?:(['"])((?:(?!\3).)*)\3|(\S+))\1/g;
passedArgs = passedArgs.replace(reg, '"$2=\'$4$5\'"');
}
// Types are custom arg types passed
// into `minimist` as per its docs.
var types = cmd._types || {};
// Make a list of all boolean options
// registered for this command. These are
// simply commands that don't have required
// or optional args.
var booleans = [];
cmd.options.forEach(function (opt) {
if (opt.required === 0 && opt.optional === 0) {
if (opt.short) {
booleans.push(opt.short);
}
if (opt.long) {
booleans.push(opt.long);
}
}
});
// Review the args passed into the command,
// and filter out the boolean list to only those
// options passed in.
// This returns a boolean list of all options
// passed in by the caller, which don't have
// required or optional args.
var passedArgParts = passedArgs.split(' ');
types.boolean = booleans.map(function (str) {
return String(str).replace(/^-*/, '');
}).filter(function (str) {
var match = false;
var strings = ['-' + str, '--' + str, '--no-' + str];
for (var i = 0; i < passedArgParts.length; ++i) {
if (strings.indexOf(passedArgParts[i]) > -1) {
match = true;
break;
}
}
return match;
});
// Use minimist to parse the args.
var parsedArgs = this.parseArgs(passedArgs, types);
function validateArg(arg, cmdArg) {
return !(arg === undefined && cmdArg.required === true);
}
// Builds varidiac args and options.
var valid = true;
var remainingArgs = _.clone(parsedArgs._);
for (var l = 0; l < 10; ++l) {
var matchArg = cmd._args[l];
var passedArg = parsedArgs._[l];
if (matchArg !== undefined) {
valid = !valid ? false : validateArg(parsedArgs._[l], matchArg);
if (!valid) {
break;
}
if (passedArg !== undefined) {
if (matchArg.variadic === true) {
args[matchArg.name] = remainingArgs;
} else {
args[matchArg.name] = passedArg;
remainingArgs.shift();
}
}
}
}
if (!valid) {
return '\n Missing required argument. Showing Help:';
}
// Looks for ommitted required options and throws help.
for (var m = 0; m < cmd.options.length; ++m) {
var o = cmd.options[m];
var short = String(o.short || '').replace(/-/g, '');
var long = String(o.long || '').replace(/--no-/g, '').replace(/^-*/g, '');
var exist = parsedArgs[short] !== undefined ? parsedArgs[short] : undefined;
exist = exist === undefined && parsedArgs[long] !== undefined ? parsedArgs[long] : exist;
var existsNotSet = exist === true || exist === false;
if (existsNotSet && o.required !== 0) {
return '\n Missing required value for option ' + (o.long || o.short) + '. Showing Help:';
}
if (exist !== undefined) {
args.options[long || short] = exist;
}
}
// Looks for supplied options that don't
// exist in the options list.
// If the command allows unknown options,
// adds it, otherwise throws help.
var passedOpts = _.chain(parsedArgs).keys().pull('_').pull('help').value();
var _loop = function _loop(key) {
var opt = passedOpts[key];
var optionFound = _.find(cmd.options, function (expected) {
if ('--' + opt === expected.long || '--no-' + opt === expected.long || '-' + opt === expected.short) {
return true;
}
return false;
});
if (optionFound === undefined) {
if (cmd._allowUnknownOptions) {
args.options[opt] = parsedArgs[opt];
} else {
return {
v: '\n Invalid option: \'' + opt + '\'. Showing Help:'
};
}
}
};
for (var key in passedOpts) {
var _ret = _loop(key);
if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v;
}
// If args were passed into the programmatic
// `vorpal.exec(cmd, args, callback)`, merge
// them here.
if (execCommand && execCommand.args && _.isObject(execCommand.args)) {
args = _.extend(args, execCommand.args);
}
// Looks for a help arg and throws help if any.
if (parsedArgs.help || parsedArgs._.indexOf('/?') > -1) {
args.options.help = true;
}
return args;
},
/**
* Makes an argument name pretty for help.
*
* @param {String} arg
* @return {String}
* @api private
*/
humanReadableArgName: function humanReadableArgName(arg) {
var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']';
},
/**
* Formats an array to display in a TTY
* in a pretty fashion.
*
* @param {Array} arr
* @return {String}
* @api public
*/
prettifyArray: function prettifyArray(arr) {
arr = arr || [];
var arrClone = _.clone(arr);
var width = process.stdout.columns;
var longest = strip(arrClone.sort(function (a, b) {
return strip(b).length - strip(a).length;
})[0] || '').length + 2;
var fullWidth = strip(String(arr.join(''))).length;
var fitsOneLine = fullWidth + arr.length * 2 <= width;
var cols = Math.floor(width / longest);
cols = cols < 1 ? 1 : cols;
if (fitsOneLine) {
return arr.join(' ');
}
var col = 0;
var lines = [];
var line = '';
for (var key in arr) {
var arrEl = arr[key];
if (col < cols) {
col++;
} else {
lines.push(line);
line = '';
col = 1;
}
line += this.pad(arrEl, longest, ' ');
}
if (line !== '') {
lines.push(line);
}
return lines.join('\n');
},
/**
* Pads a value with with space or
* a specified delimiter to match a
* given width.
*
* @param {String} str
* @param {Integer} width
* @param {String} delimiter
* @return {String}
* @api private
*/
pad: function pad(str, width, delimiter) {
width = Math.floor(width);
delimiter = delimiter || ' ';
var len = Math.max(0, width - strip(str).length);
return str + Array(len + 1).join(delimiter);
},
/**
* Pad a row on the start and end with spaces.
*
* @param {String} str
* @return {String}
*/
padRow: function padRow(str) {
return str.split('\n').map(function (row) {
return ' ' + row + ' ';
}).join('\n');
},
// When passing down applied args, we need to turn
// them from `{ '0': 'foo', '1': 'bar' }` into ['foo', 'bar']
// instead.
fixArgsForApply: function fixArgsForApply(obj) {
if (!_.isObject(obj)) {
if (!_.isArray(obj)) {
return [obj];
}
return obj;
}
var argArray = [];
for (var key in obj) {
var aarg = obj[key];
argArray.push(aarg);
}
return argArray;
}
};
/**
* Expose `util`.
*/
module.exports = exports = util;