1 Interface
functional-command-line
exn: functional-command: parse
1.1 Command-Line Specifications
1.2 Handler Calling Convention
2 Examples
3 Future Work
Version: 5.1.0.2

Functional Command Line Library

 (require (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/cmdline module. This module differs from the earlier work in two primary ways:
  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”.

  2. 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.

1 Interface

(functional-command-line spec argv)  any
  spec : 
(cons/c symbol?
        (cons/c string?
                (list/c (cons/c symbol? list?))))
  argv : (or/c (listof string?) (vectorof string?))
Parses the command line arguments in argv using the specification spec. This specification must match the form described in Command-Line Specifications; otherwise, this function raises a exn:functional-command:parse exception.

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[1]). 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.

In addition to the commands and switches defined in spec, this function always recognizes the "help" command and the "--help" switch. The precise behavior depends on the form of argv, as follows:
  • argv = (list "help") or (list "--help"): prints a summary of the commands in spec to the current output port and terminates.

  • argv = (list "help" cmd ...): prints a detailed description of cmd’s usage to the current output port and terminates.

(The semantics of "help" and "--help" are of course the same when argv is a vector instead of a list.)

(struct exn:functional-command:parse exn:fail:contract (src)
  #:transparent)
  src : any/c
Raised by functional-command-line when the supplied spec does not match the form described in Command-Line Specifications. The src field contains the offending portion of the spec.

1.1 Command-Line Specifications

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>

                     <flag-group>+

                     <lambda>)

<posn-arg-spec> ::= (<symbol>*)

                  | (<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>*)

Undefined terms in this grammar are as follows:
  • 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.

A ‹command› has the following components:
  1. The ‹name› is the name of the subcommand, like raco’s subcommands make and scribble.

  2. The ‹short-help-text› is used to describe this command when printing an overall usage summary, such as that printed by raco help.

  3. 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.

  4. 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.

  5. The ‹flag-spec-group›s specify the by-name, optional parameters that the command expects; the details of these groups is given below.

  6. 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.

The #: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:
  1. 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.

  2. A Racket keyword used to identify the flag to the associated procedure.

  3. A ‹help-spec› providing user documentation for the flag.

  4. 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 #: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 #:once-each, except that a "multi" flag must accept at least one positional argument. Finally, the #: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 #: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:
  • 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.

Any violation of these invariants results in an exn:functional-command:parse exception.

1.2 Handler Calling Convention

The handler function in each command block must observe a calling convention as follows:
  1. The handler must accept one positional argument for each of the command’s required positional arguments.

  2. If the command has a rest argument, then the function must accept arbitrarily many positional arguments.

  3. 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.

  4. 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.

  5. For a multi switch, functional-command-line passes a zipped list of the switch’s positional arguments, as in the detailed example below.

2 Examples

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.

Example:

  > (functional-command-line
      `("raco" "Racket command-line tool"
         [docs "search and view documentation"
               ""  ; no extended help string
               search-terms
               ; no switches; help is automatically present
               ; for all commands
               ,(lambda search-terms
                  `(docs (search-terms ,search-terms)))]
         [make "compile source to bytecode"
               "" ; 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          ; 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")
                ; ... 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"))

  '(setup

    (clean #f)

    (parallel ("3"))

    (no-zo #f)

    (planet

     (("cobbe" "cobbe")

      ("functional-command.plt" "contract-utils.plt")

      ("1" "4")

      ("0" "0")))

    (collections ()))

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.

I considered three possibilities for passing #:multi flags to the user-supplied function:
  1. 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")))

  2. 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"))

  3. A zipped list of arguments, as described above.

Since it seemed to me most likely that clients would process these lists with map, for-each, 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.

3 Future Work

I’d like to add support for the following features, in roughly descending order by priority:

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/.)