UNPKG

sirrobert-shell-cmd

Version:

A base class for nodejs-shell commands.

331 lines (284 loc) 8.94 kB
"use strict"; const Mix = require("sirrobert-mixin"); const Mixin = require("sirrobert-mixins"); const tokenize = require("sirrobert-tokenize"); var events = require("events"); var registerHook = new events.EventEmitter(); class Command extends Mix.with(Mixin.Params) { constructor (params) { super(params); this.names = []; this.help = []; this.subcommands = []; this.interpParams(params, { required: { handler : func => func, names : list => { this.addNames(list); return this.names; } }, optional: { active : bool => bool, expandStr : func => func || this.expandStr, findSubcommand : func => func || this.findSubcommand, help : list => { this.addHelp(list); return this.help; }, subcommands : list => { this.addSubcommands(list); return this.subcommands; }, } }); // Make all commands active by default. if (this.active === undefined) { this.activate(); } } // This allows commands to be self-registering without needing to know // anything about the calling process. static register (cmd) { registerHook.emit("register-command", cmd); } static onRegister (fxn) { registerHook.on("register-command", fxn); } interpret (command, params, context) { // This function is polymorphic. There are two forms: // // interpret(command, context) // interpret(command, params, context) // // Since the `context` param is required, but the `params` param isn't, // if we get only two arguments then we coerce the first form. If three // args, we coerce the second form. if (params && !context) { context = params; params = undefined; } if (this.isHelp(command)) { return this.requestHelp(command, context); } // If it's not a help file, else { return this.handler(command, params, context); } } setHandler (func) { this.handler = func; return this; } setExpandStr (func) { this.expandStr = func; return this; } expandStr () { } addName (name) { if (name) { this.names.push(name); } return this; } addNames () { var args = Array.prototype.slice.call(arguments, 0); for (var i = 0; i < args.length; i++) { this.names = this.names.concat(args[i]); } return this; } activate () { this.active = true; } deactivate () { this.active = false; } //******************************************************* // Subcommands // // Commands can have subcommands to help divide functionality into // reasonably-sized chunks. // // It is not necessary to use subcommands, especially for simple commands, // but it may be useful. //*** // This method allows us to find a subcommand given some tokens. For // example, if the tokens are ["view", "image"] then the command // constructed here would be "view image". findSubcommand (tokens) { var command = tokens.join(" "); for (var i = 0; i < this.subcommands.length; i++) { var cmd = this.subcommands[i]; for (var n = 0; n < cmd.names.length; n++) { if (cmd.names[n] == command) { return cmd; } } } } setFindSubcommand (func) { this.findSubcommand = func; return this; } addSubcommand (cmd) { if (objType(cmd) === "Command") { this.subcommands.push(cmd); } else { this.subcommands.push(new Command(cmd)); } return this; } addSubcommands () { var args = Array.prototype.slice.call(arguments,0); for (var i = 0; i < args.length; i++) { if (objType(args[i]) === "Array") { for (var k = 0; k < args[i].length; k++) { this.addSubcommand(args[i][k]); } } else if ( objType(args[i]) === "Object" || objType(args[i]) === "Command") { this.addSubcommand(args[i]); } } return this; } //********* // Interpreting help // // Each command may control how it wants to interpret requests for // help. The default syntaxes for help (provided by this base class) // include: // // $> help ... // $> ? ... // $> ... --help // $> ... -h // $> ... -? // // If any of these patterns is found in the command, the command is // interpreted as a request for help, rather than the issuance of the // command. // // Individual command modules are not responsible for displaying their own // help files. The shell does that for us, and the help module is // replacable. Instead, each command is responsible for providing help // content. This Command class provides default functionality for // registering and facilitating the display of help. Each of these can be // overridden by the command author, but good default behavior is codified // here. // // The basic flow goes like this: // // 1. Register a new help. A help requires three things, a token that // uniquely identifies the help cointent, a file that contains it, // and an array of aliases. Optionally, a fourth detail may be // provided: a "section", which marks a specific section of the file // to display. // // 2. Find the request token. When a user types a help command, such as // "help potato", the "potato" command gets the request and is // responsible for returning a "help token" that uniquely identifies // the help content. // // 3. Call the help command. After the help token has been identified, // the command sends the shell a request for the help to be // displayed. The shell then passes this through to the help // command, which displays the help based on the token. // addHelp (params) { if (params) { this.help = this.help.concat(params); } return this; } addHelps () { var args = Array.prototype.slice.call(arguments,0); for (var i = 0; i < args.length; i++) { if (objType(args[i]) === "Array") { for (var k = 0; k < args[i].length; k++) { this.addHelp(args[i][k]); } } else if (objType(args[i]) === "Object") { this.addHelp(args[i]); } } return this; } // The `isHelp` helper method simply takes a string and answers true or // false to the statement, "This is a request for help." isHelp (command) { let tokens = tokenize(command); if (tokens[0] == "help" || tokens[0] == "?") { return true; } for (var t = 0; t < tokens.length; t++) { if (tokens[t] == "--help" || tokens[t] == "-h" || tokens[t] == "-?") { return true; } } return false; } // Given a command, return a help token. By default, it returns the first // available command token that is not the help command. You can override // this to do custom parsing. getHelpToken (command) { let tokens = tokenize(command); let scrubbed = ""; if (tokens[0] == "help" || tokens[0] == "?") { scrubbed = tokens.slice(1).join(" "); } else { for (var i = 0; i < tokens.length; i++) { if (tokens[i] === "--help" || tokens[i] == "-h" || tokens[i] == "-?") { tokens.splice(i--,1); } } scrubbed = tokens.join(" "); } for (var i = 0; i < this.help.length; i++) { var aliases = [].concat(this.help[i].aliases); for (var a = 0; a < aliases.length; a++) { if (aliases[a] === scrubbed) { return this.help[i].token; } } } return null; } getHelp (token) { for (var i = 0; i < this.help.length; i++) { if (this.help[i].token === token) { return this.help[i]; } } return {}; } // This is a method used for requesting a help file from the shell. requestHelp (command, section, context) { if (section && context === undefined) { context = section; section = undefined; } var token = this.getHelpToken(command); context.requestCommand("help", { token: token, section: section }); } } function capFirst (str) { return str.charAt(0).toUpperCase() + str.slice(1); } function objType (obj) { // Used for typical class or functional construction. if (obj && obj.constructor && obj.constructor.name) { return capFirst(obj.constructor.name); } // This can happen when a constructor is, for example, an object property // (for example: // // var obj = { foo: function () {} }; // var f = new obj.something(); // f.constructor.name; // "" // if (obj && obj.constructor && !obj.constructor.name) { return capFirst(typeof obj); } if (obj === null ) { return "Null" } if (obj === undefined) { return "Undefined" } return capFirst(typeof obj); } module.exports = Command;