Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cli

CLI: an applicative command-line parser as a first-class value.

A parser is a pure value describing what a command accepts: typed options (long/short, valued or switch, with defaults), typed positionals, and named subcommands. Parsers compose applicatively (build2/build3/build4 over map_p/ap_p/pure_p) into the user’s own result type, so a well-typed parser yields exactly that type or a rich error, never a half-filled record. The illegal state (a config with a missing required field, a subcommand without its arguments) is unrepresentable: the constructor cannot be applied until every part has parsed.

One description, three artifacts: the same Command value drives all three. It is the parser (run_argv), turning argv into the result type or an error. It is the help text (help_text), rendered from the option and argument specs the parser carries, so --help can never drift from what parses. And it is the errors (inside run_argv), which name the offending token, state the form expected, and print the usage line derived from those same specs.

Everything here is pure: a Parser reads a Tokens structure, and Tokens comes from lexing a List(String). The tie to the real command line is run_args, which feeds args() (the Env capability) to run_argv. Because argv arrives through that capability, a recorded run replays its arguments from the .replay trace like any other observation: CLI parsing is inside the determinism contract, argv and all.

Types

CliError

type CliError
  = MissingFlag(String)
  | MissingArg(String)
  | NeedsValue(String)
  | BadValue(String, String)
  | UnknownFlag(String)
  | UnknownCommand(String)

A parse failure, carrying enough to name the offending token and the form expected. Rendered to a message by describe.

OptSpec

type OptSpec = OptSpec {
  long: String,
  short: String,
  meta: String,
  help: String,
  takes_value: Bool
}

The static description of one option, carried by the parser for help and to tell the lexer whether the flag consumes a following value.

ArgSpec

type ArgSpec = ArgSpec { meta: String, help: String }

The static description of one positional argument.

Tokens

type Tokens = Tokens { opts: List((String, String)), pos: List(String) }

argv after lexing: options canonicalized to their long name, and the positionals in order. --help/-h are handled before lexing (has_help), so they never appear here.

Parser

type Parser(a) = Parser {
  opts: List(OptSpec),
  args: List(ArgSpec),
  run: (Tokens, Int) -> Result((a, Int), CliError)
}

A parser for a value of type a: the option and positional specs it accepts (the description help is rendered from) and a pure reader over lexed tokens threading the positional cursor. Build these with the combinators below, never by hand.

SubCmd

type SubCmd(a) = SubCmd { key: String, about: String, sub: Parser(a) }

One named subcommand: its key on the command line, a one-line description, and the parser that runs when it is chosen.

Body

type Body(a) = Plain(Parser(a)) | Group(List(SubCmd(a)))

A top-level command is either a single parser or a group of subcommands, all producing the same result type (so distinct subcommands map to distinct constructors of the user’s ADT).

Command

type Command(a) = Command { name: String, about: String, body: Body(a) }

A complete command: a program name and one-line description for usage, plus its body.

Outcome

type Outcome(a) = Parsed(a) | ShowHelp(String) | BadUsage(String)

The outcome of parsing argv: a value, a help request (carrying the rendered text), or a usage error (carrying the rendered message).

Functions and Values

dash

dash : Int

help_col

help_col : Int

pure_p

pure_p : forall a. (a) -> Cli.Parser(a)

A parser that consumes nothing and yields x. The applicative unit.

map_p

map_p : forall a b. ((b) -> a, Cli.Parser(b)) -> Cli.Parser(a)

Map f over a parser’s result, leaving what it consumes unchanged.

ap_p

ap_p : forall a b. (Cli.Parser((a) -> b), Cli.Parser(a)) -> Cli.Parser(b)

Applicative application: run pf then px, threading the positional cursor left to right, and apply the function to the argument. This is what lets a curried constructor be filled field by field.

build2

build2 : forall a b c. ((a) -> (c) -> b, Cli.Parser(a), Cli.Parser(c)) -> Cli.Parser(b)

Apply a two-argument curried constructor to two parsers.

build3

build3 : forall a b c d. ((a) -> (b) -> (d) -> c, Cli.Parser(a), Cli.Parser(b), Cli.Parser(d)) -> Cli.Parser(c)

Apply a three-argument curried constructor to three parsers.

build4

build4 : forall a b c d e. ((a) -> (b) -> (c) -> (e) -> d, Cli.Parser(a), Cli.Parser(b), Cli.Parser(c), Cli.Parser(e)) -> Cli.Parser(d)

Apply a four-argument curried constructor to four parsers.

opt_str

opt_str : (String, String, String, String, String) -> Cli.Parser(String)

An optional string flag with a default when absent.

req_str

req_str : (String, String, String, String) -> Cli.Parser(String)

A required string flag: an error when absent.

opt_int

opt_int : (String, String, String, String, Int) -> Cli.Parser(Int)

An optional integer flag with a default; a non-integer value is an error.

switch

switch : (String, String, String) -> Cli.Parser(Bool)

A boolean switch: true when present, false when absent, never valued.

arg_str

arg_str : (String, String) -> Cli.Parser(String)

A required string positional, consumed in declaration order.

arg_int

arg_int : (String, String) -> Cli.Parser(Int)

A required integer positional; a non-integer token is an error.

lex

lex : (List(Cli.OptSpec), List(String)) -> Result(Cli.Tokens, Cli.CliError)

Lex argv against the option specs (which say whether each flag takes a value). Options are canonicalized to their long name; anything not a flag is a positional. --help/-h are skipped here (detected earlier by has_help).

run_argv

run_argv : forall a. (Cli.Command(a), List(String)) -> Cli.Outcome(a)

Parse argv against a command, yielding a value, a help request, or a usage error. argv is the argument list only (no program name); cmd.name supplies the name for usage text.

help_text

help_text : forall a. (Cli.Command(a)) -> String

The rendered help for a command: dispatches to the plain or group form.

describe

describe : (Cli.CliError) -> String

A one-line description of a parse error, naming the offending token and the form expected.