cli:   Simple Command Line Interfaces
1 Usage
1.1 In a script
1.2 In a main submodule
2 Forms
help
flag
constraint
program
run
3 Interoperating with Racket
4 Testing Your Script
8.2

cli: Simple Command Line Interfaces

Siddhartha Kasivajhula

 #lang cli package: cli

A language for writing command line interfaces that aims to be simple, composable, and robust. You can use it to write standalone scripts, or to extend the capabilities of an existing main submodule.

(help (usage "A convenient way to write command line interfaces."))
 
(flag (verbose)
  ("-v" "--verbose" "Show detailed messages.")
  (verbose #t))
 
(program (hello name)
  (displayln (string-append "Hello " name "!"))
  (if (verbose)
    (displayln "By golly, it is very nice to meet you, indeed!")
    (displayln "Nice to meet you.")))
 
(run hello)

1 Usage

The language provides a small set of forms that allow you to comprehensively specify the behavior of your command line interface. You may use these forms in the following ways.

1.1 In a script

In order to use the language to write a command line script, simply declare the module as #lang cli at the top of the file.

#lang cli
 
(program (hello name)
  (displayln (string-append "Hello " name "!")))
 
(run hello)

1.2 In a main submodule

To use the language in a main submodule, use cli as the module language.

#lang racket
 
(require racket/format)
 
(define (greeting name)
  (~a "Hello, " name "!"))
 
(provide greeting)
 
(module* main cli
  (require (submod ".."))
  (program (say-hello [name "Your name"])
    (displayln (greeting name)))
  (run say-hello))

Since the module language differs from the enclosing module language, we need to explicitly require the enclosing module in the main submodule via (require (submod "..")) in order to use the identifiers declared there. Also note that unlike in the typical case of using (module+ main), the main submodule would only have access to the enclosing module identifiers when they are explicitly provided, as in the example above.

2 Forms

syntax

(help help-clause ...)

 
help-clause = (usage line ...)
  | (labels line ...)
  | (ps line ...)
     
line = string
Document various aspects of the command for display via shell interaction (for instance, via the -h or --help flag). usage shows usage information, labels appear before the help section on flags, and ps appears at the end of the help text, as a "postscript" to the text. Each of the subforms, usage, labels and ps, accept strings provided in sequence, with each provided string appearing on a separate line in the output. These forms correspond to the identically-named forms in Racket’s built-in command line provisions.

(help (usage "A script to say hello." "It also validates any provided links.")
      (labels "A label" "Another label")
      (ps "Goodbye!"))

syntax

(flag (id maybe-paramspec arg ...) metadata body ...)

 
maybe-paramspec = #:param paramspec
     
paramspec = param-name
  | [param-name init-value]
     
metadata = (short-flag long-flag description)
Declare a flag that will be accepted by the program. This form specifies a function that will be called when the flag is provided at the command line. This function (and the corresponding command-line flag) can accept any number of arguments, or no arguments – but in general, a specific number of them. To accept an arbitrary number of arguments, use the multi constraint.

Each flag defined using flag results in the creation of a parameter which may be used to store relevant values or configuration which will be available to the running program defined in the program form. By default, this parameter has the same name as that of the flag, and is initialized with the value #f, but these may be configured via the keyword argument #:param. Note that Racket parameters are accessed by invoking them, so in the examples below, it is these parameters being invoked rather than the flag functions which appear to have the same name (but which, under the hood, are anonymous).

(flag (attempts n)
  ("-a" "--attempts" "Number of attempts to make")
  (attempts (string->number n)))
 
(flag (links #:param [links null] link)
  ("-l" "--link" "Links to validate")
  (links (cons link (links))))

syntax

(constraint constraint-clause)

 
constraint-clause = (one-of flag-id ...)
  | (multi flag-id ...)
  | (final flag-id ...)
Declare a constraint that applies to the flags. By default, a flag declared via flag may appear at the command line at most once. A constraint changes this expectation for the indicated flags. one-of means that only one of the flags in the indicated set may be provided, i.e. at most one in the set rather than individually. multi means that the indicated flags may appear any number of times, and final means that none of the arguments following the indicated flags will be treated as flags. See Command-Line Parsing for more on what these constraints mean.

(constraint (one-of attempts retries))
(constraint (multi links))
(constraint (final exec))

syntax

(program (name argspec ...) body ...)

 
argspec = arg
  | [arg description]
Define the command to be run. This simply defines a function, where the arguments correspond directly to those received at the command line. The arguments may optionally be documented inline, and these descriptions would appear at the command line in help text and prompts. Any declared flags are available in the body of the function via the corresponding parameters. Any number of commands may be defined in the same file, and they would all have access to the same flags and environment. A command so defined is not executed unless it is invoked via run.

(program (contact-hosts [admin "Your name"])
  (displayln (~a "Hello, " admin "!"))
  (define result 0)
  (for-each (λ (link)
              (when (contact-host link (attempts))
                (set! result (add1 result))))
            (links))
  (displayln (~a "Done. Result: " result " hosts contacted.")))

Although the function defined using program appears to accept the arguments you indicate, in reality, it accepts raw command line arguments as a vector of strings which are parsed into the arguments you expect prior to your using them in the body of the function. Thus, if you called this function directly (for instance, mistakenly assuming it to be another function with the same name), you would get the following inscrutable error: parse-command-line: expected argument of type <argument vector/list of strings>. As commands are just functions, they must have distinct names from other identifiers in scope in order to avoid shadowing them.

syntax

(run program-instance)

 
program-instance = name
  | name argv
Run a command. Any command defined via program may be indicated here. The command need not be defined in the same module – since programs are just functions, they could be defined anywhere and simply required in order to make them available for execution.

By default, run passes the current command line arguments to the command, but you could also override this by providing a vector of strings representing a command line invocation, which may be useful for testing.

(run contact-hosts)
(run contact-hosts #("-a" "3" "George"))

3 Interoperating with Racket

In addition to the forms above, the language includes all of racket/base, so that you may require any identifiers that may be needed in your command line script. You may also freely intersperse and use Racket code within the cli module.

4 Testing Your Script

The program form syntactically resembles and indeed compiles to a simple function, and so can be tested just like any other function. But since command line scripts do not typically return values, it would probably make the most sense to put any business logic in vanilla (non-CLI) functions which are independently unit-tested, with the program form dispatching to those functions.

Even so, it can be useful during development and even for "smoke" tests to be able to run your script programmatically. To do this, simply pass the arguments and flags that you would on the command line to the run form as a vector of strings, like (run contact-hosts #("-a" "3" "George")). Note that each value in the vector must be a string, even if you use it as another data type (such as a number) in the body of your program form.

Finally, bear in mind that as passing flags to a command results in parameters being set, which are dynamically rather than lexically bound, commands correspond to a dynamic runtime state. Therefore, if you plan on running a command in a test module more than once, you should do so with care since it may produce different results on a subsequent invocation depending on this varying dynamic state.