vorpal
Version:
Node's first framework for building immersive CLI apps.
574 lines (497 loc) • 11.1 kB
JavaScript
'use strict';
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter;
var Option = require('./option');
var VorpalUtil = require('./util');
var _ = require('lodash');
/**
* Command prototype.
*/
var command = Command.prototype;
/**
* Expose `Command`.
*/
module.exports = exports = Command;
/**
* Initialize a new `Command` instance.
*
* @param {String} name
* @param {Vorpal} parent
* @return {Command}
* @api public
*/
function Command(name, parent) {
if (!(this instanceof Command)) {
return new Command();
}
this.commands = [];
this.options = [];
this._allowUnknownOption = false;
this._args = [];
this._aliases = [];
this._name = name;
this._relay = false;
this._hidden = false;
this._parent = parent;
this._mode = false;
this._catch = false;
this._help = undefined;
this._init = undefined;
this._after = undefined;
}
/**
* Registers an option for given command.
*
* @param {String} flags
* @param {String} description
* @param {Function} fn
* @param {String} defaultValue
* @return {Command}
* @api public
*/
command.option = function (flags, description, fn, defaultValue) {
var self = this;
var option = new Option(flags, description);
var oname = option.name();
var name = _camelcase(oname);
// default as 3rd arg
if (typeof fn !== 'function') {
if (fn instanceof RegExp) {
var regex = fn;
fn = function (val, def) {
var m = regex.exec(val);
return m ? m[0] : def;
};
} else {
defaultValue = fn;
fn = null;
}
}
// preassign default value only for --no-*, [optional], or <required>
if (option.bool === false || option.optional || option.required) {
// when --no-* we make sure default is true
if (option.bool === false) {
defaultValue = true;
}
// preassign only if we have a default
if (defaultValue !== undefined) {
self[name] = defaultValue;
}
}
// register the option
this.options.push(option);
// when it's passed assign the value
// and conditionally invoke the callback
this.on(oname, function (val) {
// coercion
if (val !== null && fn) {
val = fn(val, self[name] === undefined ?
defaultValue :
self[name]);
}
// unassigned or bool
if (typeof self[name] === 'boolean' || typeof self[name] === 'undefined') {
// if no value, bool true, and we have a default, then use it!
if (val === null) {
self[name] = option.bool ?
defaultValue || true :
false;
} else {
self[name] = val;
}
} else if (val !== null) {
// reassign
self[name] = val;
}
});
return this;
};
/**
* Defines an action for a given command.
*
* @param {Function} fn
* @return {Command}
* @api public
*/
command.action = function (fn) {
var self = this;
self._fn = fn;
return this;
};
/**
* Defines a function to validate arguments
* before action is performed. Arguments
* are valid if no errors are thrown from
* the function.
*
* @param fn
* @returns {Command}
* @api public
*/
command.validate = function (fn) {
var self = this;
self._validate = fn;
return this;
};
/**
* Defines a function to be called when the
* command is canceled.
*
* @param fn
* @returns {Command}
* @api public
*/
command.cancel = function (fn) {
this._cancel = fn;
return this;
};
/**
* Defines a method to be called when
* the command set has completed.
*
* @param {Function} fn
* @return {Command}
* @api public
*/
command.done = function (fn) {
this._done = fn;
return this;
};
/**
* Defines tabbed auto-completion rules
* for the given command.
*
* @param {Function} fn
* @return {Command}
* @api public
*/
command.autocompletion = function (fn) {
if (!_.isFunction(fn)) {
throw new Error('An invalid object type was passed into the first parameter of command.autocompletion: function expected.');
}
this._autocompletion = fn;
return this;
};
/**
* Defines an init action for a mode command.
*
* @param {Function} fn
* @return {Command}
* @api public
*/
command.init = function (fn) {
var self = this;
if (self._mode !== true) {
throw Error('Cannot call init from a non-mode action.');
}
self._init = fn;
return this;
};
/**
* Defines a prompt delimiter for a
* mode once entered.
*
* @param {String} delimiter
* @return {Command}
* @api public
*/
command.delimiter = function (delimiter) {
this._delimiter = delimiter;
return this;
};
/**
* Sets args for static typing of options
* using minimist.
*
* @param {Object} types
* @return {Command}
* @api public
*/
command.types = function (types) {
var supported = ['string', 'boolean'];
for (var item in types) {
if (supported.indexOf(item) === -1) {
throw new Error('An invalid type was passed into command.types(): ' + item);
}
types[item] = (!_.isArray(types[item])) ? [types[item]] : types[item];
}
this._types = types;
return this;
};
/**
* Defines an alias for a given command.
*
* @param {String} alias
* @return {Command}
* @api public
*/
command.alias = function () {
var self = this;
for (var i = 0; i < arguments.length; ++i) {
var alias = arguments[i];
if (_.isArray(alias)) {
for (var j = 0; j < alias.length; ++j) {
this.alias(alias[j]);
}
return this;
}
this._parent.commands.forEach(function (cmd) {
if (!_.isEmpty(cmd._aliases)) {
if (_.contains(cmd._aliases, alias)) {
var msg = 'Duplicate alias "' + alias + '" for command "' + self._name + '" detected. Was first reserved by command "' + cmd._name + '".';
throw new Error(msg);
}
}
});
this._aliases.push(alias);
}
return this;
};
/**
* Defines description for given command.
*
* @param {String} str
* @return {Command}
* @api public
*/
command.description = function (str) {
if (arguments.length === 0) {
return this._description;
}
this._description = str;
return this;
};
/**
* Removes self from Vorpal instance.
*
* @return {Command}
* @api public
*/
command.remove = function () {
var self = this;
this._parent.commands = _.reject(this._parent.commands, function (command) {
if (command._name === self._name) {
return true;
}
});
return this;
};
/**
* Returns the commands arguments as string.
*
* @param {String} desc
* @return {String}
* @api public
*/
command.arguments = function (desc) {
return this._parseExpectedArgs(desc.split(/ +/));
};
/**
* Returns the help info for given command.
*
* @return {String}
* @api public
*/
command.helpInformation = function () {
var desc = [];
var cmdName = this._name;
var alias = '';
if (this._description) {
desc = [
' ' + this._description,
''
];
}
if (this._aliases.length > 0) {
alias = ' Alias: ' + this._aliases.join(' | ') + '\n';
}
var usage = [
'',
' Usage: ' + cmdName + ' ' + this.usage(),
''
];
var cmds = [];
var help = String(this.optionHelp().replace(/^/gm, ' '));
var options = [
' Options:',
'',
help,
''
];
var res = usage
.concat(cmds)
.concat(alias)
.concat(desc)
.concat(options)
.join('\n');
return res;
};
/**
* Doesn't show command in the help menu.
*
* @return {Command}
* @api public
*/
command.hidden = function () {
this._hidden = true;
return this;
};
/**
* Returns the command usage string for help.
*
* @param {String} str
* @return {String}
* @api public
*/
command.usage = function (str) {
var args = this._args.map(function (arg) {
return VorpalUtil.humanReadableArgName(arg);
});
var usage = '[options]' +
(this.commands.length ? ' [command]' : '') +
(this._args.length ? ' ' + args.join(' ') : '');
if (arguments.length === 0) {
return (this._usage || usage);
}
this._usage = str;
return this;
};
/**
* Returns the help string for the command's options.
*
* @return {String}
* @api public
*/
command.optionHelp = function () {
var width = this._largestOptionLength();
// Prepend the help information
return [VorpalUtil.pad('--help', width) + ' output usage information']
.concat(this.options.map(function (option) {
return VorpalUtil.pad(option.flags, width) + ' ' + option.description;
}))
.join('\n');
};
/**
* Returns the length of the longest option.
*
* @return {Integer}
* @api private
*/
command._largestOptionLength = function () {
return this.options.reduce(function (max, option) {
return Math.max(max, option.flags.length);
}, 0);
};
/**
* Adds a custom handling for the --help flag.
*
* @param {Function} fn
* @return {Command}
* @api public
*/
command.help = function (fn) {
if (_.isFunction(fn)) {
this._help = fn;
}
return this;
};
/**
* Edits the raw command string before it
* is executed.
*
* @param {String} str
* @return {String} str
* @api public
*/
command.parse = function (fn) {
if (_.isFunction(fn)) {
this._parse = fn;
}
return this;
};
/**
* Adds a command to be executed after command completion.
*
* @param {Function} fn
* @return {Command}
* @api public
*/
command.after = function (fn) {
if (_.isFunction(fn)) {
this._after = fn;
}
return this;
};
/**
* Parses and returns expected command arguments.
*
* @param {String} args
* @return {Array}
* @api private
*/
command._parseExpectedArgs = function (args) {
if (!args.length) {
return;
}
var self = this;
args.forEach(function (arg) {
var argDetails = {
required: false,
name: '',
variadic: false
};
switch (arg[0]) {
case '<':
argDetails.required = true;
argDetails.name = arg.slice(1, -1);
break;
case '[':
argDetails.name = arg.slice(1, -1);
break;
default:
break;
}
if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
argDetails.variadic = true;
argDetails.name = argDetails.name.slice(0, -3);
}
if (argDetails.name) {
self._args.push(argDetails);
}
});
// If the user entered args in a weird order,
// properly sequence them.
if (self._args.length > 1) {
self._args = self._args.sort(function (argu1, argu2) {
if (argu1.required && !argu2.required) {
return -1;
} else if (argu2.required && !argu1.required) {
return 1;
} else if (argu1.variadic && !argu2.variadic) {
return 1;
} else if (argu2.variadic && !argu1.variadic) {
return -1;
}
return 0;
});
}
return;
};
/**
* Converts string to camel case.
*
* @param {String} flag
* @return {String}
* @api private
*/
function _camelcase(flag) {
return flag.split('-').reduce(function (str, word) {
return str + word[0].toUpperCase() + word.slice(1);
});
}
/**
* Make command an EventEmitter.
*/
command.__proto__ = EventEmitter.prototype;