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:
+ * - _tokenize(): convert _typed into _parts
+ *
- _split(): convert _parts into _command and _unparsedArgs
+ *
- _assign(): convert _unparsedArgs into _assignments
+ *
+ */
+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:
+ *
+ * - param - The matching parameter.
+ *
- index - Zero based index into where the match came from on the input
+ *
- value - The matching input
+ *
+ * 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;
+
+
+});