src/cli.bend source
src/cli.bend on the hub · documented module
# shake/cli: the parser behind main.bend, internal. `parse` reads argv# against a Cli; `help` writes usage for a command path, and `help` is the# subcommand that asks for it (`--help` too, unless the command declares its# own long `help`). A `--` that reaches `parse` ends option parsing; in a# compiled program the runtime takes the first `--` itself.import Base# a flag, a valued option, an option that may repeat, a positional, or a rest# positionaltype ArgKind is Data: Flag{} Opt{} Pos{} Rest{} Many{}# what an argument the words leave unbound falls back to: the value of its# env var, when `parse_env` is given one, else its defaulttype Fallback is Data: Fallback{env: Maybe<&2, String>, default: Maybe<&2, String>}# one argument: name is the binding key; short/long are the CLI spellingstype Arg is Data: Arg{ name: String, short: Maybe<&2, String>, long: Maybe<&2, String>, kind: ArgKind, help: String, required: Bool, fallback: Fallback, choices: List<&2, String> }# a nested commandtype Sub is Data: Sub{ name: String, about: String, args: List<&2, Arg>, subcommands: List<&2, Sub> }# the program: name, about, optional version, top-level args and commandstype Cli is Data: Cli{ name: String, about: String, version: Maybe<&2, String>, args: List<&2, Arg>, subcommands: List<&2, Sub> }# an env var's name and its value, as `parse_env` is given themdef Var() -> Data: Sigma<&2, &2, String, _ => String># one bound name and its texttype Bind is Data: Bind{name: String, value: String}# one command above the current one: its arguments and the bindings made# while it was currenttype Level is Data: Level{args: List<&2, Arg>, binds: List<&2, Bind>}# a successful parse, read one command at a time, as clap's ArgMatches: the# bindings the command made, in binding order, and, when a subcommand was# selected under it, that subcommand's name and Matchedtype Matched is Data: Leaf{+binds: List<&2, Bind>} Node{+binds: List<&2, Bind>, +name: String, +sub: Matched}# a failed parse. NeedHelp is `help` / `help <path>` (or `--help`); NoValue# is an option whose value never came, Missing a required argument nothing# bound. Every error but NeedHelp carries `at`, the command path selected# where it failedtype ParseErr is Data: UnknownFlag{at: List<&2, String>, flag: String} Missing{at: List<&2, String>, name: String} NoValue{at: List<&2, String>, name: String} BadValue{at: List<&2, String>, name: String, value: String} NeedHelp{path: List<&2, String>} Unexpected{at: List<&2, String>, arg: String} Repeated{at: List<&2, String>, name: String}# how the next token is readtype Mode is Data: Free{} Need{name: String} Help{} Dead{err: ParseErr}# the walker: the current command's arguments, the commands above it (the# nearest first, one per name of the selected path), its subcommands, the# selected path, and the current command's own bindingstype St is Data: St{ mode: Mode, args: List<&2, Arg>, up: List<&2, Level>, subs: List<&2, Sub>, path: List<&2, String>, binds: List<&2, Bind>, pos: List<&2, Arg>, raw: Bool, seen: Bool }# the page a help walk has reachedtype How is Data: How{ bin: String, about: String, version: Maybe<&2, String>, args: List<&2, Arg>, subs: List<&2, Sub>, usage: String }# a program specdef app( +name: String, +about: String, version: Maybe<&2, String>, args: List<&2, Arg>, subs: List<&2, Sub>) -> Cli: Cli{name, about, version, args, subs}# a nested commanddef sub( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state +name: String, +about: String, args: List<&2, Arg>, subs: List<&2, Sub>) -> Sub: Sub{name, about, args, subs}# a boolean flagdef flag( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state +name: String, short: Maybe<&2, String>, long: Maybe<&2, String>, +help: String) -> Arg: Arg{name, short, long, Flag{}, help, False{}, Fallback{None{}, None{}}, []}# a valued optiondef opt( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state +name: String, short: Maybe<&2, String>, long: Maybe<&2, String>, +help: String, required: Bool, default: Maybe<&2, String>, choices: List<&2, String>) -> Arg: Arg{name, short, long, Opt{}, help, required, Fallback{None{}, default}, choices}# an option that may be given more than once; `get_all` reads every valuedef many( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state +name: String, short: Maybe<&2, String>, long: Maybe<&2, String>, +help: String, required: Bool, default: Maybe<&2, String>, choices: List<&2, String>) -> Arg: Arg{name, short, long, Many{}, help, required, Fallback{None{}, default}, choices}# a positionaldef pos( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state +name: String, +help: String, required: Bool, default: Maybe<&2, String>, choices: List<&2, String>) -> Arg: Arg{name, None{}, None{}, Pos{}, help, required, Fallback{None{}, default}, choices}# a rest positional: every leftover word, as one namedef rest( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state +name: String, +help: String, required: Bool, default: Maybe<&2, String>, choices: List<&2, String>) -> Arg: Arg{name, None{}, None{}, Rest{}, help, required, Fallback{None{}, default}, choices}# this argument, falling back to the env var `var` (clap's `.env`)def env(aa: Arg, +var: String) -> Arg: Arg{+n, +s, +l, kk, +h, +r, Fallback{_e, dd}, cs} = aa Arg{n, s, l, kk, h, r, Fallback{Some{var}, dd}, cs}# the larger of two Natsdef help.max(+aa: Nat, +bb: Nat) -> Nat: Bool.pick(Nat, Nat.is_le(aa, bb), bb, aa)# n spacesdef help.spaces(nn: Nat) -> String: match nn: case 0n: "" case 1n+p: " " ++ help.spaces(p)# s padded on the right to width ndef help.pad(+ss: String, nn: Nat) -> String: ss ++ help.spaces(Nat.sub(nn, String.length(ss)))# a Maybe string's text, or the defaultdef text.of(mm: Maybe<&2, String>, +dd: String) -> String: Maybe.default(&2, String, mm, dd)# whether a string is a flag token (`-` alone is not)def looks_flag(+ss: String) -> Bool: Bool.and(String.starts_with(ss, "-"), Bool.not(String.eq(ss, "-")))# chars reversed into a stringdef from.rev(cs: List<&2, Char>) -> String: String.from_list(List.reverse(&2, Char, cs))# `name` and an optional `=value`, once the next char is knowndef cut_eq.pick( eq: Bool, acc: List<&2, Char>, tt: List<&2, Char>, rest: String & Maybe<&2, String>) -> String & Maybe<&2, String>: match eq: case True{}: (from.rev(acc), Some{String.from_list(tt)}) case False{}: rest# one char of a `name=value` cutdef cut_eq.chr( +cc: Char, tt: List<&2, Char>, acc: List<&2, Char>, rest: String & Maybe<&2, String>) -> String & Maybe<&2, String>: cut_eq.pick(Char.is_eq(cc, '='), acc, tt, rest)# `name` and an optional `=value` from the body after `--`def cut_eq.go( cs: List<&2, Char>, +acc: List<&2, Char>) -> String & Maybe<&2, String>: match cs: case Nil{}: (from.rev(acc), None{}) case Con{+c, +t}: cut_eq.chr(c, t, acc, cut_eq.go(t, c <> acc))# `name` and an optional `=value`def cut_eq(ss: String) -> String & Maybe<&2, String>: cut_eq.go(String.to_list(ss), [])# the value glued after a short letter: what follows one `=` when the rest# starts with it (possibly empty, as `--name=` is), else the rest when there# is anydef short_of.val(eq: Bool, +rest: String) -> Maybe<&2, String>: match eq: case True{}: Some{String.drop(rest, 1n)} case False{}: Bool.pick(Maybe<&2, String>, String.is_empty(rest), None{}, Some{rest})# whether this Arg's long spelling is `name`def by_long.hit(aa: Arg, +name: String) -> Bool: Arg{_n, _s, long, _k, _h, _r, _d, _c} = aa String.eq(text.of(long, ""), name)# this Arg when the long spelling matches, else the restdef by_long.at(hit: Bool, hh: Arg, rest: Maybe<&2, Arg>) -> Maybe<&2, Arg>: match hit: case True{}: Some{hh} case False{}: rest# the Arg with this long spelling, if anydef by_long(args: List<&2, Arg>, +name: String) -> Maybe<&2, Arg>: match args: case Nil{}: None{} case Con{+h, t}: by_long.at(by_long.hit(h, name), h, by_long(t, name))# whether this Arg's short spelling is `name`def by_short.hit(aa: Arg, +name: String) -> Bool: Arg{_n, short, _l, _k, _h, _r, _d, _c} = aa String.eq(text.of(short, ""), name)# this Arg when the short spelling matches, else the restdef by_short.at(hit: Bool, hh: Arg, rest: Maybe<&2, Arg>) -> Maybe<&2, Arg>: match hit: case True{}: Some{hh} case False{}: rest# the Arg with this short spelling, if anydef by_short(args: List<&2, Arg>, +name: String) -> Maybe<&2, Arg>: match args: case Nil{}: None{} case Con{+h, t}: by_short.at(by_short.hit(h, name), h, by_short(t, name))# whether this Arg's name is `name`def by_name.hit(aa: Arg, +name: String) -> Bool: Arg{+n, _s, _l, _k, _h, _r, _d, _c} = aa String.eq(n, name)# this Arg when the binding name matches, else the restdef by_name.at(hit: Bool, hh: Arg, rest: Maybe<&2, Arg>) -> Maybe<&2, Arg>: match hit: case True{}: Some{hh} case False{}: rest# the Arg with this binding name, if anydef by_name(args: List<&2, Arg>, +name: String) -> Maybe<&2, Arg>: match args: case Nil{}: None{} case Con{+h, t}: by_name.at(by_name.hit(h, name), h, by_name(t, name))# this Sub when its name matches, else the restdef find_sub.at(ss: Sub, rest: Maybe<&2, Sub>, +name: String) -> Maybe<&2, Sub>: Sub{+n, _a, _g, _c} = ss Bool.pick(Maybe<&2, Sub>, String.eq(n, name), Some{ss}, rest)# the Sub with this name, if anydef find_sub(subs: List<&2, Sub>, +name: String) -> Maybe<&2, Sub>: match subs: case Nil{}: None{} case Con{+h, t}: find_sub.at(h, find_sub(t, name), name)# whether this Arg is a positionaldef pos.is.kind(kk: ArgKind) -> Bool: match kk: case Pos{}: True{} case Rest{}: True{} case Flag{}: False{} case Opt{}: False{} case Many{}: False{}# whether this ArgKind is restdef rest.is.kind(kk: ArgKind) -> Bool: match kk: case Rest{}: True{} case Pos{}: False{} case Flag{}: False{} case Opt{}: False{} case Many{}: False{}# whether this Arg is a positionaldef pos.is(aa: Arg) -> Bool: Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa pos.is.kind(kind)# whether this Arg is a rest positionaldef rest.is(aa: Arg) -> Bool: Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa rest.is.kind(kind)# this Arg kept when it is a positionaldef pos_of.put(keep: Bool, hh: Arg, rest: List<&2, Arg>) -> List<&2, Arg>: match keep: case True{}: hh <> rest case False{}: rest# the positionals of a spec, in orderdef pos_of(args: List<&2, Arg>) -> List<&2, Arg>: match args: case Nil{}: Nil{} case Con{+h, t}: pos_of.put(pos.is(h), h, pos_of(t))# whether `v` is among the choices; an empty list accepts any valuedef allowed.ok.at(hit: Bool, rest: Bool) -> Bool: match hit: case True{}: True{} case False{}: rest# whether `v` is listed; the empty list answers nodef allowed.ok(cs: List<&2, String>, +vv: String) -> Bool: match cs: case Nil{}: False{} case Con{+h, t}: allowed.ok.at(String.eq(h, vv), allowed.ok(t, vv))# empty choices accept any value; otherwise `v` must be listeddef allowed(+cs: List<&2, String>, +vv: String) -> Bool: Bool.or(List.is_empty(&2, String, cs), allowed.ok(cs, vv))# whether this bind's name is `name`def bound.hit(bb: Bind, +name: String) -> Bool: Bind{+n, _v} = bb String.eq(n, name)# True when the head matches, else the restdef bound.at(hit: Bool, rest: Bool) -> Bool: match hit: case True{}: True{} case False{}: rest# whether `name` is already bounddef bound(bs: List<&2, Bind>, +name: String) -> Bool: match bs: case Nil{}: False{} case Con{+h, t}: bound.at(bound.hit(h, name), bound(t, name))# this bind's value when the name matches, else the restdef get_all.bind.hit( hit: Bool, +vv: String, more: List<&2, String>) -> List<&2, String>: match hit: case True{}: vv <> more case False{}: more# this bind's value when the name matches, else the restdef get_all.bind.at( bb: Bind, more: List<&2, String>, +name: String) -> List<&2, String>: Bind{+n, v} = bb get_all.bind.hit(String.eq(n, name), v, more)# every value bound to `name`def get_all.bind(bs: List<&2, Bind>, +name: String) -> List<&2, String>: match bs: case Nil{}: [] case Con{+h, t}: get_all.bind.at(h, get_all.bind(t, name), name)# the bindings this command made, in binding orderdef own(mm: Matched) -> List<&2, Bind>: match mm: case Leaf{bs}: bs case Node{bs, _n, _s}: bs# every value this command bound to `name`def get_all(mm: Matched, +name: String) -> List<&2, String>: get_all.bind(own(mm), name)# the last of a list of values, or `dflt` when it is emptydef get.last(vs: List<&2, String>, dflt: Maybe<&2, String>) -> Maybe<&2, String>: match vs: case Nil{}: dflt case Con{hh, tt}: get.last(tt, Some{hh})# the value bound to `name` last, or None when nothing bound itdef get(mm: Matched, +name: String) -> Maybe<&2, String>: get.last(get_all(mm, name), None{})# whether a value is `true`def on.of(mv: Maybe<&2, String>) -> Bool: match mv: case None{}: False{} case Some{vv}: String.eq(vv, "true")# whether a flag named `name` was setdef on(mm: Matched, +name: String) -> Bool: on.of(get(mm, name))# the names of the subcommands selected below this command, in orderdef path_of(mm: Matched) -> List<&2, String>: match mm: case Leaf{_b}: [] case Node{_b, +name, next}: name <> path_of(next)# the name of the subcommand selected under this commanddef sub_name(mm: Matched) -> Maybe<&2, String>: match mm: case Leaf{_b}: None{} case Node{_b, name, _s}: Some{name}# a subcommand's Matched when it is the one asked fordef sub_of.pick(hit: Bool, next: Matched) -> Maybe<&2, Matched>: match hit: case True{}: Some{next} case False{}: None{}# the Matched of subcommand `name`, when it is the one selecteddef sub_of(mm: Matched, +name: String) -> Maybe<&2, Matched>: match mm: case Leaf{_b}: None{} case Node{_b, nn, next}: sub_of.pick(String.eq(nn, name), next)# one name further down, when there is still a Matched to go down fromdef at.step(cur: Maybe<&2, Matched>, +name: String) -> Maybe<&2, Matched>: match cur: case None{}: None{} case Some{mm}: sub_of(mm, name)# the rest of a path, followed from where the walk down standsdef at.go(path: List<&2, String>, cur: Maybe<&2, Matched>) -> Maybe<&2, Matched>: match path: case Nil{}: cur case Con{+hh, tt}: at.go(tt, at.step(cur, hh))# the Matched of the command `path` names below this one, name by name, or# None where a name is not the subcommand selecteddef at(mm: Matched, path: List<&2, String>) -> Maybe<&2, Matched>: at.go(path, Some{mm})# the starting walker for a Clidef parse.start(app: Cli) -> St: Cli{_n, _a, _v, +args, subs} = app St{Free{}, args, [], subs, [], [], pos_of(args), False{}, False{}}# a failed walker that keeps the rest of the statedef parse.dead( ee: ParseErr, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: St{Dead{ee}, args, up, subs, path, binds, pos, raw, seen}# a successful bind of `name` to `val`def parse.put( +name: String, +val: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: St{Free{}, args, up, subs, path, Bind{name, val} <> binds, pos, raw, seen}# the choices of a found Arg, or nonedef parse.choices(mm: Maybe<&2, Arg>) -> List<&2, String>: match mm: case None{}: [] case Some{a}: Arg{_n, _s, _l, _k, _h, _r, _d, cs} = a cs# bind `name` to `val` when the value is alloweddef parse.take_choice( ok: Bool, +name: String, +val: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match ok: case True{}: parse.put(name, val, args, up, subs, path, binds, pos, raw, seen) case False{}: parse.dead(BadValue{path, name, val}, args, up, subs, path, binds, pos, raw, seen)# bind an option that already has its value, checked against the choices of# the current command's argument of that name: a parent's argument of the same# name has its owndef parse.take_opt_val( +name: String, +val: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: parse.take_choice(allowed(parse.choices(by_name(args, name)), val), name, val, args, up, subs, path, binds, pos, raw, seen)# a flag with no glued value is set; a glued value is unexpecteddef parse.take_flag( val: Maybe<&2, String>, +name: String, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match val: case None{}: parse.put(name, "true", args, up, subs, path, binds, pos, raw, seen) case Some{_v}: parse.dead(Unexpected{path, orig}, args, up, subs, path, binds, pos, raw, seen)# an option: glued value now, or the next tokendef parse.take_opt( val: Maybe<&2, String>, +name: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match val: case None{}: St{Need{name}, args, up, subs, path, binds, pos, raw, seen} case Some{v}: parse.take_opt_val(name, v, args, up, subs, path, binds, pos, raw, seen)# a flag or single-valued option given again is refused, whatever the walk# would have done with it; the walker keeps the rest of its statedef parse.once(again: Bool, st: St, +name: String, +path: List<&2, String>) -> St: match again: case True{}: St{_m, +args, +up, +subs, _p, +binds, +pos, +raw, +seen} = st St{Dead{Repeated{path, name}}, args, up, subs, path, binds, pos, raw, seen} case False{}: st# dispatch on the found Arg's kinddef parse.take_arg.kind( kk: ArgKind, +name: String, val: Maybe<&2, String>, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match kk: case Flag{}: parse.once(bound(binds, name), parse.take_flag(val, name, orig, args, up, subs, path, binds, pos, raw, seen), name, path) case Opt{}: parse.once(bound(binds, name), parse.take_opt(val, name, args, up, subs, path, binds, pos, raw, seen), name, path) case Many{}: parse.take_opt(val, name, args, up, subs, path, binds, pos, raw, seen) case Pos{}: parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen) case Rest{}: parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)# a found Arg, or an unknown flagdef parse.take_arg( found: Maybe<&2, Arg>, val: Maybe<&2, String>, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match found: case None{}: parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen) case Some{a}: Arg{+name, _s, _l, kind, _h, _r, _d, _c} = a parse.take_arg.kind(kind, name, val, orig, args, up, subs, path, binds, pos, raw, seen)# whether an unknown long option is a request for help: a bare `--help`,# with no `=` value, before any positional of the current command is bounddef long_help(+name: String, val: Maybe<&2, String>, seen: Bool) -> Bool: match val: case None{}: Bool.and(String.eq(name, "help"), Bool.not(seen)) case Some{_v}: False{}# an unknown long option: a request for help (`--help`, as `help`), or an# unknown flagdef parse.take_long.help( hit: Bool, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match hit: case True{}: St{Help{}, args, up, subs, path, binds, pos, raw, seen} case False{}: parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)# a long option the current command spells, or an unknown onedef parse.take_long.found( found: Maybe<&2, Arg>, +name: String, val: Maybe<&2, String>, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match found: case None{}: parse.take_long.help(long_help(name, val, seen), orig, args, up, subs, path, binds, pos, raw, seen) case Some{a}: parse.take_arg(Some{a}, val, orig, args, up, subs, path, binds, pos, raw, seen)# a long option with a name, or `--=...`def parse.take_long.named( empty: Bool, +name: String, val: Maybe<&2, String>, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match empty: case True{}: parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen) case False{}: parse.take_long.found(by_long(args, name), name, val, orig, args, up, subs, path, binds, pos, raw, seen)# a long option after the `=` cutdef parse.take_long.cut( nv: String & Maybe<&2, String>, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: (+name, val) = nv parse.take_long.named(String.is_empty(name), name, val, orig, args, up, subs, path, binds, pos, raw, seen)# a long option body after `--`def parse.take_long( body: String, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: parse.take_long.cut(cut_eq(body), orig, args, up, subs, path, binds, pos, raw, seen)# a flag's value in a cluster: the rest when it starts with `=` (which# `take_arg` refuses), else none, so the next letter goes ondef cluster.val.flag(eq: Bool, +rest: String) -> Maybe<&2, String>: match eq: case True{}: Some{rest} case False{}: None{}# the value a letter of this kind gives its argument: an option takes the# rest of the word, after one `=`def cluster.val.kind(kk: ArgKind, +rest: String) -> Maybe<&2, String>: match kk: case Flag{}: cluster.val.flag(String.starts_with(rest, "="), rest) case Opt{}: short_of.val(String.starts_with(rest, "="), rest) case Many{}: short_of.val(String.starts_with(rest, "="), rest) case Pos{}: None{} case Rest{}: None{}# the value the letter's argument gets, or none when no argument has itdef cluster.val(found: Maybe<&2, Arg>, +rest: String) -> Maybe<&2, String>: match found: case None{}: None{} case Some{aa}: Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa cluster.val.kind(kind, rest)# whether a letter of this kind ends the cluster: every kind but a flag that# is not followed by `=`def cluster.ends.kind(kk: ArgKind, +rest: String) -> Bool: match kk: case Flag{}: String.starts_with(rest, "=") case Opt{}: True{} case Many{}: True{} case Pos{}: True{} case Rest{}: True{}# whether the letters after this one are no longer read as letters: an# unknown letter, an option (they are its value) or a flag given `=`def cluster.ends(found: Maybe<&2, Arg>, +rest: String) -> Bool: match found: case None{}: True{} case Some{aa}: Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa cluster.ends.kind(kind, rest)# one letter against a walker an earlier letter failed changes nothing;# otherwise it goes through `take_arg`def cluster.apply.mode( mode: Mode, +cc: Char, +rest: String, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match mode: case Dead{e}: St{Dead{e}, args, up, subs, path, binds, pos, raw, seen} case _other: +found = by_short(args, SCon{cc, SNil{}}) parse.take_arg(found, cluster.val(found, rest), orig, args, up, subs, path, binds, pos, raw, seen)# one letter against the walkerdef cluster.apply(st: St, +cc: Char, +rest: String, +orig: String) -> St: St{mode, +args, +up, +subs, +path, +binds, +pos, +raw, +seen} = st cluster.apply.mode(mode, cc, rest, orig, args, up, subs, path, binds, pos, raw, seen)# one letter of a cluster, unless an earlier one ended itdef cluster.letter(done: Bool, +cc: Char, +rest: String, +orig: String, st: St) -> St: match done: case True{}: st case False{}: cluster.apply(st, cc, rest, orig)# whether the cluster has ended after this letterdef cluster.done(done: Bool, +cc: Char, +rest: String, st: St) -> Bool: match done: case True{}: True{} case False{}: St{_m, +args, _g, _s, _p, _b, _o, _r, _n} = st cluster.ends(by_short(args, SCon{cc, SNil{}}), rest)# every letter of a short cluster `-abc`, in order: a flag binds and the next# letter goes on; an option takes the rest of the word, after one `=`, or the# next word when nothing is left; an unknown letter, or a flag given `=`,# refuses the whole worddef cluster(body: String, +orig: String, +st: St, +done: Bool) -> St: match body: case SNil{}: st case SCon{+cc, +rest}: cluster(rest, orig, cluster.letter(done, cc, rest, orig, st), cluster.done(done, cc, rest, st))# a short option body after `-`def parse.take_short( body: String, +orig: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: cluster(body, orig, St{Free{}, args, up, subs, path, binds, pos, raw, seen}, False{})# a rest positional stays; a plain one is consumeddef parse.take_pos.keep(keep: Bool, hh: Arg, tt: List<&2, Arg>) -> List<&2, Arg>: match keep: case True{}: hh <> tt case False{}: tt# the next remaining positional, or an unexpected worddef parse.take_pos( +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, pos: List<&2, Arg>, raw: Bool) -> St: match pos: case Nil{}: parse.dead(Unexpected{path, tok}, args, up, subs, path, binds, pos, raw, True{}) case Con{+h, t}: Arg{+name, _s, _l, _k, _h, _r, _d, +cs} = h parse.take_choice(allowed(cs, tok), name, tok, args, up, subs, path, binds, parse.take_pos.keep(rest.is(h), h, t), raw, True{})# this Sub's arguments become current, with no bindings of its own; the# command it leaves keeps its arguments and bindings above itdef parse.enter( ss: Sub, +args: List<&2, Arg>, +up: List<&2, Level>, +path: List<&2, String>, binds: List<&2, Bind>) -> St: Sub{+name, _a, +next, next_subs} = ss St{Free{}, next, Level{args, binds} <> up, next_subs, List.append(&2, String, path, [name]), [], pos_of(next), False{}, False{}}# required and no default → Some{name}def parse.req.go(req: Bool, bare: Bool, +nn: String) -> Maybe<&2, String>: Bool.pick(Maybe<&2, String>, Bool.and(req, bare), Some{nn}, None{})# a required Arg without a default, as a missing namedef parse.req(aa: Arg) -> Maybe<&2, String>: Arg{+n, _s, _l, _k, _h, +req, Fallback{_e, default}, _c} = aa parse.req.go(req, Maybe.is_none(&2, String, default), n)# this missing name, else the restdef parse.first_req.at( here: Maybe<&2, String>, rest: Maybe<&2, String>) -> Maybe<&2, String>: match here: case Some{n}: Some{n} case None{}: rest# a rest stays in the list, so it does not block a subcommanddef parse.first_req.skip(skip: Bool, aa: Arg) -> Maybe<&2, String>: match skip: case True{}: None{} case False{}: parse.req(aa)# the first leftover required positionaldef parse.first_req(pos: List<&2, Arg>) -> Maybe<&2, String>: match pos: case Nil{}: None{} case Con{+h, t}: parse.first_req.at(parse.first_req.skip(rest.is(h), h), parse.first_req(t))# a leftover required positional blocks entering a subcommanddef parse.enter_or( miss: Maybe<&2, String>, ss: Sub, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match miss: case None{}: parse.enter(ss, args, up, path, binds) case Some{n}: parse.dead(Missing{path, n}, args, up, subs, path, binds, pos, raw, seen)# a found subcommand, or a positionaldef parse.word.go( found: Maybe<&2, Sub>, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match found: case None{}: parse.take_pos(tok, args, up, subs, path, binds, pos, raw) case Some{s}: parse.enter_or(parse.first_req(pos), s, args, up, subs, path, binds, pos, raw, seen)# a word: a subcommand, or a positionaldef parse.word( +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: parse.word.go(find_sub(subs, tok), tok, args, up, subs, path, binds, pos, raw, seen)# `-` as a word, or a short optiondef parse.step.short.go( alone: Bool, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: match alone: case True{}: parse.word(tok, args, up, subs, path, binds, pos, False{}, seen) case False{}: parse.take_short(String.drop(tok, 1n), tok, args, up, subs, path, binds, pos, False{}, seen)# `-` alone is a positional; otherwise a short optiondef parse.step.short( dash: Bool, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: match dash: case True{}: parse.step.short.go(String.is_empty(String.drop(tok, 1n)), tok, args, up, subs, path, binds, pos, seen) case False{}: parse.word(tok, args, up, subs, path, binds, pos, False{}, seen)# `--long` first, then `-s`, then a worddef parse.step.kind.go( long: Bool, dash: Bool, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: match long: case True{}: parse.take_long(String.drop(tok, 2n), tok, args, up, subs, path, binds, pos, False{}, seen) case False{}: parse.step.short(dash, tok, args, up, subs, path, binds, pos, seen)# a long option, a short option, or a worddef parse.step.kind( +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: parse.step.kind.go(String.starts_with(tok, "--"), String.starts_with(tok, "-"), tok, args, up, subs, path, binds, pos, seen)# `help` in command position collects the rest of argv as a pathdef parse.step.help( hit: Bool, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: match hit: case True{}: St{Help{}, args, up, subs, path, binds, pos, False{}, seen} case False{}: parse.step.kind(tok, args, up, subs, path, binds, pos, seen)# `--` ends option parsingdef parse.step.end( end: Bool, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: match end: case True{}: St{Free{}, args, up, subs, path, binds, pos, True{}, seen} case False{}: parse.step.help(Bool.and(String.eq(tok, "help"), Bool.not(seen)), tok, args, up, subs, path, binds, pos, seen)# after `--`, every token is a positionaldef parse.step.raw( raw: Bool, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +seen: Bool) -> St: match raw: case True{}: parse.take_pos(tok, args, up, subs, path, binds, pos, True{}) case False{}: parse.step.end(String.eq(tok, "--"), tok, args, up, subs, path, binds, pos, seen)# a pending option value: a flag-shaped token is a missing valuedef parse.step.need( flag: Bool, +name: String, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match flag: case True{}: parse.dead(NoValue{path, name}, args, up, subs, path, binds, pos, raw, seen) case False{}: parse.take_opt_val(name, tok, args, up, subs, path, binds, pos, raw, seen)# a word after `help`: the subcommand it names, whose own subcommands the# next word is looked up in, or an unexpected worddef parse.step.help_path( found: Maybe<&2, Sub>, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match found: case None{}: parse.dead(Unexpected{path, tok}, args, up, subs, path, binds, pos, raw, seen) case Some{ss}: Sub{_n, _a, _g, next_subs} = ss St{Help{}, args, up, next_subs, List.append(&2, String, path, [tok]), binds, pos, raw, seen}# one token against the walkerdef parse.step.mode( mode: Mode, +tok: String, +args: List<&2, Arg>, +up: List<&2, Level>, +subs: List<&2, Sub>, +path: List<&2, String>, +binds: List<&2, Bind>, +pos: List<&2, Arg>, +raw: Bool, +seen: Bool) -> St: match mode: case Dead{e}: St{Dead{e}, args, up, subs, path, binds, pos, raw, seen} case Need{name}: parse.step.need(looks_flag(tok), name, tok, args, up, subs, path, binds, pos, raw, seen) case Help{}: parse.step.help_path(find_sub(subs, tok), tok, args, up, subs, path, binds, pos, raw, seen) case Free{}: parse.step.raw(raw, tok, args, up, subs, path, binds, pos, seen)# one tokendef parse.step(+tok: String, st: St) -> St: St{mode, args, up, subs, path, binds, pending, raw, seen} = st parse.step.mode(mode, tok, args, up, subs, path, binds, pending, raw, seen)# every token, in orderdef parse.walk(argv: List<&2, String>, st: St) -> St: match argv: case Nil{}: st case Con{+h, t}: parse.walk(t, parse.step(h, st))# a default value kept when the name is not already bounddef fill.one( skip: Bool, +name: String, +val: String, rest: List<&2, Bind>) -> List<&2, Bind>: match skip: case True{}: rest case False{}: Bind{name, val} <> rest# a Some default is filled when unbounddef fill.arg.go( dd: Maybe<&2, String>, +name: String, +binds: List<&2, Bind>) -> List<&2, Bind>: match dd: case None{}: binds case Some{v}: fill.one(bound(binds, name), name, v, binds)# this Arg's default, when it has one and is not bounddef fill.arg(aa: Arg, +binds: List<&2, Bind>) -> List<&2, Bind>: Arg{+n, _s, _l, _k, _h, _r, Fallback{_e, default}, _c} = aa fill.arg.go(default, n, binds)# defaults of every spec Argdef fill.args(args: List<&2, Arg>, +binds: List<&2, Bind>) -> List<&2, Bind>: match args: case Nil{}: binds case Con{+h, t}: fill.arg(h, fill.args(t, binds))# a required name that is not bounddef miss.one( need: Bool, +name: String, rest: Maybe<&2, String>) -> Maybe<&2, String>: match need: case True{}: Some{name} case False{}: rest# whether this Arg is required and unbounddef miss.arg( aa: Arg, +binds: List<&2, Bind>, rest: Maybe<&2, String>) -> Maybe<&2, String>: Arg{+n, _s, _l, _k, _h, +req, _d, _c} = aa miss.one(Bool.and(req, Bool.not(bound(binds, n))), n, rest)# the first required name that is not bounddef miss.args(args: List<&2, Arg>, +binds: List<&2, Bind>) -> Maybe<&2, String>: match args: case Nil{}: None{} case Con{+h, t}: miss.arg(h, binds, miss.args(t, binds))# each command's defaults, filled in among its own bindingsdef fill.levels(lvs: List<&2, Level>) -> List<&2, Level>: match lvs: case Nil{}: [] case Con{hh, tt}: Level{+args, binds} = hh Level{args, fill.args(args, binds)} <> fill.levels(tt)# the first required argument a command left unbound, in list orderdef miss.levels(lvs: List<&2, Level>) -> Maybe<&2, String>: match lvs: case Nil{}: None{} case Con{hh, tt}: Level{+args, +binds} = hh parse.first_req.at(miss.args(args, binds), miss.levels(tt))# the bindings a command madedef level.binds(lv: Level) -> List<&2, Bind>: Level{_a, binds} = lv binds# each command's Matched, root first, with the name selected under itdef matched.build(lvs: List<&2, Level>, names: List<&2, String>) -> Matched: match lvs: case Nil{}: Leaf{[]} case Con{hh, tt}: match names: case Nil{}: Leaf{List.reverse(&2, Bind, level.binds(hh))} case Con{nn, ns}: Node{List.reverse(&2, Bind, level.binds(hh)), nn, matched.build(tt, ns)}# a missing required name, or the matched resultdef parse.finish.miss( mm: Maybe<&2, String>, path: List<&2, String>, +lvs: List<&2, Level>) -> Result<&2, &2, ParseErr, Matched>: match mm: case Some{n}: Fail{Missing{path, n}} case None{}: Done{matched.build(lvs, path)}# every command's defaults, then its required arguments, root firstdef parse.finish.ok(+path: List<&2, String>, +lvs: List<&2, Level>) -> Result<&2, &2, ParseErr, Matched>: parse.finish.miss(miss.levels(lvs), path, lvs)# a finished walker: help, a pending value, an error, or defaults + requireddef parse.finish.mode( mode: Mode, +args: List<&2, Arg>, +up: List<&2, Level>, +path: List<&2, String>, +binds: List<&2, Bind>) -> Result<&2, &2, ParseErr, Matched>: match mode: case Dead{e}: Fail{e} case Need{name}: Fail{NoValue{path, name}} case Help{}: Fail{NeedHelp{path}} case Free{}: parse.finish.ok(path, List.reverse(&2, Level, fill.levels(Level{args, binds} <> up)))# the walker, finisheddef parse.finish(st: St) -> Result<&2, &2, ParseErr, Matched>: St{mode, args, up, _s, path, binds, _p, _r, _n} = st parse.finish.mode(mode, args, up, path, binds)# argv against a Clidef parse(app: Cli, argv: List<&2, String>) -> Result<&2, &2, ParseErr, Matched>: parse.finish(parse.walk(argv, parse.start(app)))# this pair's value when its name matches, else the restdef env.get.at(hit: Bool, +vv: String, rest: Maybe<&2, String>) -> Maybe<&2, String>: match hit: case True{}: Some{vv} case False{}: rest# the value of the first pair named `name`def env.get(vars: List<&2, Var>, +name: String) -> Maybe<&2, String>: match vars: case Nil{}: None{} case Con{hh, tt}: (+nn, +vv) = hh env.get.at(String.eq(nn, name), vv, env.get(tt, name))# a value that is not emptydef env.full(mv: Maybe<&2, String>) -> Maybe<&2, String>: match mv: case None{}: None{} case Some{+vv}: Bool.pick(Maybe<&2, String>, String.is_empty(vv), None{}, Some{vv})# the env value of an env var: its first pair's value, when that is not emptydef env.of(ev: Maybe<&2, String>, vars: List<&2, Var>) -> Maybe<&2, String>: match ev: case None{}: None{} case Some{+var}: env.full(env.get(vars, var))# whether an env value turns a flag off, as clap's `SetTrue` reads itdef env.off(+vv: String) -> Bool: allowed.ok(["0", "false", "no", "off", "n", "f"], String.to_lower(vv))# the default an env value gives an argument of this kind: a flag is set# unless the value turns it off; every other kind takes the valuedef env.kind(kk: ArgKind, +vv: String) -> Maybe<&2, String>: match kk: case Flag{}: Bool.pick(Maybe<&2, String>, env.off(vv), None{}, Some{"true"}) case Opt{}: Some{vv} case Many{}: Some{vv} case Pos{}: Some{vv} case Rest{}: Some{vv}# a fallback with its default replaced by what the env value gives, when# there is onedef resolve.with( found: Maybe<&2, String>, kk: ArgKind, ev: Maybe<&2, String>, dd: Maybe<&2, String>) -> Fallback: match found: case None{}: Fallback{ev, dd} case Some{+vv}: Fallback{ev, env.kind(kk, vv)}# an argument whose default is its fallback under `vars`def resolve.arg(aa: Arg, vars: List<&2, Var>) -> Arg: Arg{+n, +s, +l, +kk, +h, +r, Fallback{+ev, dd}, +cs} = aa Arg{n, s, l, kk, h, r, resolve.with(env.of(ev, vars), kk, ev, dd), cs}# every argument's default is its fallbackdef resolve.args(args: List<&2, Arg>, +vars: List<&2, Var>) -> List<&2, Arg>: match args: case Nil{}: [] case Con{hh, tt}: resolve.arg(hh, vars) <> resolve.args(tt, vars)# every argument of every subcommand, its default its fallbackdef resolve.subs(subs: List<&2, Sub>, +vars: List<&2, Var>) -> List<&2, Sub>: match subs: case Nil{}: [] case Con{hh, tt}: Sub{+n, +a, args, kids} = hh Sub{n, a, resolve.args(args, vars), resolve.subs(kids, vars)} <> resolve.subs(tt, vars)# the spec whose every argument's default is its fallback under `vars`def resolve(app: Cli, +vars: List<&2, Var>) -> Cli: Cli{+n, +a, +v, args, subs} = app Cli{n, a, v, resolve.args(args, vars), resolve.subs(subs, vars)}# whether this kind is a flagdef env.flag.is(kk: ArgKind) -> Bool: match kk: case Flag{}: True{} case Opt{}: False{} case Many{}: False{} case Pos{}: False{} case Rest{}: False{}# an argument bound to an env value outside its choices: never a flag, and# only for a value its command's bindings holddef env.bad.val( found: Maybe<&2, String>, kk: ArgKind, +name: String, +cs: List<&2, String>, +binds: List<&2, Bind>) -> Maybe<&2, Var>: match found: case None{}: None{} case Some{+vv}: Bool.pick(Maybe<&2, Var>, Bool.and(Bool.not(env.flag.is(kk)), Bool.and(Bool.not(allowed(cs, vv)), List.contains(~String, ~String.eq, get_all.bind(binds, name), vv))), Some{(name, vv)}, None{})# this argument's name and env value, when its command's bindings hold that# value and its choices do notdef env.bad.arg(aa: Arg, vars: List<&2, Var>, +binds: List<&2, Bind>) -> Maybe<&2, Var>: Arg{+n, _s, _l, kk, _h, _r, Fallback{ev, _d}, +cs} = aa env.bad.val(env.of(ev, vars), kk, n, cs, binds)# the first of two answers that is onedef env.first(here: Maybe<&2, Var>, rest: Maybe<&2, Var>) -> Maybe<&2, Var>: match here: case None{}: rest case Some{pp}: Some{pp}# the first argument of a command bound to an env value outside its choicesdef env.bad.args( args: List<&2, Arg>, +vars: List<&2, Var>, +binds: List<&2, Bind>) -> Maybe<&2, Var>: match args: case Nil{}: None{} case Con{hh, tt}: env.first(env.bad.arg(hh, vars, binds), env.bad.args(tt, vars, binds))# the arguments of a found subcommand, or nonedef env.sub.args(found: Maybe<&2, Sub>) -> List<&2, Arg>: match found: case None{}: [] case Some{ss}: Sub{_n, _a, args, _k} = ss args# the subcommands of a found subcommand, or nonedef env.sub.kids(found: Maybe<&2, Sub>) -> List<&2, Sub>: match found: case None{}: [] case Some{ss}: Sub{_n, _a, _g, kids} = ss kids# the first argument bound to an env value outside its choices, the command# first, then down the selected pathdef env.bad.go( mm: Matched, +args: List<&2, Arg>, +subs: List<&2, Sub>, +vars: List<&2, Var>) -> Maybe<&2, Var>: match mm: case Leaf{+bs}: env.bad.args(args, vars, bs) case Node{+bs, +name, next}: env.first(env.bad.args(args, vars, bs), env.bad.go(next, env.sub.args(find_sub(subs, name)), env.sub.kids(find_sub(subs, name)), vars))# a success, or the first env value outside its choices that it bounddef env.check.done(bad: Maybe<&2, Var>, +mm: Matched) -> Result<&2, &2, ParseErr, Matched>: match bad: case None{}: Done{mm} case Some{pp}: (+name, +vv) = pp Fail{BadValue{path_of(mm), name, vv}}# a successful parse checked for env values outside their choicesdef env.check.ok(app: Cli, +vars: List<&2, Var>, +mm: Matched) -> Result<&2, &2, ParseErr, Matched>: Cli{_n, _a, _v, +args, subs} = app env.check.done(env.bad.go(mm, args, subs, vars), mm)# a failed parse as it is; a successful one checked for env values outside# their choicesdef env.check( app: Cli, +vars: List<&2, Var>, got: Result<&2, &2, ParseErr, Matched>) -> Result<&2, &2, ParseErr, Matched>: match got: case Fail{ee}: Fail{ee} case Done{+mm}: env.check.ok(app, vars, mm)# argv against a Cli, every argument falling back to its env value under# `vars`, then to its defaultdef parse_env( +app: Cli, argv: List<&2, String>, +vars: List<&2, Var>) -> Result<&2, &2, ParseErr, Matched>: env.check(app, vars, parse(resolve(app, vars), argv))# an env var in front of the names, when there is onedef env.names.put(ev: Maybe<&2, String>, rest: List<&2, String>) -> List<&2, String>: match ev: case None{}: rest case Some{var}: var <> rest# the env vars of a command's arguments, in orderdef env.names.args(args: List<&2, Arg>) -> List<&2, String>: match args: case Nil{}: [] case Con{hh, tt}: Arg{_n, _s, _l, _k, _h, _r, Fallback{ev, _d}, _c} = hh env.names.put(ev, env.names.args(tt))# the env vars of every subcommand's arguments, depth firstdef env.names.forest(subs: List<&2, Sub>) -> List<&2, String>: match subs: case Nil{}: [] case Con{hh, tt}: Sub{_n, _a, args, kids} = hh List.append(&2, String, env.names.args(args), List.append(&2, String, env.names.forest(kids), env.names.forest(tt)))# the env vars of a spec: the root's arguments', then every subcommand'sdef env.names(app: Cli) -> List<&2, String>: Cli{_n, _a, _v, args, subs} = app List.append(&2, String, env.names.args(args), env.names.forest(subs))# the short/long label of an option, plus a metavar for Optdef help.label.kind(kk: ArgKind, +name: String, +label: String) -> String: match kk: case Flag{}: label case Opt{}: label ++ " <" ++ String.to_upper(name) ++ ">" case Many{}: label ++ " <" ++ String.to_upper(name) ++ ">..." case Pos{}: label case Rest{}: label# `-s, --long` or one of themdef help.label.pair(+short: String, +long: String) -> String: Bool.pick(String, String.is_empty(short), Bool.pick(String, String.is_empty(long), "", " --" ++ long), Bool.pick(String, String.is_empty(long), " -" ++ short, " -" ++ short ++ ", --" ++ long))# the usage label of an optiondef help.label(aa: Arg) -> String: Arg{+name, short, long, kind, _h, _r, _d, _c} = aa help.label.kind(kind, name, help.label.pair(text.of(short, ""), text.of(long, "")))# extras after the help string: default and choicesdef help.extra.go(+dd: String, +cs: List<&2, String>) -> String: Bool.pick(String, String.is_empty(dd), Bool.pick(String, List.is_empty(&2, String, cs), "", " [possible values: " ++ String.join(cs, ", ") ++ "]"), Bool.pick(String, List.is_empty(&2, String, cs), " [default: " ++ dd ++ "]", " [default: " ++ dd ++ "] [possible values: " ++ String.join(cs, ", ") ++ "]"))# the env var after the help string, before the defaultdef help.env(ev: Maybe<&2, String>) -> String: match ev: case None{}: "" case Some{var}: " [env: " ++ var ++ "]"# extras after the help string: env var, default and choicesdef help.extra(aa: Arg) -> String: Arg{_n, _s, _l, _k, _h, _r, Fallback{ev, default}, cs} = aa help.env(ev) ++ help.extra.go(text.of(default, ""), cs)# one option line, given the label widthdef help.opt_line(+aa: Arg, ww: Nat) -> String: Arg{_n, _s, _l, _k, +h, _r, _d, _c} = aa help.pad(help.label(aa), ww) ++ " " ++ h ++ help.extra(aa)# the max option-label widthdef help.opt_width(args: List<&2, Arg>, nn: Nat) -> Nat: match args: case Nil{}: nn case Con{+h, t}: help.opt_width(t, help.max(nn, String.length(help.label(h))))# a non-positional option linedef help.opt_lines.at(skip: Bool, hh: Arg, rest: String, ww: Nat) -> String: match skip: case True{}: rest case False{}: help.opt_line(hh, ww) ++ "\n" ++ rest# option lines, skipping positionalsdef help.opt_lines(args: List<&2, Arg>, +ww: Nat) -> String: match args: case Nil{}: "" case Con{+h, t}: help.opt_lines.at(pos.is(h), h, help.opt_lines(t, ww), ww)# wrap option lines in a headingdef help.opts.go(+body: String) -> String: Bool.pick(String, String.is_empty(body), "", "Options:\n" ++ body ++ "\n")# the Options block, or empty when there are nonedef help.opts(+args: List<&2, Arg>) -> String: help.opts.go(help.opt_lines(args, help.opt_width(args, 0n)))# a command's namedef help.sub_name(ss: Sub) -> String: Sub{n, _a, _g, _c} = ss n# a command's aboutdef help.sub_about(ss: Sub) -> String: Sub{_n, a, _g, _c} = ss a# max command-name width, including `help`def help.cmd_width(subs: List<&2, Sub>, nn: Nat) -> Nat: match subs: case Nil{}: help.max(nn, String.length("help")) case Con{+h, t}: help.cmd_width(t, help.max(nn, String.length(help.sub_name(h))))# one command linedef help.cmd_line(+name: String, +about: String, ww: Nat) -> String: " " ++ help.pad(name, ww) ++ " " ++ about ++ "\n"# command lines, then `help`def help.cmd_lines(subs: List<&2, Sub>, +ww: Nat) -> String: match subs: case Nil{}: help.cmd_line("help", "Print this message or the help of the given subcommand(s)", ww) case Con{+h, t}: help.cmd_line(help.sub_name(h), help.sub_about(h), ww) ++ help.cmd_lines(t, ww)# the Commands blockdef help.cmds(+subs: List<&2, Sub>) -> String: "Commands:\n" ++ help.cmd_lines(subs, help.cmd_width(subs, 0n)) ++ "\n"# a rest mark: `...`, or emptydef help.pos.dots(many: Bool) -> String: Bool.pick(String, many, "...", "")# a positional in a usage line: `<NAME>` or `[NAME]`def help.pos.wrap(req: Bool, +nn: String) -> String: Bool.pick(String, req, " <" ++ nn ++ ">", " [" ++ nn ++ "]")# a positional in a usage line: `<NAME>` or `[NAME]`, plus `...` when restdef help.pos.one(aa: Arg) -> String: Arg{+n, _s, _l, kind, _h, +req, _d, _c} = aa help.pos.wrap(req, String.to_upper(n)) ++ help.pos.dots(rest.is.kind(kind))# this positional's hint, else the restdef help.pos.at(keep: Bool, hh: Arg, rest: String) -> String: match keep: case True{}: help.pos.one(hh) ++ rest case False{}: rest# every positional in a usage linedef help.pos(args: List<&2, Arg>) -> String: match args: case Nil{}: "" case Con{+h, t}: help.pos.at(pos.is(h), h, help.pos(t))# the bracketed options word when the spec has a flag or optiondef help.opts_hint.go(has: Bool) -> String: Bool.pick(String, has, " [" ++ String.to_upper("options") ++ "]", "")# True when this Arg is a flag or option, else the restdef help.has_opt.at(is_pos: Bool, rest: Bool) -> Bool: match is_pos: case True{}: rest case False{}: True{}# whether any Arg is not a positionaldef help.has_opt(args: List<&2, Arg>) -> Bool: match args: case Nil{}: False{} case Con{+h, t}: help.has_opt.at(pos.is(h), help.has_opt(t))# the options hint when neededdef help.opts_hint(args: List<&2, Arg>) -> String: help.opts_hint.go(help.has_opt(args))# `[COMMAND]` when there are subcommandsdef help.cmd_hint(+subs: List<&2, Sub>) -> String: Bool.pick(String, List.is_empty(&2, Sub, subs), "", " [COMMAND]")# `Usage: name ...`def help.usage( +name: String, +args: List<&2, Arg>, +subs: List<&2, Sub>) -> String: "Usage: " ++ name ++ help.opts_hint(args) ++ help.pos(args) ++ help.cmd_hint(subs)# `name` or `name version`def title.go(+name: String, vv: Maybe<&2, String>) -> String: match vv: case None{}: name case Some{ver}: name ++ " " ++ ver# one help pagedef help.page( +title: String, +about: String, +usage: String, +args: List<&2, Arg>, +subs: List<&2, Sub>) -> String: title ++ "\n" ++ about ++ "\n\n" ++ usage ++ "\n\n" ++ help.cmds(subs) ++ help.opts(args)# a found child becomes the page; an unknown name staysdef help.step.go( found: Maybe<&2, Sub>, +bin: String, +about: String, version: Maybe<&2, String>, args: List<&2, Arg>, subs: List<&2, Sub>, +next: String, +usage: String) -> How: match found: case None{}: How{bin, about, version, args, subs, usage} case Some{s}: Sub{+name, +about2, args2, subs2} = s How{name, about2, None{}, args2, subs2, usage ++ " " ++ next}# one path segment against the current pagedef help.step(how: How, +name: String) -> How: How{+bin, +about, version, args, +subs, +usage} = how help.step.go(find_sub(subs, name), bin, about, version, args, subs, name, usage)# the page at the end of a shrinking pathdef help.go(path: List<&2, String>, how: How) -> String: match path: case Nil{}: How{+bin, +about, version, +args, +subs, +usage} = how help.page(title.go(bin, version), about, help.usage(usage, args, subs), args, subs) case Con{+h, t}: help.go(t, help.step(how, h))# usage for `path` under `app`; an unknown segment shows the last found pagedef help(+app: Cli, path: List<&2, String>) -> String: Cli{+name, +about, version, args, subs} = app help.go(path, How{name, about, version, args, subs, name})# the usage line of the page at the end of a shrinking pathdef usage.go(path: List<&2, String>, how: How) -> String: match path: case Nil{}: How{_b, _a, _v, +args, +subs, +usage} = how help.usage(usage, args, subs) case Con{+hh, tt}: usage.go(tt, help.step(how, hh))# the usage line of the command at `path`, as `help` shows itdef usage.at(+app: Cli, path: List<&2, String>) -> String: Cli{+name, +about, version, args, subs} = app usage.go(path, How{name, about, version, args, subs, name})# where to look for more: `help`, and the path when there is onedef err.hint(+path: List<&2, String>) -> String: "For more information, try 'help" ++ Bool.pick(String, List.is_empty(&2, String, path), "", " " ++ String.join(path, " ")) ++ "'.\n"# a short error, the usage line of the command where it happened, and the hintdef err.wrap(+app: Cli, +at: List<&2, String>, +msg: String) -> String: msg ++ "\n\n" ++ usage.at(app, at) ++ "\n\n" ++ err.hint(at)# the head of a row, or 0 past its enddef osa.head(xs: List<&2, Nat>) -> Nat: match xs: case Nil{}: 0n case Con{hh, _t}: hh# a row after its headdef osa.tail(xs: List<&2, Nat>) -> List<&2, Nat>: match xs: case Nil{}: [] case Con{_h, tt}: tt# the last cell of a row, `dd` for an empty onedef osa.last(xs: List<&2, Nat>, dd: Nat) -> Nat: match xs: case Nil{}: dd case Con{hh, tt}: osa.last(tt, hh)# the first row: `ii`, then one more for each chardef osa.first(bs: List<&2, Char>, +ii: Nat) -> List<&2, Nat>: match bs: case Nil{}: [ii] case Con{_c, tt}: ii <> osa.first(tt, 1n+ii)# whether the char before `cb` is `ca`, once the char before `ca` is `xa`def osa.swap.b(+xa: Char, pb: Maybe<&2, Char>, +ca: Char, +cb: Char) -> Bool: match pb: case None{}: False{} case Some{xb}: Bool.and(Char.is_eq(ca, xb), Char.is_eq(xa, cb))# whether the two chars before these, swapped, are these twodef osa.swap(pa: Maybe<&2, Char>, pb: Maybe<&2, Char>, +ca: Char, +cb: Char) -> Bool: match pa: case None{}: False{} case Some{xa}: osa.swap.b(xa, pb, ca, cb)# the cheaper of a cell and a swap from two rows up and two cells leftdef osa.cell.swap(swap: Bool, +best: Nat, +far: Nat) -> Nat: Bool.pick(Nat, swap, Nat.min(best, 1n+far), best)# one cell: the cheapest of a deletion, an insertion, a substitution and a# swap of two adjacent charsdef osa.cell( swap: Bool, +up: Nat, +left: Nat, +diag: Nat, +cost: Nat, +far: Nat) -> Nat: osa.cell.swap(swap, Nat.min(Nat.min(1n+up, 1n+left), Nat.add(diag, cost)), far)# the rest of a row for the char `ca`, whose char before is `pa`: `ps` is the# row above from the cell above-left on, `qs` the row two up from two cells# left on, `pb` the char of `bs` before its headdef osa.row( bs: List<&2, Char>, +ca: Char, +pa: Maybe<&2, Char>, pb: Maybe<&2, Char>, left: Nat, +ps: List<&2, Nat>, +qs: List<&2, Nat>) -> List<&2, Nat>: match bs: case Nil{}: [] case Con{+cb, bt}: +rr = osa.cell(osa.swap(pa, pb, ca, cb), osa.head(osa.tail(ps)), left, osa.head(ps), Bool.pick(Nat, Char.is_eq(ca, cb), 0n, 1n), osa.head(qs)) rr <> osa.row(bt, ca, pa, Some{cb}, rr, osa.tail(ps), osa.tail(qs))# the rows for the chars of `as`, the last row `pp` and the one before `qq`,# down to the distance in the last row's last celldef osa.rows( as: List<&2, Char>, +bs: List<&2, Char>, pa: Maybe<&2, Char>, +ii: Nat, +pp: List<&2, Nat>, qq: List<&2, Nat>) -> Nat: match as: case Nil{}: osa.last(pp, 0n) case Con{+ca, at}: osa.rows(at, bs, Some{ca}, 1n+ii, (1n+ii) <> osa.row(bs, ca, pa, None{}, 1n+ii, pp, 0n <> qq), pp)# the optimal string alignment distance from `aa` to `bb`: the fewest# insertions, deletions, substitutions and swaps of two adjacent chars, no# char edited twicedef osa(aa: String, bb: String) -> Nat: +bs = String.to_list(bb) osa.rows(String.to_list(aa), bs, None{}, 0n, osa.first(bs, 0n), osa.first(bs, 0n))# whether a candidate is close to the typed spellingdef sug.close.go(+dd: Nat, +room: Nat) -> Bool: Bool.and(Nat.is_lt(0n, dd), Nat.is_le(Nat.mul(3n, dd), room))# whether `cc` is close to `typed`: some edits away, and at most a third of# the longer as manydef sug.close(+typed: String, +cc: String) -> Bool: sug.close.go(osa(typed, cc), Nat.max(String.length(typed), String.length(cc)))# a close candidate against the closest one after it: the earlier on a tiedef sug.keep.rest(+typed: String, +cc: String, rest: Maybe<&2, String>) -> Maybe<&2, String>: match rest: case None{}: Some{cc} case Some{+ww}: Bool.pick(Maybe<&2, String>, Nat.is_le(osa(typed, cc), osa(typed, ww)), Some{cc}, Some{ww})# a candidate, when it is close, against the closest one after itdef sug.keep(close: Bool, +typed: String, +cc: String, rest: Maybe<&2, String>) -> Maybe<&2, String>: match close: case False{}: rest case True{}: sug.keep.rest(typed, cc, rest)# the closest close candidate, the first among equalsdef sug.best(cands: List<&2, String>, +typed: String) -> Maybe<&2, String>: match cands: case Nil{}: None{} case Con{+cc, tt}: sug.keep(sug.close(typed, cc), typed, cc, sug.best(tt, typed))# a long spelling in front of the rest, when there is onedef sug.cands.put(long: Maybe<&2, String>, rest: List<&2, String>) -> List<&2, String>: match long: case None{}: rest case Some{ll}: ll <> rest# the long spellings of a command's arguments, in orderdef sug.cands(args: List<&2, Arg>) -> List<&2, String>: match args: case Nil{}: [] case Con{hh, tt}: Arg{_n, _s, long, _k, _h, _r, _d, _c} = hh sug.cands.put(long, sug.cands(tt))# the arguments of the page at the end of a shrinking pathdef sug.args.go(path: List<&2, String>, how: How) -> List<&2, Arg>: match path: case Nil{}: How{_b, _a, _v, args, _s, _u} = how args case Con{+hh, tt}: sug.args.go(tt, help.step(how, hh))# the arguments of the command at `path`, reached as `help` reaches itdef sug.args(+app: Cli, path: List<&2, String>) -> List<&2, Arg>: Cli{+name, +about, version, args, subs} = app sug.args.go(path, How{name, about, version, args, subs, name})# the spelling a long option word was typed with: its chars up to `=`def sug.typed(cut: String & Maybe<&2, String>) -> String: (nn, _v) = cut nn# a candidate spelled as a long option worddef sug.found(mm: Maybe<&2, String>) -> Maybe<&2, String>: match mm: case None{}: None{} case Some{cc}: Some{"--" ++ cc}# the suggestion for a word, when it is a long option worddef sug.word.go(long: Bool, +app: Cli, +at: List<&2, String>, +word: String) -> Maybe<&2, String>: match long: case False{}: None{} case True{}: sug.found(sug.best(sug.cands(sug.args(app, at)), sug.typed(cut_eq(String.drop(word, 2n)))))# the long spelling a word refused at `at` was likely meant to bedef sug.word(+app: Cli, +at: List<&2, String>, +word: String) -> Maybe<&2, String>: sug.word.go(String.starts_with(word, "--"), app, at, word)# the long spelling an unknown long option was likely meant to be, as clap's# "a similar argument exists"; None for every other errordef suggestion(+app: Cli, ee: ParseErr) -> Maybe<&2, String>: match ee: case UnknownFlag{+at, +word}: sug.word(app, at, word) case Missing{_at, _name}: None{} case NoValue{_at, _name}: None{} case BadValue{_at, _name, _value}: None{} case NeedHelp{_path}: None{} case Unexpected{_at, _arg}: None{} case Repeated{_at, _name}: None{}# clap's tip line for a suggestion, or nothingdef sug.tip(mm: Maybe<&2, String>) -> String: match mm: case None{}: "" case Some{ss}: "\n\n tip: a similar argument exists: '" ++ ss ++ "'"# error text; NeedHelp is empty (the caller prints `help`)def err_text(+app: Cli, ee: ParseErr) -> String: match ee: case UnknownFlag{+at, +word}: err.wrap(app, at, "error: unexpected argument '" ++ word ++ "' found" ++ sug.tip(sug.word(app, at, word))) case Missing{+at, name}: err.wrap(app, at, "error: the following required argument was not provided:\n " ++ name) case NoValue{+at, name}: err.wrap(app, at, "error: a value is required for '" ++ name ++ "' but none was supplied") case BadValue{+at, name, value}: err.wrap(app, at, "error: invalid value '" ++ value ++ "' for '" ++ name ++ "'") case NeedHelp{_path}: "" case Unexpected{+at, arg}: err.wrap(app, at, "error: unexpected argument '" ++ arg ++ "' found") case Repeated{+at, name}: err.wrap(app, at, "error: the argument '" ++ name ++ "' cannot be used multiple times")