#lang scribble/manual @(require (for-label racket) (for-label (planet cobbe/functional-command:1)) scribble/bnf scribble/eval) @(define the-eval (make-base-eval)) @(interaction-eval #:eval the-eval (begin (require racket/pretty (planet cobbe/functional-command:1)) (current-print pretty-print-handler) (pretty-print-columns 60))) @title{Functional Command Line Library} @defmodule[(planet cobbe/functional-command:1)] This module defines a function that allows you to parse a sequence of strings from the command line against a high-level specification, similar to the @racket[racket/cmdline] module. This module differs from the earlier work in two primary ways: @itemlist[#:style 'ordered @item{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''.} @item{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.}] @section{Interface} @defproc[(functional-command-line [spec (cons/c symbol? (cons/c string? (list/c (cons/c symbol? list?))))] [argv (or/c (listof string?) (vectorof string?))]) any]{ Parses the command line arguments in @racket[argv] using the specification @racket[spec]. This specification must match the form described in @secref["spec-syntax"]; otherwise, this function raises a @racket[exn:functional-command:parse] exception. The @racket[argv] argument must begin with the first argument @emph{after} the name of the executable (that is, in C terms, it should begin with @tt{argv[1]}). The value of @racket[current-command-line-arguments] is an acceptable value for @racket[argv]. On successfully parsing the command line according to the specification, @racket[functional-command-line] tail-calls the procedure in the appropriate clause of @racket[spec]. If the given command line does not match @racket[spec], then @racket[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 @racket[1]. In addition to the commands and switches defined in @racket[spec], this function always recognizes the @racket["help"] command and the @racket["--help"] switch. The precise behavior depends on the form of @racket[argv], as follows: @itemlist[ @item{@racket[argv] = @racket[(list "help")] or @racket[(list "--help")]: prints a summary of the commands in @racket[spec] to the current output port and terminates.} @item{@racket[argv] = @racket[(list "help" cmd ...)]: prints a detailed description of @racket[cmd]'s usage to the current output port and terminates.} ] (The semantics of @racket{help} and @racket{--help} are of course the same when @racket[argv] is a vector instead of a list.)} @defstruct*[(exn:functional-command:parse exn:fail:contract) ([src any/c]) #:transparent]{ Raised by @racket[functional-command-line] when the supplied spec does not match the form described in @secref["spec-syntax"]. The @racket[src] field contains the offending portion of the spec.} @subsection[#:tag "spec-syntax"]{Command-Line Specifications} A command-line specification must be a list whose structure is given by the following grammar: @verbatim{ ::= ( +) ::= ( + ) ::= (*) | (* . ) | ::= #:once-each + | #:multi + | #:once-any + ::= ((+) *) ::= | (*)} @; Can't use @BNF & related forms here, as they do a lousy job of rendering @; productions whose right-hand sides are too long to fit on a single line. Undefined terms in this grammar are as follows: @itemlist[ @item{A @nonterm{switch} is a string naming a switch, subject to the constraints on switch names.} @item{A @nonterm{name} is either a string or a symbol.} @item{The various flavors of @nonterm{help-text} are strings.} @item{A @nonterm{handler} is a Racket procedure that meets the calling convention described in @secref["calling-convention"].} ] A @nonterm{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. A @nonterm{command} has the following components: @itemlist[#:style 'ordered @item{The @nonterm{name} is the name of the subcommand, like raco's subcommands @tt{make} and @tt{scribble}.} @item{The @nonterm{short-help-text} is used to describe this command when printing an overall usage summary, such as that printed by @tt{raco help}.} @item{The @nonterm{long-help-text} is a longer description of the command's functionality, used when printing detailed usage information for a command, like @tt{raco help make} or @tt{svn help merge}.} @item{The @nonterm{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.} @item{The @nonterm{flag-spec-group}s specify the by-name, optional parameters that the command expects; the details of these groups is given below.} @item{The @nonterm{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 @secref["calling-convention"].} ] The @racket[#:once-each] keyword introduces a group of optional flags, each of which may appear at most once on the command line. Each flag has the following components: @itemlist[#:style 'ordered @item{A sequence of one or more @nonterm{switch}es. These are treated as synonyms; the user may supply any (but only one) of these on the command line.} @item{A Racket keyword used to identify the flag to the associated procedure.} @item{A @nonterm{help-spec} providing user documentation for the flag.} @item{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.}] The @racket[#:multi] keyword introduces a group of optional flags, each of which may appear arbitrarily many times on the command line. The flags have the same components as with @racket[#:once-each], except that a "multi" flag must accept at least one positional argument. Finally, the @racket[#:once-any] keyword introduces a group of mutually-exclusive flags: at most one flag in the group may appear on the command line. Each flag in this group has the same components and semantics as a @racket[#:once-each] flag. 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.) Finally, a spec must satisfy the following additional invariants: @itemlist[ @item{Command names must be unique within a spec.} @item{Switch names must be unique within a command.} @item{The command name @racket{help} and switch @racket{--help} are reserved.} ] Any violation of these invariants results in an @racket[exn:functional-command:parse] exception. @subsection[#:tag "calling-convention"]{Handler Calling Convention} The @racket[handler] function in each @racket[command] block must observe a calling convention as follows: @itemlist[#:style 'ordered @item{The handler must accept one positional argument for each of the command's required positional arguments.} @item{If the command has a rest argument, then the function must accept arbitrarily many positional arguments.} @item{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 @racket[#f] as a default value for these arguments, as @racket[functional-command-line] never passes this value as an argument.} @item{For a once-any or once-each switch, @racket[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 @racket[functional-command-line] passes the empty list.} @item{For a multi switch, @racket[functional-command-line] passes a zipped list of the switch's positional arguments, as in the detailed example below.}] @section{Examples} The first spec in the following example emulates some of the @tt{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. @examples[#:eval the-eval (functional-command-line `("raco" "Racket command-line tool" [docs "search and view documentation" "" (code:comment @#,t{no extended help string}) search-terms (code:comment @#,t{no switches; --help is automatically present}) (code:comment @#,t{for all commands}) ,(lambda search-terms `(docs (search-terms ,search-terms)))] [make "compile source to bytecode" "" (code:comment @#,t{no extended help string}) (file . another-file) #:once-each (("--disable-inline") #:disable-inline "Disable prcedure inlining during compilation") (("--no-deps") #:no-deps "Compile immediate files without updating dependencies") (("-p" "--prefix") #:prefix "Add elaboration-time prefix file for --no-deps" file) (("--no-prim") #:no-prim "Do not assume `scheme' bindings at top level for --no-deps") (("-v") #:verbose "verbose mode") (("-j") #:parallel "Parallel job count" wc) (("--vv") #:very-verbose "Very verbose mode") ,(lambda (#:disable-inline [disable-inline #f] #:no-deps [no-deps #f] #:prefix [prefix #f] #:no-prim [no-prim #f] #:verbose [verbose #f] #:parallel [parallel #f] #:very-verbose [very-verbose #f] . files) `(make (disable-inline ,disable-inline) (no-deps ,no-deps) (prefix ,prefix) (no-prim ,no-prim) (verbose ,verbose) (parallel ,parallel) (very-verbose ,very-verbose) (files ,files)))] [setup "install and build libraries and documentation" "" collection (code:comment @#,t{no parens, so rest arg}) #:once-each (("-c" "--clean") #:clean "Delete existing compiled files; implies -nxi") (("-j" "--workers") #:parallel "Use <#> parallel workers" workers) (("-n" "--no-zo") #:no-zo "Do not produce .zo files") (code:comment @#,t{... etc ...}) #:multi (("-P") #:planet "Setup specified PLaneT packages only" owner package-name maj min) ,(lambda (#:clean [clean #f] #:parallel [parallel #f] #:no-zo [no-zo #f] #:planet [planet #f] . collections) `(setup (clean ,clean) (parallel ,parallel) (no-zo ,no-zo) (planet ,planet) (collections ,collections)))]) '("setup" "-j" "3" "-P" "cobbe" "functional-command.plt" "1" "0" "-P" "cobbe" "contract-utils.plt" "4" "0")) #;(functional-command-line `(sample "Sample command line spec" (cmd "Sample command" "..." (x . rest) #:once-each (("-a") #:a "...") (("-b") #:b "..." b-arg) #:multi (("-c") #:c "..." c-arg-1 c-arg-2) #:once-any (("-d") #:d "..." d-arg) (("-e") #:e "...") ,(lambda (x #:a [a #f] #:b [b #f] #:c [c #f] #:d [d #f] #:e [e #f] . rest) `((x ,x) (rest ,rest) (a ,a) (b ,b) (c ,c) (d ,d) (e ,e))))) '("cmd" "positional 1" "-a" "-c" "first c1" "first c2" "positional 2" "-e" "-b" "b-arg" "-c" "second c1" "second c2" "positional 3"))] Note that positional arguments and switches can be mixed freely. Also note particularly the value passed to the @racket[#: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 @italic{n}th occurrence of the flag on the command line can be found in the @italic{n}th position of each of the sublists. I considered three possibilities for passing @racket[#:multi] flags to the user-supplied function: @itemlist[#:style 'ordered @item{A list of maps (alists, hashes, etc.), one per flag occurrence, indexed by formal argument name. In the exampe above, this would be @racketblock['(((owner . "cobbe") (package-name . "functional-command.plt") (major . "1") (minor . "0")) ((owner . "cobbe") (package-name . "contract-utils.plt") (major . "4") (minor . "0")))]} @item{A list of lists, each containing all arguments from a single flag occurrence. In the example above, this would be @racketblock['(("cobbe" "functional-command.plt" "1" "0") ("cobbe" "contract-utils.plt" "4" "0"))]} @item{A zipped list of arguments, as described above.}] Since it seemed to me most likely that clients would process these lists with @racket[map], @racket[for-each], @racket[foldl], and related functions, I chose the representation that was most convenient for that use. If experience shows that another representation would be better, this may change in later major versions of this package. @section[#:tag "future-work"]{Future Work} I'd like to add support for the following features, in roughly descending order by priority: @itemlist[ @item{Provide a macro interface, similar to @racket[command-line].} @item{Support "@literal{--}" as a signal that all subsequent tokens on the command line are to be interpreted as positional arguments, not switches.} @item{Support required switches---that is, allow the user to specify that a given switch or group of switches must appear on the command line.} @item{Support grouped switches---i.e., allow @tt{foo -abc} as a synonym for @tt{foo -a -b -c}.} @item{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.} @item{Optional positional arguments, for both commands and switches.} @item{Synonyms for commands, like @tt{svn ci} is a synonym for @tt{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/.)