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.