sirrobert-shell-cmd
Version:
A base class for nodejs-shell commands.
331 lines (284 loc) • 8.94 kB
JavaScript
"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;