cli: Simple Command Line Interfaces
#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
(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)
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 (transaction n timeout) ("-t" "--transaction" "Size of transaction, and timeout value") (transaction (map string->number (list n timeout)))) (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 ...)
A constraint does not modify the number of arguments the flag will consume, which is determined by the flag definition. For instance, a flag with a multi constraint that accepts two arguments in its definition will consume two arguments each time it appears at the command line.
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]
(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
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 Implementation
This library internally leverages Racket’s built-in command line facilities including parse-command-line for core functionality, and current-command-line-arguments for arguments parsed from the command line. The functionality provided by this library therefore includes all of the functionality of the core interfaces.
5 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.