diff --git a/plugins/pilot/lib/canon.js b/plugins/pilot/lib/canon.js index 15bd8c24..a68458c6 100644 --- a/plugins/pilot/lib/canon.js +++ b/plugins/pilot/lib/canon.js @@ -123,7 +123,7 @@ exports.getCommand = function(name) { return commands[name]; }; -exports.getCommands = function() { +exports.getCommandNames = function() { return Object.keys(commands); }; @@ -148,7 +148,7 @@ exports.exec = function(name, args) { */ exports.execRequisition = function(requisition) { var request = new Request(); - requisition.command.exec(env, requisition.args, request); + requisition.command.exec(env, requisition.getArgs(), request); }; /** @@ -182,7 +182,18 @@ Requisition.prototype = { * The set of values that we are assigning to parameters in the command * @readonly */ - assignments: undefined + assignments: undefined, + + /** + * + */ + getArgs: function() { + var args = {}; + Object.keys(assignments).forEach(function(name) { + args[name] = getCommand(name); + }); + return args; + } }; exports.Requisition = Requisition; diff --git a/plugins/pilot/lib/cli.js b/plugins/pilot/lib/cli.js new file mode 100644 index 00000000..b47aff0f --- /dev/null +++ b/plugins/pilot/lib/cli.js @@ -0,0 +1,466 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker (jwalker@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + + +var console = require('skywriter/console'); +var util = require('pilot/util'); + +var keyboard = require('keyboard/keyboard'); +var Status = require('pilot/canon').Status; +var Requisition = require('pilot/canon').Requisition; + +var typehint = require('command_line/typehint'); + +/** + * The information required to tell the user there is a problem with their + * input. + * TODO: Consider formalizing alternatives + */ +function Hint(status, message, start, end) { + this.status = status; + this.message = message; + this.start = start; + this.end = end; +} +Hint.prototype = { + +}; +exports.Hint = Hint; + + +/** + * An object used during command line parsing to hold the various intermediate + * data steps. + *

The 'output' of the parse is held in 2 objects: input.hints which is an + * array of hints to display to the user. In the future this will become a + * single value. + *

The other output value is input.requisition which gives access to an + * args object for use in executing the final command. + * @param typed {string} The instruction as typed by the user so far + * @param options {object} A list of optional named parameters. Can be any of: + * flags: Flags for us to check against the predicates specified with the + * commands. Defaulted to keyboard.buildFlags({ }); + * if not specified. + * @constructor + */ +function Input(typed, options) { + if (util.none(typed)) { + throw new Error('Input requires something \'typed\' to work on'); + } + this.typed = typed; + this.hints = []; + + options = options || {}; + + // TODO: We were using a default of keyboard.buildFlags({ }); + // I think this allowed us to have commands that only existed in certain + // contexts - i.e. Javascript specific commands. + this.flags = options.flags || {}; + + // Once tokenize() has been called, we have the #typed string cut up into + // #_parts + this._parts = []; + + // Once split has been called we have #_parts split into #_unparsedArgs and + // #_command (if there is a matching command). + this._unparsedArgs = undefined; + + // Once we know what the command is, we can fire up a Requisition + this.requisition = undefined; + + // Assign matches #_unparsedArgs to the params declared by the #_command + // A list of arguments in _command.params order + this._assignments = undefined; + + this._tokenize(); +}; + +/** + * Implementation of Input. + * The majority of the functions in this class are called in sequence by the + * constructor. Their task is to add to hints fill out the requisition. + *

The general sequence is:

+ */ +Input.prototype = { + /** + * Split up the input taking into account ' and " + */ + _tokenize: function() { + if (!this.typed || this.typed === '') { + // We would like to put some initial help here, but for anyone but + // a complete novice a 'type help' message is very annoying, so we + // need to find a way to only display this message once, or for + // until the user click a 'close' button or similar + this.hints.push(new Hint(Status.INCOMPLETE)); + return; + } + + // replace(/^\s\s*/, '') = trimLeft() + var incoming = this.typed.replace(/^\s\s*/, '').split(/\s+/); + + var nextToken; + while (true) { + nextToken = incoming.shift(); + if (util.none(nextToken)) { + break; + } + if (nextToken[0] == '"' || nextToken[0] == '\'') { + // It's quoting time + var eaten = [ nextToken.substring(1, nextToken.length) ]; + var eataway; + while (true) { + eataway = incoming.shift(); + if (!eataway) { + break; + } + if (eataway[eataway.length - 1] == '"' || + eataway[eataway.length - 1] == '\'') { + // End quoting time + eaten.push(eataway.substring(0, eataway.length - 1)); + break; + } else { + eaten.push(eataway); + } + } + this._parts.push(eaten.join(' ')); + } else { + this._parts.push(nextToken); + } + } + + // Split the command from the args + this._split(); + }, + + /** + * Looks in the canon for a command extension that matches what has been + * typed at the command line. + */ + _split: function() { + this._unparsedArgs = this._parts.slice(); // aka clone() + var initial = this._unparsedArgs.shift(); + var command; + + while (true) { + command = canon.getCommand(initial); + + if (!command) { + // Not found. break with command == null + break; + } + + if (!keyboard.flagsMatch(command.predicates, this.flags)) { + // If the predicates say 'no match' then go LA LA LA + command = null; + break; + } + + if (command.exec) { + // Valid command, break with command valid + break; + } + + // command, but no exec - this must be a sub-command + initial += ' ' + this._unparsedArgs.shift(); + } + + // Do we know what the command is. + if (!command) { + // We don't know what the command is + // TODO: We should probably cache this + var commands = []; + canon.getCommandNames().forEach(function(name) { + var command = canon.getCommand(name); + if (keyboard.flagsMatch(command.predicates, this.flags) && + command.description) { + commands.push(command); + } + }.bind(this)); + + // TODO: make this a hint + var hintSpec = { + param: { + type: { name: 'selection', data: commands }, + description: 'Commands' + }, + value: this.typed + }; + + return; + } + + // The user hasn't started to type any params + if (this._parts.length === 1) { + if (this.typed == command.name || + !command.params || + command.params.length === 0) { + this.hints.push(documentCommand(command, this.typed)); + } + } + + this.requisition = new Requisition(command); + + // Assign input to declared parameters + this._assign(); + }, + + /** + * Work out which arguments are applicable to which parameters. + *

This takes #_command.params and #_unparsedArgs and creates a map of + * param names to 'assignment' objects, which have the following properties: + *

+ * The resulting #_assignments member created by this function is a list of + * assignments of arguments in command.params order. + * TODO: _unparsedArgs should be a list of objects that contain the + * following values: name, param (when assigned) and maybe hints? + */ + _assign: function() { + // TODO: something smarter than just assuming that they are all in order + this._assignments = []; + var params = this.requisition.command.params; + var unparsedArgs = this._unparsedArgs; + var message; + + // Create an error if the command does not take parameters, but we have + // been given them ... + if (!params || params.length === 0) { + // No problem if we're passed nothing or an empty something + var argCount = 0; + unparsedArgs.forEach(function(unparsedArg) { + if (unparsedArg.trim() !== '') { + argCount++; + } + }); + + if (argCount !== 0) { + message = this.requisition.command.name + ' does not take any parameters'; + this.hints.push(new Hint(Status.INVALID, message)); + } + + return; + } + + // Special case: if there is only 1 parameter, and that's of type + // text we put all the params into the first param + if (params.length == 1 && params[0].type == 'text') { + // Warning: There is some potential problem here if spaces are + // significant. It might be better to chop the command of the + // start of this.typed? But that's not easy because there could + // be multiple spaces in the command if we're doing sub-commands + this._assignments[0] = { + value: unparsedArgs.length === 0 ? null : unparsedArgs.join(' '), + param: params[0] + }; + } else { + // The normal case where we have to assign params individually + var index = 0; + var used = []; + params.forEach(function(param) { + this._assignParam(param, index++, used); + }.bind(this)); + + // Check there are no params that don't fit + var unparsed = false; + unparsedArgs.forEach(function(unparsedArg) { + if (used.indexOf(unparsedArg) == -1) { + message = 'Parameter \'' + unparsedArg + '\' makes no sense.'; + this.hints.push(new Hint(Status.INVALID, message)); + unparsed = true; + } + }.bind(this)); + + if (unparsed) { + return; + } + } + + // Show a hint for the last parameter + if (this._parts.length > 1) { + var assignment = this._getAssignmentForLastArg(); + + // HACK! deferred types need to have some parameters + // by which to determine which type they should defer to + // so we hack in the assignments so the deferrer can work + assignment.param.type.assignments = this._assignments; + + if (assignment) { + this.hints.push(typehint.getHint(this, assignment)); + } + } + + // Convert input into declared types + this._convertTypes(); + }, + + /** + * Extract a value from the set of inputs for a given param. + * @param param The param that we are providing a value for. This is taken + * from the command meta-data for the command in question. + * @param index The number of the param - i.e. the index of param + * into the original params array. + */ + _assignParam: function(param, index, used) { + var message; + // Look for '--param X' style inputs + for (var i = 0; i < this._unparsedArgs.length; i++) { + var unparsedArg = this._unparsedArgs[i]; + + if ('--' + param.name == unparsedArg) { + used.push(unparsedArg); + // boolean parameters don't have values, they default to false + if (param.type.name === 'boolean') { + this._assignments[index] = { + value: true, + param: param + }; + } else { + if (i + 1 < this._unparsedArgs.length) { + message = 'Missing parameter: ' + param.name; + // Missing value for this param + this.hints.push(new Hint(Status.INCOMPLETE, message)); + } else { + used.push(this._unparsedArgs[i + 1]); + } + } + return; + } + } + + var value = null; + if (this._unparsedArgs.length > index) { + value = this._unparsedArgs[index]; + used.push(this._unparsedArgs[index]); + } + + // null is a valid default value, and common because it identifies an + // parameter that is optional. undefined means there is no value from + // the command line + if (value !== undefined) { + this._assignments[index] = { value: value, param: param }; + } else { + this._assignments[index] = { param: param }; + + if (param.defaultValue === undefined) { + // There is no default, and we've not supplied one so far + message = 'Missing parameter: ' + param.name; + this.hints.push(new Hint(Status.INCOMPLETE, message)); + } + } + } +}; +exports.Input = Input; + +/** + * Provide some documentation for a command. + * TODO: this should return a hint + */ +function documentCommand(cmdExt, typed) { + var docs = []; + docs.push('

' + cmdExt.name + '

'); + docs.push('

Summary

'); + docs.push('

' + cmdExt.description + '

'); + + if (cmdExt.manual) { + docs.push('

Description

'); + docs.push('

' + cmdExt.description + '

'); + } + + if (cmdExt.params && cmdExt.params.length > 0) { + docs.push('

Synopsis

'); + docs.push('
');
+        docs.push(cmdExt.name);
+        var optionalParamCount = 0;
+        cmdExt.params.forEach(function(param) {
+            if (param.defaultValue === undefined) {
+                docs.push(' ');
+                docs.push(param.name);
+                docs.push('');
+            } else if (param.defaultValue === null) {
+                docs.push(' [');
+                docs.push(param.name);
+                docs.push(']');
+            } else {
+                optionalParamCount++;
+            }
+        });
+        if (optionalParamCount > 3) {
+            docs.push(' [options]');
+        } else if (optionalParamCount > 0) {
+            cmdExt.params.forEach(function(param) {
+                if (param.defaultValue) {
+                    docs.push(' [--');
+                    docs.push(param.name);
+                    if (param.type.name === 'boolean') {
+                        docs.push('');
+                    } else {
+                        docs.push(' ' + param.type.name);
+                    }
+                    docs.push(']');
+                }
+            });
+        }
+        docs.push('
'); + + docs.push('

Parameters

'); + cmdExt.params.forEach(function(param) { + docs.push('

' + param.name + '

'); + docs.push('

' + param.description + '

'); + if (param.type.defaultValue) { + docs.push('

Default: ' + param.type.defaultValue + '

'); + } + }); + } + + return { + param: { type: 'text', description: docs.join('') }, + value: typed + }; +}; +exports.documentCommand = documentCommand; + + +});