UNPKG

tilda

Version:

Tiny module for building command line tools.

711 lines (621 loc) 25.3 kB
"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);