berlioz
Version:
Berlioz - cloud deployment and migration services
465 lines (411 loc) • 13.7 kB
JavaScript
;
/**
* 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 === undefined;
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;