Write Large CLIs Easily
1 Quick Start
2 Flag Handling
2.1 Getting Help
2.2 Example:   Illustrating Command Scope
3 Tweaking the CLI
4 Command Modules
summary
process-command-line
5 API Reference
make-subcommand-handlers
get-program-name
7.5

Write Large CLIs Easily

Sage Gerard

 (require natural-cli) package: natural-cli

This collection helps you write non-trivial command-line interfaces (CLIs).

If you want to put together a simple CLI or offer a single level of subcommands, then don’t use this. Just use racket/cmdline.

Use natural-cli if you have subcommands that have subcommands that have subcommands, and are constantly iterating on the interface’s design. In this situation, the CLI’s structure is a distraction and you’ll need help managing it.

1 Quick Start

After installing, run the following:

    $ mkdir cli && cd cli

    $ natural-cli mkmodule olympian.rkt

    $ racket olympian.rkt

You just made a command module that supports subcommands (See Command Modules for details).

To add a subcommand, make a new module with an underscore separating the command name from the subcommand name.

    $ natural-cli mkmodule olympian_jump.rkt

The code duplication you’ll observe is intentional. In practice, each module needs to evolve independently.

If you run olympian.rkt again, you’ll see jump as a subcommand.

You can keep adding deeper subcommands.

    $ natural-cli mkmodule olympian_jump_high.rkt olympian_jump_far.rkt

If your shell supports brace expansion, you can express command trees succiently. You do not have to run this command. It’s only here as a quick tip.

    $ natural-cli mkmodule olympian_run{_{fast,slow},}.rkt

For polish, you can create a Racket launcher using the mklauncher command. If you want a GRacket launcher, use the -g switch.

    $ natural-cli mklauncher olympian.rkt

    $ ./olympian

    $ ./olympian jump

    $ ./olympian jump high

    $ ./olympian jump far

Each module created using natural-cli mkmodule is eligible for use in racket-launcher-libraries or gracket-launcher-libraries in info.rkt.

To recap on what we observed:

2 Flag Handling

natural-cli scopes flags to their associated commands. This allows you to pass flags between commands to configure behavior at different levels of a program.

    $ ./olympian --home Germany jump far --start-distance 20m

This means that a flag’s position matters between commands and subcommands. Here we cover the implications.

2.1 Getting Help

racket/cmdline reserves the -h flag for requesting help, and normally exits after doing so.

You can request help for different commands by moving the -h flag.

    $ ./olympian jump high -h

    $ ./olympian jump -h high

    $ ./olympian -h jump high

The exit behavior prevents any subcommands from running, so any additional -h or subcommand following the first -h will have no effect.

You can configure the behavior of -h using #:handlers in command-line in the associated module.

2.2 Example: Illustrating Command Scope

To show how flags are scoped to subsets of a command line, we’ll review how two flags of the same name are subject to the rules of different parsers.

Here’s a config.rkt that holds dynamic runtime data shared between commands.

"config.rkt"

#lang racket/base
(provide (all-defined-out))
(define top (make-parameter 0))
(define sub (make-parameter 0))

Next, let’s run natural-cli mkmodule top.rkt top_sub.rkt and edit each new file as follows:

"top.rkt"

#lang racket/base
(provide process-command-line)
(require "./config.rkt")
 
(module+ main (void (process-command-line)))
 
(require racket/cmdline
         racket/runtime-path
         (only-in mzlib/etc this-expression-file-name)
         natural-cli)
 
(define program-name
  (get-program-name (this-expression-file-name)))
(define-runtime-path cli-directory ".")
 
(define (process-command-line)
  (define-values (fin arg-strs help unknown)
    (make-subcommand-handlers cli-directory program-name))
  (command-line #:program program-name
                #:multi
                [("-a") "Increment top" (top (add1 (top)))]
                #:handlers fin arg-strs help unknown))

"sub.rkt"

#lang racket/base
(provide process-command-line summary)
(require "./config.rkt")
 
(define summary "Prints counters.")
 
(require racket/cmdline
         racket/runtime-path
         (only-in mzlib/etc this-expression-file-name)
         natural-cli)
 
(define program-name
  (get-program-name (this-expression-file-name)))
(define-runtime-path cli-directory ".")
 
(define (process-command-line)
  (command-line #:program program-name
                #:once-each
                [("-a") "Increment sub"
                        (sub (add1 (sub)))]
                #:args _
                (printf "~a ~a~n" (top) (sub))))

Notice that top allows multiple uses of -a with #:multi, and sub uses #:once-each.

For this collection, this session holds:

    $ ./top sub -a

    0 1

    $ ./top -a sub

    1 0

    $ ./top -a sub -a

    1 1

    $ ./top -a -a sub -a

    2 1

    $ ./top -a sub -a -a

    top_sub: the -a option can only be specified once

3 Tweaking the CLI

The mkmodule command writes code that is coupled to the file system. So long as that code is preserved, you can design a CLI by changing files.

To remove a subcommand from a project, just delete its file.

    $ rm olympian_jump_high.rkt

To add a command, run natural-cli mkmodule as discussed or copy an existing module for later editing.

    $ natural-cli mkmodule olympian_throw.rkt # or...

    $ cp olympian_jump.rkt olympian_throw.rkt

To rename a command, rename the file.

    $ mv olympian_swim.rkt olympian_dive.rkt

To move a command under a different parent, rename the file.

    $ mv olympian_jump_far.rkt olympian_throw_far.rkt

For more advanced cases, use batch renaming.

4 Command Modules

Each Racket module managed by natural-cli should (provide process-command-line summary).

value

summary : string?

A one-line summary of the command’s intended function. This will appear in help strings of a parent command.

If not provided, this will default to "Run with -h for details."

procedure

(process-command-line)  any/c

A procedure that may use racket/cmdline as it wishes. When control enters this procedure, current-command-line-arguments will hold only the options, switches, and arguments meant for that command.

If not provided, this will default to a procedure that announces a missing implementation and evaluates (exit 1).

5 API Reference

(require natural-cli) offers bindings that cooperate with code created by natural-cli mkmodule.

procedure

(make-subcommand-handlers cli-directory 
  program-name) 
  
((list?) () #:rest list? . ->* . any)
(listof string?)
(string? . -> . any)
(string? . -> . any)
  cli-directory : directory-exists?
  program-name : string?
Returns (in the order shown), a finish-expr, an arg-strings-expr, a help-expr, and an unknown-expr for use in the #:handlers clause of command-line. You can modify these values, but you need finish-expr to transfer control to subcommands.

finish-expr dynamically instantiates the command module referenced by the first positional (non-flag) argument from the command line.

procedure

(get-program-name source-file)  string?

  source-file : path-string?
Returns the canonical program name based on a path string of the calling module file.