diff options
Diffstat (limited to 'devtools/shared/gcli/source/docs/writing-commands.md')
-rw-r--r-- | devtools/shared/gcli/source/docs/writing-commands.md | 757 |
1 files changed, 757 insertions, 0 deletions
diff --git a/devtools/shared/gcli/source/docs/writing-commands.md b/devtools/shared/gcli/source/docs/writing-commands.md new file mode 100644 index 000000000..e73050279 --- /dev/null +++ b/devtools/shared/gcli/source/docs/writing-commands.md @@ -0,0 +1,757 @@ + +# Writing Commands + +## Basics + +GCLI has opinions about how commands should be written, and it encourages you +to do The Right Thing. The opinions are based on helping users convert their +intentions to commands and commands to what's actually going to happen. + +- Related commands should be sub-commands of a parent command. One of the goals + of GCLI is to support a large number of commands without things becoming + confusing, this will require some sort of namespacing or there will be + many people wanting to implement the ``add`` command. This style of + writing commands has become common place in Unix as the number of commands + has gone up. + The ```context``` command allows users to focus on a parent command, promoting + its sub-commands above others. + +- Each command should do exactly and only one thing. An example of a Unix + command that breaks this principle is the ``tar`` command + + $ tar -zcf foo.tar.gz . + $ tar -zxf foo.tar.gz . + + These 2 commands do exactly opposite things. Many a file has died as a result + of a x/c typo. In GCLI this would be better expressed: + + $ tar create foo.tar.gz -z . + $ tar extract foo.tar.gz -z . + + There may be commands (like tar) which have enough history behind them + that we shouldn't force everyone to re-learn a new syntax. The can be achieved + by having a single string parameter and parsing the input in the command) + +- Avoid errors. We try to avoid the user having to start again with a command + due to some problem. The majority of problems are simple typos which we can + catch using command metadata, but there are 2 things command authors can do + to prevent breakage. + + - Where possible avoid the need to validate command line parameters in the + exec function. This can be done by good parameter design (see 'do exactly + and only one thing' above) + + - If there is an obvious fix for an unpredictable problem, offer the + solution in the command output. So rather than use request.error (see + Request Object below) output some HTML which contains a link to a fixed + command line. + +Currently these concepts are not enforced at a code level, but they could be in +the future. + + +## How commands work + +This is how to create a basic ``greet`` command: + + gcli.addItems([{ + name: 'greet', + description: 'Show a greeting', + params: [ + { + name: 'name', + type: 'string', + description: 'The name to greet' + } + ], + returnType: 'string', + exec: function(args, context) { + return 'Hello, ' + args.name; + } + }]); + +This command is used as follows: + + : greet Joe + Hello, Joe + +Some terminology that isn't always obvious: a function has 'parameters', and +when you call a function, you pass 'arguments' to it. + + +## Internationalization (i18n) + +There are several ways that GCLI commands can be localized. The best method +depends on what context you are writing your command for. + +### Firefox Embedding + +GCLI supports Mozilla style localization. To add a command that will only ever +be used embedded in Firefox, this is the way to go. Your strings should be +stored in ``toolkit/locales/en-US/chrome/global/devtools/gclicommands.properties``, +And you should access them using ``let l10n = require("gcli/l10n")`` and then +``l10n.lookup(...)`` or ``l10n.lookupFormat()`` + +For examples of existing commands, take a look in +``devtools/client/webconsole/GcliCommands.jsm``, which contains most of the +current GCLI commands. If you will be adding a number of new commands, then +consider starting a new JSM. + +Your command will then look something like this: + + gcli.addItems([{ + name: 'greet', + description: gcli.lookup("greetDesc") + ... + }]); + +### Web Commands + +There are 2 ways to provide translated strings for web use. The first is to +supply the translated strings in the description: + + gcli.addItems([{ + name: 'greet', + description: { + 'root': 'Show a greeting', + 'fr-fr': 'Afficher un message d'accueil', + 'de-de': 'Zeige einen Gruß', + 'gk-gk': 'Εμφάνιση ένα χαιρετισμό', + ... + } + ... + }]); + +Each description should contain at least a 'root' entry which is the +default if no better match is found. This method has the benefit of being +compact and simple, however it has the significant drawback of being wasteful +of memory and bandwidth to transmit and store a significant number of strings, +the majority of which will never be used. + +More efficient is to supply a lookup key and ask GCLI to lookup the key from an +appropriate localized strings file: + + gcli.addItems([{ + name: 'greet', + description: { 'key': 'demoGreetingDesc' } + ... + }]); + +For web usage, the central store of localized strings is +``lib/gcli/nls/strings.js``. Other string files can be added using the +``l10n.registerStringsSource(...)`` function. + +This method can be used both in Firefox and on the Web (see the help command +for an example). However this method has the drawback that it will not work +with DryIce built files until we fix bug 683844. + + +## Default argument values + +The ``greet`` command requires the entry of the ``name`` parameter. This +parameter can be made optional with the addition of a ``defaultValue`` to the +parameter: + + gcli.addItems([{ + name: 'greet', + description: 'Show a message to someone', + params: [ + { + name: 'name', + type: 'string', + description: 'The name to greet', + defaultValue: 'World!' + } + ], + returnType: 'string', + exec: function(args, context) { + return "Hello, " + args.name; + } + }]); + +Now we can also use the ``greet`` command as follows: + + : greet + Hello, World! + + +## Positional vs. named arguments + +Arguments can be entered either positionally or as named arguments. Generally +users will prefer to type the positional version, however the named alternative +can be more self documenting. + +For example, we can also invoke the greet command as follows: + + : greet --name Joe + Hello, Joe + + +## Short argument names + +GCLI allows you to specify a 'short' character for any parameter: + + gcli.addItems([{ + name: 'greet', + params: [ + { + name: 'name', + short: 'n', + type: 'string', + ... + } + ], + ... + }]); + +This is used as follows: + + : greet -n Fred + Hello, Fred + +Currently GCLI does not allow short parameter merging (i.e. ```ls -la```) +however this is planned. + + +## Parameter types + +Initially the available types are: + +- string +- boolean +- number +- selection +- delegate +- date +- array +- file +- node +- nodelist +- resource +- command +- setting + +This list can be extended. See [Writing Types](writing-types.md) on types for +more information. + +The following examples assume the following definition of the ```greet``` +command: + + gcli.addItems([{ + name: 'greet', + params: [ + { name: 'name', type: 'string' }, + { name: 'repeat', type: 'number' } + ], + ... + }]); + +Parameters can be specified either with named arguments: + + : greet --name Joe --repeat 2 + +And sometimes positionally: + + : greet Joe 2 + +Parameters can be specified positionally if they are considered 'important'. +Unimportant parameters must be specified with a named argument. + +Named arguments can be specified anywhere on the command line (after the +command itself) however positional arguments must be in order. So +these examples are the same: + + : greet --name Joe --repeat 2 + : greet --repeat 2 --name Joe + +However (obviously) these are not the same: + + : greet Joe 2 + : greet 2 Joe + +(The second would be an error because 'Joe' is not a number). + +Named arguments are assigned first, then the remaining arguments are assigned +to the remaining parameters. So the following is valid and unambiguous: + + : greet 2 --name Joe + +Positional parameters quickly become unwieldy with long parameter lists so we +recommend only having 2 or 3 important parameters. GCLI provides hints for +important parameters more obviously than unimportant ones. + +Parameters are 'important' if they are not in a parameter group. The easiest way +to achieve this is to use the ```option: true``` property. + +For example, using: + + gcli.addItems([{ + name: 'greet', + params: [ + { name: 'name', type: 'string' }, + { name: 'repeat', type: 'number', option: true, defaultValue: 1 } + ], + ... + }]); + +Would mean that this is an error + + : greet Joe 2 + +You would instead need to do the following: + + : greet Joe --repeat 2 + +For more on parameter groups, see below. + +In addition to being 'important' and 'unimportant' parameters can also be +optional. If is possible to be important and optional, but it is not possible +to be unimportant and non-optional. + +Parameters are optional if they either: +- Have a ```defaultValue``` property +- Are of ```type=boolean``` (boolean arguments automatically default to being false) + +There is currently no way to make parameters mutually exclusive. + + +## Selection types + +Parameters can have a type of ``selection``. For example: + + gcli.addItems([{ + name: 'greet', + params: [ + { name: 'name', ... }, + { + name: 'lang', + description: 'In which language should we greet', + type: { name: 'selection', data: [ 'en', 'fr', 'de', 'es', 'gk' ] }, + defaultValue: 'en' + } + ], + ... + }]); + +GCLI will enforce that the value of ``arg.lang`` was one of the values +specified. Alternatively ``data`` can be a function which returns an array of +strings. + +The ``data`` property is useful when the underlying type is a string but it +doesn't work when the underlying type is something else. For this use the +``lookup`` property as follows: + + type: { + name: 'selection', + lookup: { + 'en': Locale.EN, + 'fr': Locale.FR, + ... + } + }, + +Similarly, ``lookup`` can be a function returning the data of this type. + + +## Number types + +Number types are mostly self explanatory, they have one special property which +is the ability to specify upper and lower bounds for the number: + + gcli.addItems([{ + name: 'volume', + params: [ + { + name: 'vol', + description: 'How loud should we go', + type: { name: 'number', min: 0, max: 11 } + } + ], + ... + }]); + +You can also specify a ``step`` property which specifies by what amount we +should increment and decrement the values. The ``min``, ``max``, and ``step`` +properties are used by the command line when up and down are pressed and in +the input type of a dialog generated from this command. + + +## Delegate types + +Delegate types are needed when the type of some parameter depends on the type +of another parameter. For example: + + : set height 100 + : set name "Joe Walker" + +We can achieve this as follows: + + gcli.addItems([{ + name: 'set', + params: [ + { + name: 'setting', + type: { name: 'selection', values: [ 'height', 'name' ] } + }, + { + name: 'value', + type: { + name: 'delegate', + delegateType: function() { ... } + } + } + ], + ... + }]); + +Several details are left out of this example, like how the delegateType() +function knows what the current setting is. See the ``pref`` command for an +example. + + +## Array types + +Parameters can have a type of ``array``. For example: + + gcli.addItems([{ + name: 'greet', + params: [ + { + name: 'names', + type: { name: 'array', subtype: 'string' }, + description: 'The names to greet', + defaultValue: [ 'World!' ] + } + ], + ... + exec: function(args, context) { + return "Hello, " + args.names.join(', ') + '.'; + } + }]); + +This would be used as follows: + + : greet Fred Jim Shiela + Hello, Fred, Jim, Shiela. + +Or using named arguments: + + : greet --names Fred --names Jim --names Shiela + Hello, Fred, Jim, Shiela. + +There can only be one ungrouped parameter with an array type, and it must be +at the end of the list of parameters (i.e. just before any parameter groups). +This avoids confusion as to which parameter an argument should be assigned. + + +## Sub-commands + +It is common for commands to be groups into those with similar functionality. +Examples include virtually all VCS commands, ``apt-get``, etc. There are many +examples of commands that should be structured as in a sub-command style - +``tar`` being the obvious example, but others include ``crontab``. + +Groups of commands are specified with the top level command not having an +exec function: + + gcli.addItems([ + { + name: 'tar', + description: 'Commands to manipulate archives', + }, + { + name: 'tar create', + description: 'Create a new archive', + exec: function(args, context) { ... }, + ... + }, + { + name: 'tar extract', + description: 'Extract from an archive', + exec: function(args, context) { ... }, + ... + } + ]); + + +## Parameter groups + +Parameters can be grouped into sections. + +There are 3 ways to assign a parameter to a group. + +The simplest uses ```option: true``` to put a parameter into the default +'Options' group: + + gcli.addItems([{ + name: 'greet', + params: [ + { name: 'repeat', type: 'number', option: true } + ], + ... + }]); + +The ```option``` property can also take a string to use an alternative parameter +group: + + gcli.addItems([{ + name: 'greet', + params: [ + { name: 'repeat', type: 'number', option: 'Advanced' } + ], + ... + }]); + +An example of how this can be useful is 'git' which categorizes parameters into +'porcelain' and 'plumbing'. + +Finally, parameters can be grouped together as follows: + + gcli.addItems([{ + name: 'greet', + params: [ + { name: 'name', type: 'string', description: 'The name to greet' }, + { + group: 'Advanced Options', + params: [ + { name: 'repeat', type: 'number', defaultValue: 1 }, + { name: 'debug', type: 'boolean' } + ] + } + ], + ... + }]); + +This could be used as follows: + + : greet Joe --repeat 2 --debug + About to send greeting + Hello, Joe + Hello, Joe + Done! + +Parameter groups must come after non-grouped parameters because non-grouped +parameters can be assigned positionally, so their index is important. We don't +want 'holes' in the order caused by parameter groups. + + +## Command metadata + +Each command should have the following properties: + +- A string ``name``. +- A short ``description`` string. Generally no more than 20 characters without + a terminating period/fullstop. +- A function to ``exec``ute. (Optional for the parent containing sub-commands) + See below for more details. + +And optionally the following extra properties: + +- A declaration of the accepted ``params``. +- A ``hidden`` property to stop the command showing up in requests for help. +- A ``context`` property which defines the scope of the function that we're + calling. Rather than simply call ``exec()``, we do ``exec.call(context)``. +- A ``manual`` property which allows a fuller description of the purpose of the + command. +- A ``returnType`` specifying how we should handle the value returned from the + exec function. + +The ``params`` property is an array of objects, one for each parameter. Each +parameter object should have the following 3 properties: + +- A string ``name``. +- A short string ``description`` as for the command. +- A ``type`` which refers to an existing Type (see Writing Types). + +Optionally each parameter can have these properties: + +- A ``defaultValue`` (which should be in the type specified in ``type``). + The defaultValue will be used when there is no argument supplied for this + parameter on the command line. + If the parameter has a ``defaultValue``, other than ``undefined`` then the + parameter is optional, and if unspecified on the command line, the matching + argument will have this value when the function is called. + If ``defaultValue`` is missing, or if it is set to ``undefined``, then the + system will ensure that a value is provided before anything is executed. + There are 2 special cases: + - If the type is ``selection``, then defaultValue must not be undefined. + The defaultValue must either be ``null`` (meaning that a value must be + supplied by the user) or one of the selection values. + - If the type is ``boolean``, then ``defaultValue:false`` is implied and + can't be changed. Boolean toggles are assumed to be off by default, and + should be named to match. +- A ``manual`` property for parameters is exactly analogous to the ``manual`` + property for commands - descriptive text that is longer than than 20 + characters. + + +## The Command Function (exec) + +The parameters to the exec function are designed to be useful when you have a +large number of parameters, and to give direct access to the environment (if +used). + + gcli.addItems([{ + name: 'echo', + description: 'The message to display.', + params: [ + { + name: 'message', + type: 'string', + description: 'The message to display.' + } + ], + returnType: 'string', + exec: function(args, context) { + return args.message; + } + }]); + +The ``args`` object contains the values specified on the params section and +provided on the command line. In this example it would contain the message for +display as ``args.message``. + +The ``context`` object has the following signature: + + { + environment: ..., // environment object passed to createTerminal() + exec: ..., // function to execute a command + update: ..., // function to alter the text of the input area + createView: ..., // function to help creating rich output + defer: ..., // function to create a deferred promise + } + +The ``environment`` object is opaque to GCLI. It can be used for providing +arbitrary data to your commands about their environment. It is most useful +when more than one command line exists on a page with similar commands in both +which should act in their own ways. +An example use for ``environment`` would be a page with several tabs, each +containing an editor with a command line. Commands executed in those editors +should apply to the relevant editor. +The ``environment`` object is passed to GCLI at startup (probably in the +``createTerminal()`` function). + +The ``document`` object is also passed to GCLI at startup. In some environments +(e.g. embedded in Firefox) there is no global ``document``. This object +provides a way to create DOM nodes. + +``defer()`` allows commands to execute asynchronously. + + +## Returning data + +The command meta-data specifies the type of data returned by the command using +the ``returnValue`` setting. + +``returnValue`` processing is currently functioning, but incomplete, and being +tracked in [Bug 657595](http://bugzil.la/657595). Currently you should specify +a ``returnType`` of ``string`` or ``html``. If using HTML, you can return +either an HTML string or a DOM node. + +In the future, JSON will be strongly encouraged as the return type, with some +formatting functions to convert the JSON to HTML. + +Asynchronous output is achieved using a promise created from the ``context`` +parameter: ``context.defer()``. + +Some examples of this is practice: + + { returnType: "string" } + ... + return "example"; + +GCLI interprets the output as a plain string. It will be escaped before display +and available as input to other commands as a plain string. + + { returnType: "html" } + ... + return "<p>Hello</p>"; + +GCLI will interpret this as HTML, and parse it for display. + + { returnType: "dom" } + ... + return util.createElement(context.document, 'div'); + +``util.createElement`` is a utility to ensure use of the XHTML namespace in XUL +and other XML documents. In an HTML document it's functionally equivalent to +``context.document.createElement('div')``. If your command is likely to be used +in Firefox or another XML environment, you should use it. You can import it +with ``var util = require('util/util');``. + +GCLI will use the returned HTML element as returned. See notes on ``context`` +above. + + { returnType: "number" } + ... + return 42; + +GCLI will display the element in a similar way to a string, but it the value +will be available to future commands as a number. + + { returnType: "date" } + ... + return new Date(); + + { returnType: "file" } + ... + return new File(); + +Both these examples return data as a given type, for which a converter will +be required before the value can be displayed. The type system is likely to +change before this is finalized. Please contact the author for more +information. + + { returnType: "string" } + ... + var deferred = context.defer(); + setTimeout(function() { + deferred.resolve("hello"); + }, 500); + return deferred.promise; + +Errors can be signaled by throwing an exception. GCLI will display the message +property (or the toString() value if there is no message property). (However +see *3 principles for writing commands* above for ways to avoid doing this). + + +## Specifying Types + +Types are generally specified by a simple string, e.g. ``'string'``. For most +types this is enough detail. There are a number of exceptions: + +* Array types. We declare a parameter to be an array of things using ``[]``, + for example: ``number[]``. +* Selection types. There are 3 ways to specify the options in a selection: + * Using a lookup map + + type: { + name: 'selection', + lookup: { one:1, two:2, three:3 } + } + + (The boolean type is effectively just a selection that uses + ``lookup:{ 'true': true, 'false': false }``) + + * Using given strings + + type: { + name: 'selection', + data: [ 'left', 'center', 'right' ] + } + + * Using named objects, (objects with a ``name`` property) + + type: { + name: 'selection', + data: [ + { name: 'Google', url: 'http://www.google.com/' }, + { name: 'Microsoft', url: 'http://www.microsoft.com/' }, + { name: 'Yahoo', url: 'http://www.yahoo.com/' } + ] + } + +* Delegate type. It is generally best to inherit from Delegate in order to + provide a customization of this type. See settingValue for an example. + +See below for more information. |