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)
  | UnexpectedArg(String)
  | NeedsValue(String)
  | BadValue(String, String)
  | UnknownFlag(String)
  | UnknownCommand(String)
  deriving (Eq, Show)

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
} deriving (Eq, Show)

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,
  required: Bool
} deriving (Eq, Show)

The static description of one positional argument.

Tokens

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

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. The parser it carries is a reader function, so the subcommand as a whole has no meaningful equality or printed form.

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). Both alternatives bottom out in parser functions, so the body is not comparable or printable.

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. The body holds parser functions; render a command with help_text rather than expecting a printed form.

Outcome

type Outcome(a)
  = Parsed(a)
  | ShowHelp(String)
  | BadUsage(String)
  deriving (Eq, Show)

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

ASCII byte of -, the flag sigil.

help_col

help_col : Int

Column the help text aligns descriptions to.

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_str_default

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

An optional string positional, consumed in declaration order and yielding default when absent. Help renders it as [<META>].

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.

run_argv(cmd, ["--host", "example.com"])
Cli.Parsed((example.com, 8080))

help_text

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

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

print(help_text(cmd))
Usage: serve [--host HOST] [--port N] [--help]

run the server

Options:
  -H, --host HOST       server host
  -p, --port N          listen port
  -h, --help            show this help and exit

describe

describe : (Cli.CliError) -> String

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

describe(MissingFlag("host"))
error: required flag '--host' was not provided

run_args

run_args : forall a. (Cli.Command(a)) -> Cli.Outcome(a) ! {Env}

Parse the process arguments against cmd. argv arrives through the Env capability (args), so a recorded run replays its arguments from the trace: CLI parsing is inside the determinism contract.