tilda
Version:
Tiny module for building command line tools.
711 lines (621 loc) • 25.3 kB
JavaScript
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var events = require("events"),
ul = require("ul"),
clp = require("clp"),
iterateObject = require("iterate-object"),
isUndefined = require("is-undefined"),
deffy = require("deffy"),
arrsToObj = require("arrs-to-obj"),
errors = require("./errors"),
Err = require("err"),
Table = require("le-table"),
isEmptyObj = require("is-empty-obj"),
textWrap = require("wrap-text"),
indento = require("indento"),
removeEmptyLines = require("remove-blank-lines"),
ansiParser = require("ansi-parser"),
typpy = require("typpy"),
rJson = require("r-json"),
camelo = require("camelo"),
prompt = require("promptify"),
debugMode = require("debug-mode"),
autoparse = require("auto-parse");
var TABLE_OPTIONS = {
cell: {
hAlign: "left"
},
noAnsi: true,
marks: {
nw: " ",
n: " ",
ne: " ",
e: " ",
se: " ",
s: " ",
sw: " ",
w: " ",
b: " ",
mt: " ",
ml: " ",
mr: " ",
mb: " ",
mm: " "
}
};
var TildaOption =
/**
* TildaOption
* The `TildaOption` class used for creating option objects.
*
* @name TildaOption
* @function
* @param {Object} input An object containing the following fields:
*
* - `name` (String): The option name (optional).
* - `description` (String): The option description.
* - `opts` (Array): An array of aliases (e.g. `["age", "a"]`).
* - `default` (Anything): The default value.
* - `handler` (Function): The option handler which will be called when the
* option is found in the arguments. The first parameter is the option
* object and the second argument is the action where the option belongs to.
* - `required` (Boolean): A flag representing if the option is mandatory or not (default: `false`).
* - `type` (Class|String): The type class (e.g. `String`) or its stringified representation (e.g. `"string"`).
* - `prompt` (Boolean|Object): If `false`, it will disable the prompt even if the option is required. If it's an object, it will passed as options to `prompt-sync`.
*
* @returns {TildaOption} The `TildaOption` instance.
*
* - `description` (String): The option description.
* - `opts` (Array): An array of aliases (e.g. `["age", "a"]`).
* - `aliases` (Array): An array of strings containing the computed aliases,
* the single letter ones being the first (e.g. `["-n", "--name"]`).
* - `value` (null|String|DefaultValue): The option value which was found
* after processing the arguments.
* - `def` (Anything): The provided default value.
* - `is_provided` (Boolean): A flag if the option was or not been provided.
* - `handler` (Function): The handler function.
* - `required` (Boolean): The required value.
* - `type` (Class|String): The option value type.
* - `prompt` (Boolean|Object): The prompt settings..
*/
function TildaOption(input) {
_classCallCheck(this, TildaOption);
this.name = input.name;
this.description = input.description || input.desc;
this.opts = input.opts;
this.value = null;
if (!isUndefined(input.default)) {
this.value = this.default = input.default;
}
this.opts.sort(function (a) {
return a.length === 1 ? -1 : 1;
});
this.aliases = this.opts.map(function (c) {
return (c.length === 1 ? "-" : "--") + c;
});
this.is_provided = false;
this.handler = input.handler;
this.required = input.required || false;
this.type = input.type;
this.prompt = input.prompt;
};
var TildaActionArg = function TildaActionArg(input) {
_classCallCheck(this, TildaActionArg);
if (typpy(input, String)) {
input = { name: input };
}
this.name = input.name;
this.description = input.desc || input.description || "";
this.type = input.type;
this.stdin = input.stdin || false;
this.prompt = input.prompt;
};
var TildaAction = function (_events$EventEmitter) {
_inherits(TildaAction, _events$EventEmitter);
/**
* TildaAction
* The `TildaAction` class used for creating action objects.
*
* This is extended `EventEmitter`.
*
* @name TildaAction
* @function
* @param {String|Object} info The path to a containing the needed information or an object containing:
*
* - `description|desc` (String): The action description.
* - `name` (String): The action name.
* - `bin` (Object): A `package.json`-like `bin` field (optional).
* - `args` (Array): An array of strings/objects representing the action argument names (default: `[]`).
* - `examples` (Array): An array of strings containing examples how to use the action.
* - `notes` (String): Additional notes to display in the help command.
* - `documentation` (String): Action-related documentation.
*
* @param {Object} options An object containing the following fields (if
* provided, they have priority over the `info` object):
*
* - `args` (Array): An array of strings/objects representing the action argument names (default: `[]`).
* - `examples` (Array): An array of strings containing examples how to use the action.
* - `notes` (String): Additional notes to display in the help command.
* - `documentation` (String): Action-related documentation.
*
* @returns {TildaAction} The `TildaAction` instance containing:
*
* - `options` (Object): The action options.
* - `description` (String): The action description.
* - `name` (String): The action name.
* - `uniqueOpts` (Array): An array of unique options in order.
* - `_args` (Array): The action arguments.
* - `argNames` (Array): The action argument names.
* - `args` (Object): The arguments' values.
* - `examples` (Array): An array of strings containing examples how to use the action.
* - `notes` (String): Additional notes to display in the help command.
* - `documentation` (String): Action-related documentation.
* - `stdinData` (String): The stdin data.
*
*/
function TildaAction(info, options) {
_classCallCheck(this, TildaAction);
info = TildaAction.readInfo(info);
var _this = _possibleConstructorReturn(this, (TildaAction.__proto__ || Object.getPrototypeOf(TildaAction)).call(this));
options = options || {};
_this.options = {};
_this.description = info.description || info.desc || "";
_this.name = Object.keys(info.bin || {})[0] || info.name;
_this.uniqueOpts = [];
_this._args = Tilda.convertTo(TildaActionArg, options.args || info.args || []);
_this.argNames = _this._args.map(function (x) {
return x.name;
});
_this.args = {};
_this.examples = options.examples || info.examples || [];
_this.notes = options.notes || info.notes || "";
_this.documentation = info.homepage || options.documentation || info.documentation || "";
return _this;
}
/**
* readInfo
* Converts the info input into json output.
*
* @name readInfo
* @function
* @param {String|Object} info The info object or path to a json file.
* @returns {Object} The info object.
*/
_createClass(TildaAction, [{
key: "option",
/**
* option
* Adds one or more options to the action object.
*
* @name option
* @function
* @param {Array|Object} input An array of option objects or an object
* passed to the `TildaOption` class.
*/
value: function option(input) {
var _this2 = this;
if (!input) {
return;
}
if (Array.isArray(input)) {
input.forEach(function (c) {
return _this2.option(c);
});
return this;
}
if (!Array.isArray(input.opts)) {
throw new Error("The opts array is mandatory.");
}
var opt = Tilda.convertTo(TildaOption, input);
opt.opts.forEach(function (c) {
var cc = camelo(c);
if (_this2.options[cc] || _this2.options[c]) {
throw new Error("Found duplicated option: " + c + ". The option names should be unique.");
}
_this2.options[cc] = _this2.options[c] = opt;
});
this.uniqueOpts.push(opt);
return this;
}
}], [{
key: "readInfo",
value: function readInfo(info) {
if (typeof info === "string") {
info = rJson(info);
}
return info;
}
}]);
return TildaAction;
}(events.EventEmitter);
var Tilda = module.exports = function (_TildaAction) {
_inherits(Tilda, _TildaAction);
/**
* Tilda
* Creates the parser instance.
*
* @name Tilda
* @function
* @param {Object} info The `info` object passed to `TildaAction`.
* @param {Object} options The `options` passed to `TildaAction`, extended with:
*
* - `defaultOptions` (Array): Default and global options (default: help and version options).
* - `argv` (Array): A cutom array of arguments to parse (default: process arguments).
* - `stdin` (Boolean): Whether to listen for stdin data or not (default: `false`).
*
* @returns {Tilda} The `Tilda` instance containing:
*
* - `actions` (Object): An object containing the action objects.
* - `version` (String): The version (used in help and version outputs).
* - `argv` (Array): Array of arguments to parse.
* - `_globalOptions` (Array): The global options, appended to all the actions.
* - `actionNames` (Array): Action names in order.
*/
function Tilda(info, options) {
_classCallCheck(this, Tilda);
options = ul.merge(options, {
defaultOptions: [{
opts: ["help", "h"],
description: "Displays this help.",
handler: function handler(opt, action) {
_this3.displayHelp(action);
}
}, {
opts: ["version", "v"],
description: "Displays version information.",
handler: function handler() {
_this3.displayVersion();
}
}],
options: [],
autoparse: true,
argv: process.argv.slice(2),
stdin: options && options.args ? options.args.some(function (c) {
return c.stdin;
}) : false
});
info = TildaAction.readInfo(info);
var _this3 = _possibleConstructorReturn(this, (Tilda.__proto__ || Object.getPrototypeOf(Tilda)).call(this, info, options));
_this3.actions = {};
_this3.version = info.version;
_this3.argv = options.argv;
_this3._globalOptions = [];
_this3.actionNames = [];
_this3.globalOption(options.defaultOptions);
_this3.option(options.options);
process.nextTick(function (_) {
_this3.stdinData = "";
if (!options.stdin || debugMode || process.stdin.isTTY) {
_this3.parse();
} else {
process.stdin.setEncoding("utf8");
process.stdin.on("data", function (chunk) {
_this3.stdinData += chunk;
});
process.stdin.on("close", function () {
_this3.parse();
});
}
});
return _this3;
}
/**
* globalOption
* Adds a global option for all the actions.
*
* @name globalOption
* @function
* @param {Array|Object} input The option object.
*/
_createClass(Tilda, [{
key: "globalOption",
value: function globalOption(input) {
input = Tilda.convertTo(TildaOption, input);
this._globalOptions.push(input);
iterateObject(this.actions, function (action) {
action.option(input);
});
return this.option(input);
}
/**
* action
* Adds a new action.
*
* @name action
* @function
* @param {Object} input The info object passed to `TildaAction`.
* @param {Array} opts The action options.
*/
}, {
key: "action",
value: function action(input, opts) {
var _this4 = this;
if (Array.isArray(input)) {
input.forEach(function (c) {
return _this4.action(c);
});
return this;
}
var nAction = Tilda.convertTo(TildaAction, input, opts);
nAction.option(input.options);
this._globalOptions.forEach(function (c) {
return nAction.option(c);
});
this.actionNames.push(nAction.name);
if (this.actions[nAction.name]) {
throw new Err("Duplicated action name '<actionName>'", {
actionName: nAction.name
});
}
this.actions[nAction.name] = this.actions[camelo(nAction.name)] = nAction;
return this;
}
/**
* exit
* Exits the process.
*
* @name exit
* @function
* @param {String|Object} msg The stringified message or an object containing the error code.
* @param {Number} code The exit code (default: `0`).
*/
}, {
key: "exit",
value: function exit(msg, code) {
code = code || 0;
if (typeof msg === "string") {
console.log(msg);
return process.exit(code);
} else if (msg) {
if (msg.code && errors[msg.code]) {
msg = new Err(errors[msg.code], msg);
}
return this.exit(msg.message, 1);
}
}
/**
* parse
* Parses the arguments. This is called internally.
*
* This emits the action names as events.
*
* @name parse
* @function
*/
}, {
key: "parse",
value: function parse() {
var _this5 = this;
var res = clp(this.argv),
actionName = res._[0],
action = this.actions[actionName] || this;
// Parse the options
iterateObject(action.uniqueOpts, function (c) {
var optValue = null,
name = null;
// Find the option value
iterateObject(c.opts, function (cOpt) {
if (!isUndefined(res[cOpt])) {
optValue = res[cOpt];
name = cOpt;
return false;
}
});
// Handle required options
if (optValue === null && c.required) {
if (c.prompt !== false) {
var pr = c.prompt || {};
if (c.name === "password") {
pr.echo = pr.echo || "*";
}
pr.value = pr.value || "";
optValue = c.value = prompt(c.name + " (" + c.description + ")", undefined, pr) || "";
} else {
_this5.exit({
code: "MISSING_REQUIRED_OPTION",
option: c.aliases[1] || c.aliases[0],
exe: _this5.name
});
return false;
}
}
// Ignore empty options
if (optValue === null) {
return;
}
// Missing value?
if (c.name && typeof optValue === "boolean" && c.type !== Boolean) {
_this5.exit({
code: "MISSING_VALUE",
option: c.aliases[c.opts.indexOf(name)],
exe: _this5.name
});
}
if (c.type) {
optValue = autoparse(optValue, c.type);
}
// Handle validate option value type
if (c.type && !typpy(optValue, c.type)) {
_this5.exit({
code: "INVALID_OPTION_VALUE",
option: c.aliases[c.opts.indexOf(name)],
exe: _this5.name
});
return false;
}
// Valid option value
c.value = optValue;
c.is_provided = true;
if (c.handler) {
c.handler(c, action);
}
});
// Handle the action args
if (action._args.length) {
var values = res._.slice(action.actions ? 0 : 1);
action._args.forEach(function (arg, i) {
if (_this5.stdinData && arg.stdin) {
arg.type = arg.type || String;
values[i] = _this5.stdinData;
} else if (!values[i] && arg.prompt !== false) {
var pr = arg.prompt || {};
if (arg.name === "password") {
pr.echo = pr.echo || "*";
}
pr.value = pr.value || "";
values[i] = prompt(arg.name + " (" + arg.description + ")", pr) || "";
}
});
var diff = action._args.length - values.length;
if (diff > 0) {
return this.exit({
code: "MISSING_ACTION_ARG",
argName: action.argNames[diff - 1],
exe: this.name
});
} else if (diff < 0) {
return this.exit({
code: "TOO_MANY_ACTION_ARGS",
exe: this.name
});
}
iterateObject(values, function (c, i) {
var arg = action._args[i];
if (!arg.type) {
return;
}
if (arg.type) {
c = values[i] = autoparse(values[i], arg.type);
}
if (arg.type && !typpy(c, arg.type)) {
return _this5.exit({
code: "INVALID_ARG_VALUE",
exe: _this5.name,
arg: arg
});
}
});
action.args = arrsToObj(action.argNames, values);
}
this.emit(action.name, action);
}
/**
* displayVersion
* Returns the version information.
*
* @name displayVersion
* @function
* @return {String} The version information.
*/
}, {
key: "displayVersion",
value: function displayVersion() {
this.exit(this.name + " " + this.version);
}
/**
* displayHelp
* Displays the help output.
*
* @name displayHelp
* @function
* @param {TildaAction} action The action you want to display help for.
*/
}, {
key: "displayHelp",
value: function displayHelp(action) {
action = action || this;
var isMain = !!action.actions,
hasActions = !isEmptyObj(action.actions || {}),
hasOptions = !isEmptyObj(action.options),
helpOutput = "";
helpOutput += "Usage: " + this.name + (isMain ? hasActions ? " <command>" : "" : " " + action.name) + action.argNames.map(function (x) {
return " <" + x + ">";
}).join("") + (hasOptions ? " [options]" : "") + ("\n\n" + textWrap(action.description, 80) + "\n");
if (action._args.length) {
helpOutput += "\nCommand arguments:\n";
var tbl = new Table(TABLE_OPTIONS);
action._args.forEach(function (c) {
tbl.addRow(["<" + c.name + ">", textWrap(c.description, 50)]);
});
helpOutput += removeEmptyLines(tbl.stringify());
}
if (hasActions) {
helpOutput += "\nCommands:\n";
var _tbl = new Table(TABLE_OPTIONS);
action.actionNames.forEach(function (cActionName) {
var cAct = action.actions[cActionName];
_tbl.addRow([cAct.name, textWrap(cAct.description, 50)]);
});
helpOutput += removeEmptyLines(_tbl.stringify());
}
if (hasOptions) {
helpOutput += "\nOptions:\n";
var _tbl2 = new Table(TABLE_OPTIONS);
action.uniqueOpts.sort(function (x) {
return ~x.aliases.indexOf("-h") || ~x.aliases.indexOf("-v") ? 1 : -1;
}).forEach(function (cOpt) {
var optStr = cOpt.aliases.join(", ");
if (cOpt.name) {
optStr += " <" + cOpt.name + ">";
}
_tbl2.addRow([optStr, textWrap(cOpt.description, 50)]);
});
helpOutput += removeEmptyLines(_tbl2.stringify());
}
if (action.examples.length) {
helpOutput += "\nExamples:";
helpOutput += "\n" + action.examples.map(function (c) {
return indento((/^(\$|\#)/.test(c) ? "" : "$ ") + c, 2);
}).join("\n") + "\n";
}
if (action.notes.length) {
helpOutput += "\n" + textWrap(action.notes, 80) + "\n";
}
if (this.documentation) {
helpOutput += "\nDocumentation can be found at " + this.documentation + ".";
}
this.exit(helpOutput);
}
/**
* main
* Append a handler when the main action is used.
*
* @name main
* @function
* @param {Function} cb The callback function.
*/
}, {
key: "main",
value: function main(cb) {
return this.on(this.name, cb);
}
/**
* convertTo
* Converts an input into a class instance.
*
* @name convertTo
* @function
* @param {Class} classConst The class to convert to.
* @param {Object|Array} input The object info.
* @param {Object} opts The options object (optional).
* @returns {TildaAction|TildaOption} The input converted into a class instance.
*/
}], [{
key: "convertTo",
value: function convertTo(classConst, input, opts) {
if (typpy(input, classConst)) {
return input;
}
if (typpy(input, Array)) {
return input.map(function (x) {
return new classConst(x, opts);
});
}
return new classConst(input, opts);
}
}]);
return Tilda;
}(TildaAction);