UNPKG

berlioz

Version:

Berlioz - cloud deployment and migration services

465 lines (411 loc) 13.7 kB
'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 === 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;