Version 1.1, February 2011
Richard Cobbe, <cobbe at ccs dot neu dot edu>
|(require (planet cobbe/functional-command:1))|
This library supports command lines of the form used by CVS, Subversion, and other tools. In this form, the first “argument” is a command name within the application, and subsequent arguments are parameters to that command, such as “svn stat info.rkt” or “cvs up”.
This library avoids the earlier work’s heavy reliance on side effects. In the older library, the only way to make the value of an optional switch available to the rest of the program is through the use of side effects. With this module, the user provides a function for each command, and optional switches are passed to this function as keyword arguments.
|(functional-command-line spec argv) → any|
|argv : (or/c (listof string?) (vectorof string?))|
The argv argument must begin with the first argument after the name of the executable (that is, in C terms, it should begin with argv). The value of current-command-line-arguments is an acceptable value for argv.
On successfully parsing the command line according to the specification, functional-command-line tail-calls the procedure in the appropriate clause of spec. If the given command line does not match spec, then functional-command-line prints an error message to the current error port, prints a usage message to the current output port, and terminates the process with exit code 1.
(The semantics of "help" and "--help" are of course the same when argv is a vector instead of a list.)
A command-line specification must be a list whose structure is given by the following grammar:
<spec> ::= (<name> <program-help-text> <command>+)
<command> ::= (<name> <short-help-text> <long-help-text>
<posn-arg-spec> ::= (<symbol>*)
| (<symbol>* . <symbol>)
<flag-group> ::= #:once-each <flag-spec>+
| #:multi <flag-spec>+
| #:once-any <flag-spec>+
<flag-spec> ::= ((<switch>+) <keyword> <help-spec> <symbol>*)
<help-spec> ::= <string> | (<string>*)
A ‹switch› is a string naming a switch, subject to the constraints on switch names.
A ‹name› is either a string or a symbol.
The various flavors of ‹help-text› are strings.
A ‹handler› is a Racket procedure that meets the calling convention described in Handler Calling Convention.
A ‹spec› has three components: the name of the program, a short description of the program’s functionality, and a sequence of commands. The program name and help text are printed in the description of the program’s command-line usage; for best results, the help text should be a single line.
The ‹name› is the name of the subcommand, like raco’s subcommands make and scribble.
The ‹short-help-text› is used to describe this command when printing an overall usage summary, such as that printed by raco help.
The ‹long-help-text› is a longer description of the command’s functionality, used when printing detailed usage information for a command, like raco help make or svn help merge.
The ‹posn-arg-spec› indicates how many positional arguments the command expects, using the standard Racket syntax for arbitrarily many arguments. The precise symbol names are used only when printing usage information and do not otherwise matter.
The ‹flag-spec-group›s specify the by-name, optional parameters that the command expects; the details of these groups is given below.
The ‹procedure› is the Racket procedure to be invoked when the user specifies this command’s name on the command line. It must support the calling convention described in Handler Calling Convention.
A sequence of one or more ‹switch›es. These are treated as synonyms; the user may supply any (but only one) of these on the command line.
A Racket keyword used to identify the flag to the associated procedure.
A ‹help-spec› providing user documentation for the flag.
A sequence of zero or more symbols, indicating the flag’s parameters. As with positional arguments, the precise symbol names are used only for documentation and have no other effect.
This library supports both the standard Unix short and long form switches. A short-form switch consists of a single dash followed by a single letter. A long-form switch begins with two dashes, followed by an arbitrarily long (though not empty) sequence of letters, numbers, and dashes. (However, the third character in a switch must be a number or a letter.)
Command names must be unique within a spec.
Switch names must be unique within a command.
The command name "help" and switch "--help" are reserved.
The handler must accept one positional argument for each of the command’s required positional arguments.
If the command has a rest argument, then the function must accept arbitrarily many positional arguments.
For each flag (whether once-only, multi, or group), the function must accept a corresponding optional keyword argument, using the keyword in the flag spec. We recommend using #f as a default value for these arguments, as functional-command-line never passes this value as an argument.
For a once-any or once-each switch, functional-command-line passes a list of the switch’s positional arguments to the handler’s corresponding keyword argument. If the switch has no arguments, then functional-command-line passes the empty list.
For a multi switch, functional-command-line passes a zipped list of the switch’s positional arguments, as in the detailed example below.
The first spec in the following example emulates some of the raco command line tool’s interface. The handler functions simply demonstrate the calling convention described in the next section; in a real application, these functions would actually perform the program’s functionality.
Note that positional arguments and switches can be mixed freely.
Also note particularly the value passed to the #:planet argument in the example above. It is a list with one element for each of the corresponding flag’s parameters; the parameters from the nth occurrence of the flag on the command line can be found in the nth position of each of the sublists.
- A list of maps (alists, hashes, etc.), one per flag occurrence, indexed by formal argument name. In the exampe above, this would be
'(((owner . "cobbe") (package-name . "functional-command.plt") (major . "1") (minor . "0")) ((owner . "cobbe") (package-name . "contract-utils.plt") (major . "4") (minor . "0")))
- A list of lists, each containing all arguments from a single flag occurrence. In the example above, this would be
'(("cobbe" "functional-command.plt" "1" "0") ("cobbe" "contract-utils.plt" "4" "0"))
A zipped list of arguments, as described above.
I’d like to add support for the following features, in roughly descending order by priority:
Provide a macro interface, similar to command-line.
Support "--" as a signal that all subsequent tokens on the command line are to be interpreted as positional arguments, not switches.
Support required switches—
that is, allow the user to specify that a given switch or group of switches must appear on the command line.
Support grouped switches—
i.e., allow foo -abc as a synonym for foo -a -b -c.
Typed arguments. That is, I’d like to allow the user to specify that a particular argument must be, for example, an integer, and have the library pass an integer to the corresponding handler function, rather than a string which the handler must convert.
Optional positional arguments, for both commands and switches.
Synonyms for commands, like svn ci is a synonym for svn commit.
I welcome patches for any of these features! (Please submit them by attaching them to a Trac ticket filed against this package at http://planet.racket-lang.org/.)
This work is copyrighted and distributed under the terms of the Modified BSD License, included below.
Copyright (c) 2011, Richard Cobbe.
All rights reserved.
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
The name of Richard Cobbe may not be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RICHARD COBBE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.