src/cli.bend
shake@0.4.0.0 · 1,725 lines · 0 definitions
1# shake/cli: the parser behind main.bend, internal. `parse` reads argv
2# against a Cli; `help` writes usage for a command path, and `help` is the
3# subcommand that asks for it (`--help` too, unless the command declares its
4# own long `help`). A `--` that reaches `parse` ends option parsing; in a
5# compiled program the runtime takes the first `--` itself.
6import Base
7
8# a flag, a valued option, an option that may repeat, a positional, or a rest
9# positional
10type ArgKind is Data:
11 Flag{}
12 Opt{}
13 Pos{}
14 Rest{}
15 Many{}
16
17# one argument: name is the binding key; short/long are the CLI spellings
18type Arg is Data:
19 Arg{
20 name: String,
21 short: Maybe<&2, String>,
22 long: Maybe<&2, String>,
23 kind: ArgKind,
24 help: String,
25 required: Bool,
26 default: Maybe<&2, String>,
27 choices: List<&2, String>
28 }
29
30# a nested command
31type Sub is Data:
32 Sub{
33 name: String,
34 about: String,
35 args: List<&2, Arg>,
36 subcommands: List<&2, Sub>
37 }
38
39# the program: name, about, optional version, top-level args and commands
40type Cli is Data:
41 Cli{
42 name: String,
43 about: String,
44 version: Maybe<&2, String>,
45 args: List<&2, Arg>,
46 subcommands: List<&2, Sub>
47 }
48
49# one bound name and its text
50type Bind is Data:
51 Bind{name: String, value: String}
52
53# one command above the current one: its arguments and the bindings made
54# while it was current
55type Level is Data:
56 Level{args: List<&2, Arg>, binds: List<&2, Bind>}
57
58# a successful parse, read one command at a time, as clap's ArgMatches: the
59# bindings the command made, in binding order, and, when a subcommand was
60# selected under it, that subcommand's name and Matched
61type Matched is Data:
62 Leaf{+binds: List<&2, Bind>}
63 Node{+binds: List<&2, Bind>, +name: String, +sub: Matched}
64
65# a failed parse. NeedHelp is `help` / `help <path>` (or `--help`); NoValue
66# is an option whose value never came, Missing a required argument nothing
67# bound. Every error but NeedHelp carries `at`, the command path selected
68# where it failed
69type ParseErr is Data:
70 UnknownFlag{at: List<&2, String>, flag: String}
71 Missing{at: List<&2, String>, name: String}
72 NoValue{at: List<&2, String>, name: String}
73 BadValue{at: List<&2, String>, name: String, value: String}
74 NeedHelp{path: List<&2, String>}
75 Unexpected{at: List<&2, String>, arg: String}
76 Repeated{at: List<&2, String>, name: String}
77
78# how the next token is read
79type Mode is Data:
80 Free{}
81 Need{name: String}
82 Help{}
83 Dead{err: ParseErr}
84
85# the walker: the current command's arguments, the commands above it (the
86# nearest first, one per name of the selected path), its subcommands, the
87# selected path, and the current command's own bindings
88type St is Data:
89 St{
90 mode: Mode,
91 args: List<&2, Arg>,
92 up: List<&2, Level>,
93 subs: List<&2, Sub>,
94 path: List<&2, String>,
95 binds: List<&2, Bind>,
96 pos: List<&2, Arg>,
97 raw: Bool,
98 seen: Bool
99 }
100
101# the page a help walk has reached
102type How is Data:
103 How{
104 bin: String,
105 about: String,
106 version: Maybe<&2, String>,
107 args: List<&2, Arg>,
108 subs: List<&2, Sub>,
109 usage: String
110 }
111
112# a program spec
113def app(
114 +name: String,
115 +about: String,
116 version: Maybe<&2, String>,
117 args: List<&2, Arg>,
118 subs: List<&2, Sub>
119) -> Cli:
120 Cli{name, about, version, args, subs}
121
122# a nested command
123def sub( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
124 +name: String,
125 +about: String,
126 args: List<&2, Arg>,
127 subs: List<&2, Sub>
128) -> Sub:
129 Sub{name, about, args, subs}
130
131# a boolean flag
132def flag( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
133 +name: String,
134 short: Maybe<&2, String>,
135 long: Maybe<&2, String>,
136 +help: String
137) -> Arg:
138 Arg{name, short, long, Flag{}, help, False{}, None{}, []}
139
140# a valued option
141def opt( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
142 +name: String,
143 short: Maybe<&2, String>,
144 long: Maybe<&2, String>,
145 +help: String,
146 required: Bool,
147 default: Maybe<&2, String>,
148 choices: List<&2, String>
149) -> Arg:
150 Arg{name, short, long, Opt{}, help, required, default, choices}
151
152# an option that may be given more than once; `get_all` reads every value
153def many( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
154 +name: String,
155 short: Maybe<&2, String>,
156 long: Maybe<&2, String>,
157 +help: String,
158 required: Bool,
159 default: Maybe<&2, String>,
160 choices: List<&2, String>
161) -> Arg:
162 Arg{name, short, long, Many{}, help, required, default, choices}
163
164# a positional
165def pos( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
166 +name: String,
167 +help: String,
168 required: Bool,
169 default: Maybe<&2, String>,
170 choices: List<&2, String>
171) -> Arg:
172 Arg{name, None{}, None{}, Pos{}, help, required, default, choices}
173
174# a rest positional: every leftover word, as one name
175def rest( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
176 +name: String,
177 +help: String,
178 required: Bool,
179 default: Maybe<&2, String>,
180 choices: List<&2, String>
181) -> Arg:
182 Arg{name, None{}, None{}, Rest{}, help, required, default, choices}
183
184# the larger of two Nats
185def help.max(+aa: Nat, +bb: Nat) -> Nat:
186 Bool.pick(Nat, Nat.is_le(aa, bb), bb, aa)
187
188# n spaces
189def help.spaces(nn: Nat) -> String:
190 match nn:
191 case 0n:
192 ""
193 case 1n+p:
194 " " ++ help.spaces(p)
195
196# s padded on the right to width n
197def help.pad(+ss: String, nn: Nat) -> String:
198 ss ++ help.spaces(Nat.sub(nn, String.length(ss)))
199
200# a Maybe string's text, or the default
201def text.of(mm: Maybe<&2, String>, +dd: String) -> String:
202 Maybe.default(&2, String, mm, dd)
203
204# whether a string is a flag token (`-` alone is not)
205def looks_flag(+ss: String) -> Bool:
206 Bool.and(String.starts_with(ss, "-"), Bool.not(String.eq(ss, "-")))
207
208# chars reversed into a string
209def from.rev(cs: List<&2, Char>) -> String:
210 String.from_list(List.reverse(&2, Char, cs))
211
212# `name` and an optional `=value`, once the next char is known
213def cut_eq.pick(
214 eq: Bool,
215 acc: List<&2, Char>,
216 tt: List<&2, Char>,
217 rest: String & Maybe<&2, String>
218) -> String & Maybe<&2, String>:
219 match eq:
220 case True{}:
221 (from.rev(acc), Some{String.from_list(tt)})
222 case False{}:
223 rest
224
225# one char of a `name=value` cut
226def cut_eq.chr(
227 +cc: Char,
228 tt: List<&2, Char>,
229 acc: List<&2, Char>,
230 rest: String & Maybe<&2, String>
231) -> String & Maybe<&2, String>:
232 cut_eq.pick(Char.is_eq(cc, '='), acc, tt, rest)
233
234# `name` and an optional `=value` from the body after `--`
235def cut_eq.go(
236 cs: List<&2, Char>,
237 +acc: List<&2, Char>
238) -> String & Maybe<&2, String>:
239 match cs:
240 case Nil{}:
241 (from.rev(acc), None{})
242 case Con{+c, +t}:
243 cut_eq.chr(c, t, acc, cut_eq.go(t, c <> acc))
244
245# `name` and an optional `=value`
246def cut_eq(ss: String) -> String & Maybe<&2, String>:
247 cut_eq.go(String.to_list(ss), [])
248
249# the value glued after a short letter: what follows one `=` when the rest
250# starts with it (possibly empty, as `--name=` is), else the rest when there
251# is any
252def short_of.val(eq: Bool, +rest: String) -> Maybe<&2, String>:
253 match eq:
254 case True{}:
255 Some{String.drop(rest, 1n)}
256 case False{}:
257 Bool.pick(Maybe<&2, String>, String.is_empty(rest), None{}, Some{rest})
258
259# whether this Arg's long spelling is `name`
260def by_long.hit(aa: Arg, +name: String) -> Bool:
261 Arg{_n, _s, long, _k, _h, _r, _d, _c} = aa
262 String.eq(text.of(long, ""), name)
263
264# this Arg when the long spelling matches, else the rest
265def by_long.at(hit: Bool, hh: Arg, rest: Maybe<&2, Arg>) -> Maybe<&2, Arg>:
266 match hit:
267 case True{}:
268 Some{hh}
269 case False{}:
270 rest
271
272# the Arg with this long spelling, if any
273def by_long(args: List<&2, Arg>, +name: String) -> Maybe<&2, Arg>:
274 match args:
275 case Nil{}:
276 None{}
277 case Con{+h, t}:
278 by_long.at(by_long.hit(h, name), h, by_long(t, name))
279
280# whether this Arg's short spelling is `name`
281def by_short.hit(aa: Arg, +name: String) -> Bool:
282 Arg{_n, short, _l, _k, _h, _r, _d, _c} = aa
283 String.eq(text.of(short, ""), name)
284
285# this Arg when the short spelling matches, else the rest
286def by_short.at(hit: Bool, hh: Arg, rest: Maybe<&2, Arg>) -> Maybe<&2, Arg>:
287 match hit:
288 case True{}:
289 Some{hh}
290 case False{}:
291 rest
292
293# the Arg with this short spelling, if any
294def by_short(args: List<&2, Arg>, +name: String) -> Maybe<&2, Arg>:
295 match args:
296 case Nil{}:
297 None{}
298 case Con{+h, t}:
299 by_short.at(by_short.hit(h, name), h, by_short(t, name))
300
301# whether this Arg's name is `name`
302def by_name.hit(aa: Arg, +name: String) -> Bool:
303 Arg{+n, _s, _l, _k, _h, _r, _d, _c} = aa
304 String.eq(n, name)
305
306# this Arg when the binding name matches, else the rest
307def by_name.at(hit: Bool, hh: Arg, rest: Maybe<&2, Arg>) -> Maybe<&2, Arg>:
308 match hit:
309 case True{}:
310 Some{hh}
311 case False{}:
312 rest
313
314# the Arg with this binding name, if any
315def by_name(args: List<&2, Arg>, +name: String) -> Maybe<&2, Arg>:
316 match args:
317 case Nil{}:
318 None{}
319 case Con{+h, t}:
320 by_name.at(by_name.hit(h, name), h, by_name(t, name))
321
322# this Sub when its name matches, else the rest
323def find_sub.at(ss: Sub, rest: Maybe<&2, Sub>, +name: String) -> Maybe<&2, Sub>:
324 Sub{+n, _a, _g, _c} = ss
325 Bool.pick(Maybe<&2, Sub>, String.eq(n, name), Some{ss}, rest)
326
327# the Sub with this name, if any
328def find_sub(subs: List<&2, Sub>, +name: String) -> Maybe<&2, Sub>:
329 match subs:
330 case Nil{}:
331 None{}
332 case Con{+h, t}:
333 find_sub.at(h, find_sub(t, name), name)
334
335# whether this Arg is a positional
336def pos.is.kind(kk: ArgKind) -> Bool:
337 match kk:
338 case Pos{}:
339 True{}
340 case Rest{}:
341 True{}
342 case Flag{}:
343 False{}
344 case Opt{}:
345 False{}
346 case Many{}:
347 False{}
348
349# whether this ArgKind is rest
350def rest.is.kind(kk: ArgKind) -> Bool:
351 match kk:
352 case Rest{}:
353 True{}
354 case Pos{}:
355 False{}
356 case Flag{}:
357 False{}
358 case Opt{}:
359 False{}
360 case Many{}:
361 False{}
362
363# whether this Arg is a positional
364def pos.is(aa: Arg) -> Bool:
365 Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa
366 pos.is.kind(kind)
367
368# whether this Arg is a rest positional
369def rest.is(aa: Arg) -> Bool:
370 Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa
371 rest.is.kind(kind)
372
373# this Arg kept when it is a positional
374def pos_of.put(keep: Bool, hh: Arg, rest: List<&2, Arg>) -> List<&2, Arg>:
375 match keep:
376 case True{}:
377 hh <> rest
378 case False{}:
379 rest
380
381# the positionals of a spec, in order
382def pos_of(args: List<&2, Arg>) -> List<&2, Arg>:
383 match args:
384 case Nil{}:
385 Nil{}
386 case Con{+h, t}:
387 pos_of.put(pos.is(h), h, pos_of(t))
388
389# whether `v` is among the choices; an empty list accepts any value
390def allowed.ok.at(hit: Bool, rest: Bool) -> Bool:
391 match hit:
392 case True{}:
393 True{}
394 case False{}:
395 rest
396
397# whether `v` is listed; the empty list answers no
398def allowed.ok(cs: List<&2, String>, +vv: String) -> Bool:
399 match cs:
400 case Nil{}:
401 False{}
402 case Con{+h, t}:
403 allowed.ok.at(String.eq(h, vv), allowed.ok(t, vv))
404
405# empty choices accept any value; otherwise `v` must be listed
406def allowed(+cs: List<&2, String>, +vv: String) -> Bool:
407 Bool.or(List.is_empty(&2, String, cs), allowed.ok(cs, vv))
408
409# whether this bind's name is `name`
410def bound.hit(bb: Bind, +name: String) -> Bool:
411 Bind{+n, _v} = bb
412 String.eq(n, name)
413
414# True when the head matches, else the rest
415def bound.at(hit: Bool, rest: Bool) -> Bool:
416 match hit:
417 case True{}:
418 True{}
419 case False{}:
420 rest
421
422# whether `name` is already bound
423def bound(bs: List<&2, Bind>, +name: String) -> Bool:
424 match bs:
425 case Nil{}:
426 False{}
427 case Con{+h, t}:
428 bound.at(bound.hit(h, name), bound(t, name))
429
430# this bind's value when the name matches, else the rest
431def get_all.bind.hit(
432 hit: Bool,
433 +vv: String,
434 more: List<&2, String>
435) -> List<&2, String>:
436 match hit:
437 case True{}:
438 vv <> more
439 case False{}:
440 more
441
442# this bind's value when the name matches, else the rest
443def get_all.bind.at(
444 bb: Bind,
445 more: List<&2, String>,
446 +name: String
447) -> List<&2, String>:
448 Bind{+n, v} = bb
449 get_all.bind.hit(String.eq(n, name), v, more)
450
451# every value bound to `name`
452def get_all.bind(bs: List<&2, Bind>, +name: String) -> List<&2, String>:
453 match bs:
454 case Nil{}:
455 []
456 case Con{+h, t}:
457 get_all.bind.at(h, get_all.bind(t, name), name)
458
459# the bindings this command made, in binding order
460def own(mm: Matched) -> List<&2, Bind>:
461 match mm:
462 case Leaf{bs}:
463 bs
464 case Node{bs, _n, _s}:
465 bs
466
467# every value this command bound to `name`
468def get_all(mm: Matched, +name: String) -> List<&2, String>:
469 get_all.bind(own(mm), name)
470
471# the last of a list of values, or `dflt` when it is empty
472def get.last(vs: List<&2, String>, dflt: Maybe<&2, String>) -> Maybe<&2, String>:
473 match vs:
474 case Nil{}:
475 dflt
476 case Con{hh, tt}:
477 get.last(tt, Some{hh})
478
479# the value bound to `name` last, or None when nothing bound it
480def get(mm: Matched, +name: String) -> Maybe<&2, String>:
481 get.last(get_all(mm, name), None{})
482
483# whether a value is `true`
484def on.of(mv: Maybe<&2, String>) -> Bool:
485 match mv:
486 case None{}:
487 False{}
488 case Some{vv}:
489 String.eq(vv, "true")
490
491# whether a flag named `name` was set
492def on(mm: Matched, +name: String) -> Bool:
493 on.of(get(mm, name))
494
495# the names of the subcommands selected below this command, in order
496def path_of(mm: Matched) -> List<&2, String>:
497 match mm:
498 case Leaf{_b}:
499 []
500 case Node{_b, +name, next}:
501 name <> path_of(next)
502
503# the name of the subcommand selected under this command
504def sub_name(mm: Matched) -> Maybe<&2, String>:
505 match mm:
506 case Leaf{_b}:
507 None{}
508 case Node{_b, name, _s}:
509 Some{name}
510
511# a subcommand's Matched when it is the one asked for
512def sub_of.pick(hit: Bool, next: Matched) -> Maybe<&2, Matched>:
513 match hit:
514 case True{}:
515 Some{next}
516 case False{}:
517 None{}
518
519# the Matched of subcommand `name`, when it is the one selected
520def sub_of(mm: Matched, +name: String) -> Maybe<&2, Matched>:
521 match mm:
522 case Leaf{_b}:
523 None{}
524 case Node{_b, nn, next}:
525 sub_of.pick(String.eq(nn, name), next)
526
527# one name further down, when there is still a Matched to go down from
528def at.step(cur: Maybe<&2, Matched>, +name: String) -> Maybe<&2, Matched>:
529 match cur:
530 case None{}:
531 None{}
532 case Some{mm}:
533 sub_of(mm, name)
534
535# the rest of a path, followed from where the walk down stands
536def at.go(path: List<&2, String>, cur: Maybe<&2, Matched>) -> Maybe<&2, Matched>:
537 match path:
538 case Nil{}:
539 cur
540 case Con{+hh, tt}:
541 at.go(tt, at.step(cur, hh))
542
543# the Matched of the command `path` names below this one, name by name, or
544# None where a name is not the subcommand selected
545def at(mm: Matched, path: List<&2, String>) -> Maybe<&2, Matched>:
546 at.go(path, Some{mm})
547
548# the starting walker for a Cli
549def parse.start(app: Cli) -> St:
550 Cli{_n, _a, _v, +args, subs} = app
551 St{Free{}, args, [], subs, [], [], pos_of(args), False{}, False{}}
552
553# a failed walker that keeps the rest of the state
554def parse.dead(
555 ee: ParseErr,
556 +args: List<&2, Arg>,
557 +up: List<&2, Level>,
558 +subs: List<&2, Sub>,
559 +path: List<&2, String>,
560 +binds: List<&2, Bind>,
561 pos: List<&2, Arg>,
562 +raw: Bool,
563 +seen: Bool
564) -> St:
565 St{Dead{ee}, args, up, subs, path, binds, pos, raw, seen}
566
567# a successful bind of `name` to `val`
568def parse.put(
569 +name: String,
570 +val: String,
571 +args: List<&2, Arg>,
572 +up: List<&2, Level>,
573 +subs: List<&2, Sub>,
574 +path: List<&2, String>,
575 +binds: List<&2, Bind>,
576 +pos: List<&2, Arg>,
577 +raw: Bool,
578 +seen: Bool
579) -> St:
580 St{Free{}, args, up, subs, path, Bind{name, val} <> binds, pos, raw, seen}
581
582# the choices of a found Arg, or none
583def parse.choices(mm: Maybe<&2, Arg>) -> List<&2, String>:
584 match mm:
585 case None{}:
586 []
587 case Some{a}:
588 Arg{_n, _s, _l, _k, _h, _r, _d, cs} = a
589 cs
590
591# bind `name` to `val` when the value is allowed
592def parse.take_choice(
593 ok: Bool,
594 +name: String,
595 +val: String,
596 +args: List<&2, Arg>,
597 +up: List<&2, Level>,
598 +subs: List<&2, Sub>,
599 +path: List<&2, String>,
600 +binds: List<&2, Bind>,
601 +pos: List<&2, Arg>,
602 +raw: Bool,
603 +seen: Bool
604) -> St:
605 match ok:
606 case True{}:
607 parse.put(name, val, args, up, subs, path, binds, pos, raw, seen)
608 case False{}:
609 parse.dead(BadValue{path, name, val}, args, up, subs, path, binds, pos, raw, seen)
610
611# bind an option that already has its value, checked against the choices of
612# the current command's argument of that name: a parent's argument of the same
613# name has its own
614def parse.take_opt_val(
615 +name: String,
616 +val: String,
617 +args: List<&2, Arg>,
618 +up: List<&2, Level>,
619 +subs: List<&2, Sub>,
620 +path: List<&2, String>,
621 +binds: List<&2, Bind>,
622 +pos: List<&2, Arg>,
623 +raw: Bool,
624 +seen: Bool
625) -> St:
626 parse.take_choice(allowed(parse.choices(by_name(args, name)), val), name, val,
627 args, up, subs, path, binds, pos, raw, seen)
628
629# a flag with no glued value is set; a glued value is unexpected
630def parse.take_flag(
631 val: Maybe<&2, String>,
632 +name: String,
633 +orig: String,
634 +args: List<&2, Arg>,
635 +up: List<&2, Level>,
636 +subs: List<&2, Sub>,
637 +path: List<&2, String>,
638 +binds: List<&2, Bind>,
639 pos: List<&2, Arg>,
640 +raw: Bool,
641 +seen: Bool
642) -> St:
643 match val:
644 case None{}:
645 parse.put(name, "true", args, up, subs, path, binds, pos, raw, seen)
646 case Some{_v}:
647 parse.dead(Unexpected{path, orig}, args, up, subs, path, binds, pos, raw, seen)
648
649# an option: glued value now, or the next token
650def parse.take_opt(
651 val: Maybe<&2, String>,
652 +name: String,
653 +args: List<&2, Arg>,
654 +up: List<&2, Level>,
655 +subs: List<&2, Sub>,
656 +path: List<&2, String>,
657 +binds: List<&2, Bind>,
658 +pos: List<&2, Arg>,
659 +raw: Bool,
660 +seen: Bool
661) -> St:
662 match val:
663 case None{}:
664 St{Need{name}, args, up, subs, path, binds, pos, raw, seen}
665 case Some{v}:
666 parse.take_opt_val(name, v, args, up, subs, path, binds, pos, raw, seen)
667
668# a flag or single-valued option given again is refused, whatever the walk
669# would have done with it; the walker keeps the rest of its state
670def parse.once(again: Bool, st: St, +name: String, +path: List<&2, String>) -> St:
671 match again:
672 case True{}:
673 St{_m, +args, +up, +subs, _p, +binds, +pos, +raw, +seen} = st
674 St{Dead{Repeated{path, name}}, args, up, subs, path, binds, pos, raw, seen}
675 case False{}:
676 st
677
678# dispatch on the found Arg's kind
679def parse.take_arg.kind(
680 kk: ArgKind,
681 +name: String,
682 val: Maybe<&2, String>,
683 +orig: String,
684 +args: List<&2, Arg>,
685 +up: List<&2, Level>,
686 +subs: List<&2, Sub>,
687 +path: List<&2, String>,
688 +binds: List<&2, Bind>,
689 +pos: List<&2, Arg>,
690 +raw: Bool,
691 +seen: Bool
692) -> St:
693 match kk:
694 case Flag{}:
695 parse.once(bound(binds, name), parse.take_flag(val, name, orig, args, up, subs, path,
696 binds, pos, raw, seen), name, path)
697 case Opt{}:
698 parse.once(bound(binds, name), parse.take_opt(val, name, args, up, subs, path, binds, pos,
699 raw, seen), name, path)
700 case Many{}:
701 parse.take_opt(val, name, args, up, subs, path, binds, pos, raw, seen)
702 case Pos{}:
703 parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)
704 case Rest{}:
705 parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)
706
707# a found Arg, or an unknown flag
708def parse.take_arg(
709 found: Maybe<&2, Arg>,
710 val: Maybe<&2, String>,
711 +orig: String,
712 +args: List<&2, Arg>,
713 +up: List<&2, Level>,
714 +subs: List<&2, Sub>,
715 +path: List<&2, String>,
716 +binds: List<&2, Bind>,
717 pos: List<&2, Arg>,
718 +raw: Bool,
719 +seen: Bool
720) -> St:
721 match found:
722 case None{}:
723 parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)
724 case Some{a}:
725 Arg{+name, _s, _l, kind, _h, _r, _d, _c} = a
726 parse.take_arg.kind(kind, name, val, orig, args, up, subs, path, binds, pos,
727 raw, seen)
728
729# whether an unknown long option is a request for help: a bare `--help`,
730# with no `=` value, before any positional of the current command is bound
731def long_help(+name: String, val: Maybe<&2, String>, seen: Bool) -> Bool:
732 match val:
733 case None{}:
734 Bool.and(String.eq(name, "help"), Bool.not(seen))
735 case Some{_v}:
736 False{}
737
738# an unknown long option: a request for help (`--help`, as `help`), or an
739# unknown flag
740def parse.take_long.help(
741 hit: Bool,
742 +orig: String,
743 +args: List<&2, Arg>,
744 +up: List<&2, Level>,
745 +subs: List<&2, Sub>,
746 +path: List<&2, String>,
747 +binds: List<&2, Bind>,
748 +pos: List<&2, Arg>,
749 +raw: Bool,
750 +seen: Bool
751) -> St:
752 match hit:
753 case True{}:
754 St{Help{}, args, up, subs, path, binds, pos, raw, seen}
755 case False{}:
756 parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)
757
758# a long option the current command spells, or an unknown one
759def parse.take_long.found(
760 found: Maybe<&2, Arg>,
761 +name: String,
762 val: Maybe<&2, String>,
763 +orig: String,
764 +args: List<&2, Arg>,
765 +up: List<&2, Level>,
766 +subs: List<&2, Sub>,
767 +path: List<&2, String>,
768 +binds: List<&2, Bind>,
769 +pos: List<&2, Arg>,
770 +raw: Bool,
771 +seen: Bool
772) -> St:
773 match found:
774 case None{}:
775 parse.take_long.help(long_help(name, val, seen), orig, args, up, subs, path,
776 binds, pos, raw, seen)
777 case Some{a}:
778 parse.take_arg(Some{a}, val, orig, args, up, subs, path, binds, pos, raw, seen)
779
780# a long option with a name, or `--=...`
781def parse.take_long.named(
782 empty: Bool,
783 +name: String,
784 val: Maybe<&2, String>,
785 +orig: String,
786 +args: List<&2, Arg>,
787 +up: List<&2, Level>,
788 +subs: List<&2, Sub>,
789 +path: List<&2, String>,
790 +binds: List<&2, Bind>,
791 +pos: List<&2, Arg>,
792 +raw: Bool,
793 +seen: Bool
794) -> St:
795 match empty:
796 case True{}:
797 parse.dead(UnknownFlag{path, orig}, args, up, subs, path, binds, pos, raw, seen)
798 case False{}:
799 parse.take_long.found(by_long(args, name), name, val, orig, args, up, subs,
800 path, binds, pos, raw, seen)
801
802# a long option after the `=` cut
803def parse.take_long.cut(
804 nv: String & Maybe<&2, String>,
805 +orig: String,
806 +args: List<&2, Arg>,
807 +up: List<&2, Level>,
808 +subs: List<&2, Sub>,
809 +path: List<&2, String>,
810 +binds: List<&2, Bind>,
811 pos: List<&2, Arg>,
812 +raw: Bool,
813 +seen: Bool
814) -> St:
815 (+name, val) = nv
816 parse.take_long.named(String.is_empty(name), name, val, orig, args, up, subs,
817 path, binds, pos, raw, seen)
818
819# a long option body after `--`
820def parse.take_long(
821 body: String,
822 +orig: String,
823 +args: List<&2, Arg>,
824 +up: List<&2, Level>,
825 +subs: List<&2, Sub>,
826 +path: List<&2, String>,
827 +binds: List<&2, Bind>,
828 +pos: List<&2, Arg>,
829 +raw: Bool,
830 +seen: Bool
831) -> St:
832 parse.take_long.cut(cut_eq(body), orig, args, up, subs, path, binds, pos, raw,
833 seen)
834
835# a flag's value in a cluster: the rest when it starts with `=` (which
836# `take_arg` refuses), else none, so the next letter goes on
837def cluster.val.flag(eq: Bool, +rest: String) -> Maybe<&2, String>:
838 match eq:
839 case True{}:
840 Some{rest}
841 case False{}:
842 None{}
843
844# the value a letter of this kind gives its argument: an option takes the
845# rest of the word, after one `=`
846def cluster.val.kind(kk: ArgKind, +rest: String) -> Maybe<&2, String>:
847 match kk:
848 case Flag{}:
849 cluster.val.flag(String.starts_with(rest, "="), rest)
850 case Opt{}:
851 short_of.val(String.starts_with(rest, "="), rest)
852 case Many{}:
853 short_of.val(String.starts_with(rest, "="), rest)
854 case Pos{}:
855 None{}
856 case Rest{}:
857 None{}
858
859# the value the letter's argument gets, or none when no argument has it
860def cluster.val(found: Maybe<&2, Arg>, +rest: String) -> Maybe<&2, String>:
861 match found:
862 case None{}:
863 None{}
864 case Some{aa}:
865 Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa
866 cluster.val.kind(kind, rest)
867
868# whether a letter of this kind ends the cluster: every kind but a flag that
869# is not followed by `=`
870def cluster.ends.kind(kk: ArgKind, +rest: String) -> Bool:
871 match kk:
872 case Flag{}:
873 String.starts_with(rest, "=")
874 case Opt{}:
875 True{}
876 case Many{}:
877 True{}
878 case Pos{}:
879 True{}
880 case Rest{}:
881 True{}
882
883# whether the letters after this one are no longer read as letters: an
884# unknown letter, an option (they are its value) or a flag given `=`
885def cluster.ends(found: Maybe<&2, Arg>, +rest: String) -> Bool:
886 match found:
887 case None{}:
888 True{}
889 case Some{aa}:
890 Arg{_n, _s, _l, kind, _h, _r, _d, _c} = aa
891 cluster.ends.kind(kind, rest)
892
893# one letter against a walker an earlier letter failed changes nothing;
894# otherwise it goes through `take_arg`
895def cluster.apply.mode(
896 mode: Mode,
897 +cc: Char,
898 +rest: String,
899 +orig: String,
900 +args: List<&2, Arg>,
901 +up: List<&2, Level>,
902 +subs: List<&2, Sub>,
903 +path: List<&2, String>,
904 +binds: List<&2, Bind>,
905 +pos: List<&2, Arg>,
906 +raw: Bool,
907 +seen: Bool
908) -> St:
909 match mode:
910 case Dead{e}:
911 St{Dead{e}, args, up, subs, path, binds, pos, raw, seen}
912 case _other:
913 +found = by_short(args, SCon{cc, SNil{}})
914 parse.take_arg(found, cluster.val(found, rest), orig, args, up, subs, path, binds, pos,
915 raw, seen)
916
917# one letter against the walker
918def cluster.apply(st: St, +cc: Char, +rest: String, +orig: String) -> St:
919 St{mode, +args, +up, +subs, +path, +binds, +pos, +raw, +seen} = st
920 cluster.apply.mode(mode, cc, rest, orig, args, up, subs, path, binds, pos, raw, seen)
921
922# one letter of a cluster, unless an earlier one ended it
923def cluster.letter(done: Bool, +cc: Char, +rest: String, +orig: String, st: St) -> St:
924 match done:
925 case True{}:
926 st
927 case False{}:
928 cluster.apply(st, cc, rest, orig)
929
930# whether the cluster has ended after this letter
931def cluster.done(done: Bool, +cc: Char, +rest: String, st: St) -> Bool:
932 match done:
933 case True{}:
934 True{}
935 case False{}:
936 St{_m, +args, _g, _s, _p, _b, _o, _r, _n} = st
937 cluster.ends(by_short(args, SCon{cc, SNil{}}), rest)
938
939# every letter of a short cluster `-abc`, in order: a flag binds and the next
940# letter goes on; an option takes the rest of the word, after one `=`, or the
941# next word when nothing is left; an unknown letter, or a flag given `=`,
942# refuses the whole word
943def cluster(body: String, +orig: String, +st: St, +done: Bool) -> St:
944 match body:
945 case SNil{}:
946 st
947 case SCon{+cc, +rest}:
948 cluster(rest, orig, cluster.letter(done, cc, rest, orig, st), cluster.done(done, cc, rest, st))
949
950# a short option body after `-`
951def parse.take_short(
952 body: String,
953 +orig: String,
954 +args: List<&2, Arg>,
955 +up: List<&2, Level>,
956 +subs: List<&2, Sub>,
957 +path: List<&2, String>,
958 +binds: List<&2, Bind>,
959 +pos: List<&2, Arg>,
960 +raw: Bool,
961 +seen: Bool
962) -> St:
963 cluster(body, orig, St{Free{}, args, up, subs, path, binds, pos, raw, seen}, False{})
964
965# a rest positional stays; a plain one is consumed
966def parse.take_pos.keep(keep: Bool, hh: Arg, tt: List<&2, Arg>) -> List<&2, Arg>:
967 match keep:
968 case True{}:
969 hh <> tt
970 case False{}:
971 tt
972
973# the next remaining positional, or an unexpected word
974def parse.take_pos(
975 +tok: String,
976 +args: List<&2, Arg>,
977 +up: List<&2, Level>,
978 subs: List<&2, Sub>,
979 +path: List<&2, String>,
980 +binds: List<&2, Bind>,
981 pos: List<&2, Arg>,
982 raw: Bool
983) -> St:
984 match pos:
985 case Nil{}:
986 parse.dead(Unexpected{path, tok}, args, up, subs, path, binds, pos, raw, True{})
987 case Con{+h, t}:
988 Arg{+name, _s, _l, _k, _h, _r, _d, +cs} = h
989 parse.take_choice(allowed(cs, tok), name, tok, args, up, subs, path, binds,
990 parse.take_pos.keep(rest.is(h), h, t), raw, True{})
991
992# this Sub's arguments become current, with no bindings of its own; the
993# command it leaves keeps its arguments and bindings above it
994def parse.enter(
995 ss: Sub,
996 +args: List<&2, Arg>,
997 +up: List<&2, Level>,
998 +path: List<&2, String>,
999 binds: List<&2, Bind>
1000) -> St:
1001 Sub{+name, _a, +next, next_subs} = ss
1002 St{Free{}, next, Level{args, binds} <> up, next_subs,
1003 List.append(&2, String, path, [name]), [], pos_of(next), False{},
1004 False{}}
1005
1006# required and no default → Some{name}
1007def parse.req.go(req: Bool, bare: Bool, +nn: String) -> Maybe<&2, String>:
1008 Bool.pick(Maybe<&2, String>, Bool.and(req, bare), Some{nn}, None{})
1009
1010# a required Arg without a default, as a missing name
1011def parse.req(aa: Arg) -> Maybe<&2, String>:
1012 Arg{+n, _s, _l, _k, _h, +req, default, _c} = aa
1013 parse.req.go(req, Maybe.is_none(&2, String, default), n)
1014
1015# this missing name, else the rest
1016def parse.first_req.at(
1017 here: Maybe<&2, String>,
1018 rest: Maybe<&2, String>
1019) -> Maybe<&2, String>:
1020 match here:
1021 case Some{n}:
1022 Some{n}
1023 case None{}:
1024 rest
1025
1026# a rest stays in the list, so it does not block a subcommand
1027def parse.first_req.skip(skip: Bool, aa: Arg) -> Maybe<&2, String>:
1028 match skip:
1029 case True{}:
1030 None{}
1031 case False{}:
1032 parse.req(aa)
1033
1034# the first leftover required positional
1035def parse.first_req(pos: List<&2, Arg>) -> Maybe<&2, String>:
1036 match pos:
1037 case Nil{}:
1038 None{}
1039 case Con{+h, t}:
1040 parse.first_req.at(parse.first_req.skip(rest.is(h), h), parse.first_req(t))
1041
1042# a leftover required positional blocks entering a subcommand
1043def parse.enter_or(
1044 miss: Maybe<&2, String>,
1045 ss: Sub,
1046 +args: List<&2, Arg>,
1047 +up: List<&2, Level>,
1048 +subs: List<&2, Sub>,
1049 +path: List<&2, String>,
1050 +binds: List<&2, Bind>,
1051 +pos: List<&2, Arg>,
1052 +raw: Bool,
1053 +seen: Bool
1054) -> St:
1055 match miss:
1056 case None{}:
1057 parse.enter(ss, args, up, path, binds)
1058 case Some{n}:
1059 parse.dead(Missing{path, n}, args, up, subs, path, binds, pos, raw, seen)
1060
1061# a found subcommand, or a positional
1062def parse.word.go(
1063 found: Maybe<&2, Sub>,
1064 +tok: String,
1065 +args: List<&2, Arg>,
1066 +up: List<&2, Level>,
1067 +subs: List<&2, Sub>,
1068 +path: List<&2, String>,
1069 +binds: List<&2, Bind>,
1070 +pos: List<&2, Arg>,
1071 +raw: Bool,
1072 +seen: Bool
1073) -> St:
1074 match found:
1075 case None{}:
1076 parse.take_pos(tok, args, up, subs, path, binds, pos, raw)
1077 case Some{s}:
1078 parse.enter_or(parse.first_req(pos), s, args, up, subs, path, binds, pos, raw,
1079 seen)
1080
1081# a word: a subcommand, or a positional
1082def parse.word(
1083 +tok: String,
1084 +args: List<&2, Arg>,
1085 +up: List<&2, Level>,
1086 +subs: List<&2, Sub>,
1087 +path: List<&2, String>,
1088 +binds: List<&2, Bind>,
1089 +pos: List<&2, Arg>,
1090 +raw: Bool,
1091 +seen: Bool
1092) -> St:
1093 parse.word.go(find_sub(subs, tok), tok, args, up, subs, path, binds, pos,
1094 raw, seen)
1095
1096# `-` as a word, or a short option
1097def parse.step.short.go(
1098 alone: Bool,
1099 +tok: String,
1100 +args: List<&2, Arg>,
1101 +up: List<&2, Level>,
1102 +subs: List<&2, Sub>,
1103 +path: List<&2, String>,
1104 +binds: List<&2, Bind>,
1105 +pos: List<&2, Arg>,
1106 +seen: Bool
1107) -> St:
1108 match alone:
1109 case True{}:
1110 parse.word(tok, args, up, subs, path, binds, pos, False{}, seen)
1111 case False{}:
1112 parse.take_short(String.drop(tok, 1n), tok, args, up, subs, path, binds, pos,
1113 False{}, seen)
1114
1115# `-` alone is a positional; otherwise a short option
1116def parse.step.short(
1117 dash: Bool,
1118 +tok: String,
1119 +args: List<&2, Arg>,
1120 +up: List<&2, Level>,
1121 +subs: List<&2, Sub>,
1122 +path: List<&2, String>,
1123 +binds: List<&2, Bind>,
1124 +pos: List<&2, Arg>,
1125 +seen: Bool
1126) -> St:
1127 match dash:
1128 case True{}:
1129 parse.step.short.go(String.is_empty(String.drop(tok, 1n)), tok, args, up, subs, path, binds,
1130 pos, seen)
1131 case False{}:
1132 parse.word(tok, args, up, subs, path, binds, pos, False{}, seen)
1133
1134# `--long` first, then `-s`, then a word
1135def parse.step.kind.go(
1136 long: Bool,
1137 dash: Bool,
1138 +tok: String,
1139 +args: List<&2, Arg>,
1140 +up: List<&2, Level>,
1141 +subs: List<&2, Sub>,
1142 +path: List<&2, String>,
1143 +binds: List<&2, Bind>,
1144 +pos: List<&2, Arg>,
1145 +seen: Bool
1146) -> St:
1147 match long:
1148 case True{}:
1149 parse.take_long(String.drop(tok, 2n), tok, args, up, subs, path, binds, pos,
1150 False{}, seen)
1151 case False{}:
1152 parse.step.short(dash, tok, args, up, subs, path, binds, pos, seen)
1153
1154# a long option, a short option, or a word
1155def parse.step.kind(
1156 +tok: String,
1157 +args: List<&2, Arg>,
1158 +up: List<&2, Level>,
1159 subs: List<&2, Sub>,
1160 +path: List<&2, String>,
1161 +binds: List<&2, Bind>,
1162 +pos: List<&2, Arg>,
1163 +seen: Bool
1164) -> St:
1165 parse.step.kind.go(String.starts_with(tok, "--"), String.starts_with(tok, "-"), tok,
1166 args, up, subs, path, binds, pos, seen)
1167
1168# `help` in command position collects the rest of argv as a path
1169def parse.step.help(
1170 hit: Bool,
1171 +tok: String,
1172 +args: List<&2, Arg>,
1173 +up: List<&2, Level>,
1174 +subs: List<&2, Sub>,
1175 +path: List<&2, String>,
1176 +binds: List<&2, Bind>,
1177 +pos: List<&2, Arg>,
1178 +seen: Bool
1179) -> St:
1180 match hit:
1181 case True{}:
1182 St{Help{}, args, up, subs, path, binds, pos, False{}, seen}
1183 case False{}:
1184 parse.step.kind(tok, args, up, subs, path, binds, pos, seen)
1185
1186# `--` ends option parsing
1187def parse.step.end(
1188 end: Bool,
1189 +tok: String,
1190 +args: List<&2, Arg>,
1191 +up: List<&2, Level>,
1192 +subs: List<&2, Sub>,
1193 +path: List<&2, String>,
1194 +binds: List<&2, Bind>,
1195 +pos: List<&2, Arg>,
1196 +seen: Bool
1197) -> St:
1198 match end:
1199 case True{}:
1200 St{Free{}, args, up, subs, path, binds, pos, True{}, seen}
1201 case False{}:
1202 parse.step.help(Bool.and(String.eq(tok, "help"), Bool.not(seen)), tok, args,
1203 up, subs, path, binds, pos, seen)
1204
1205# after `--`, every token is a positional
1206def parse.step.raw(
1207 raw: Bool,
1208 +tok: String,
1209 +args: List<&2, Arg>,
1210 +up: List<&2, Level>,
1211 +subs: List<&2, Sub>,
1212 +path: List<&2, String>,
1213 +binds: List<&2, Bind>,
1214 +pos: List<&2, Arg>,
1215 +seen: Bool
1216) -> St:
1217 match raw:
1218 case True{}:
1219 parse.take_pos(tok, args, up, subs, path, binds, pos, True{})
1220 case False{}:
1221 parse.step.end(String.eq(tok, "--"), tok, args, up, subs, path, binds, pos,
1222 seen)
1223
1224# a pending option value: a flag-shaped token is a missing value
1225def parse.step.need(
1226 flag: Bool,
1227 +name: String,
1228 +tok: String,
1229 +args: List<&2, Arg>,
1230 +up: List<&2, Level>,
1231 +subs: List<&2, Sub>,
1232 +path: List<&2, String>,
1233 +binds: List<&2, Bind>,
1234 +pos: List<&2, Arg>,
1235 +raw: Bool,
1236 +seen: Bool
1237) -> St:
1238 match flag:
1239 case True{}:
1240 parse.dead(NoValue{path, name}, args, up, subs, path, binds, pos, raw, seen)
1241 case False{}:
1242 parse.take_opt_val(name, tok, args, up, subs, path, binds, pos, raw, seen)
1243
1244# a word after `help`: the subcommand it names, whose own subcommands the
1245# next word is looked up in, or an unexpected word
1246def parse.step.help_path(
1247 found: Maybe<&2, Sub>,
1248 +tok: String,
1249 +args: List<&2, Arg>,
1250 +up: List<&2, Level>,
1251 +subs: List<&2, Sub>,
1252 +path: List<&2, String>,
1253 +binds: List<&2, Bind>,
1254 +pos: List<&2, Arg>,
1255 +raw: Bool,
1256 +seen: Bool
1257) -> St:
1258 match found:
1259 case None{}:
1260 parse.dead(Unexpected{path, tok}, args, up, subs, path, binds, pos, raw, seen)
1261 case Some{ss}:
1262 Sub{_n, _a, _g, next_subs} = ss
1263 St{Help{}, args, up, next_subs, List.append(&2, String, path, [tok]), binds,
1264 pos, raw, seen}
1265
1266# one token against the walker
1267def parse.step.mode(
1268 mode: Mode,
1269 +tok: String,
1270 +args: List<&2, Arg>,
1271 +up: List<&2, Level>,
1272 +subs: List<&2, Sub>,
1273 +path: List<&2, String>,
1274 +binds: List<&2, Bind>,
1275 +pos: List<&2, Arg>,
1276 +raw: Bool,
1277 +seen: Bool
1278) -> St:
1279 match mode:
1280 case Dead{e}:
1281 St{Dead{e}, args, up, subs, path, binds, pos, raw, seen}
1282 case Need{name}:
1283 parse.step.need(looks_flag(tok), name, tok, args, up, subs, path, binds, pos,
1284 raw, seen)
1285 case Help{}:
1286 parse.step.help_path(find_sub(subs, tok), tok, args, up, subs, path, binds, pos, raw,
1287 seen)
1288 case Free{}:
1289 parse.step.raw(raw, tok, args, up, subs, path, binds, pos, seen)
1290
1291# one token
1292def parse.step(+tok: String, st: St) -> St:
1293 St{mode, args, up, subs, path, binds, pending, raw, seen} = st
1294 parse.step.mode(mode, tok, args, up, subs, path, binds, pending, raw, seen)
1295
1296# every token, in order
1297def parse.walk(argv: List<&2, String>, st: St) -> St:
1298 match argv:
1299 case Nil{}:
1300 st
1301 case Con{+h, t}:
1302 parse.walk(t, parse.step(h, st))
1303
1304# a default value kept when the name is not already bound
1305def fill.one(
1306 skip: Bool,
1307 +name: String,
1308 +val: String,
1309 rest: List<&2, Bind>
1310) -> List<&2, Bind>:
1311 match skip:
1312 case True{}:
1313 rest
1314 case False{}:
1315 Bind{name, val} <> rest
1316
1317# a Some default is filled when unbound
1318def fill.arg.go(
1319 dd: Maybe<&2, String>,
1320 +name: String,
1321 +binds: List<&2, Bind>
1322) -> List<&2, Bind>:
1323 match dd:
1324 case None{}:
1325 binds
1326 case Some{v}:
1327 fill.one(bound(binds, name), name, v, binds)
1328
1329# this Arg's default, when it has one and is not bound
1330def fill.arg(aa: Arg, +binds: List<&2, Bind>) -> List<&2, Bind>:
1331 Arg{+n, _s, _l, _k, _h, _r, default, _c} = aa
1332 fill.arg.go(default, n, binds)
1333
1334# defaults of every spec Arg
1335def fill.args(args: List<&2, Arg>, +binds: List<&2, Bind>) -> List<&2, Bind>:
1336 match args:
1337 case Nil{}:
1338 binds
1339 case Con{+h, t}:
1340 fill.arg(h, fill.args(t, binds))
1341
1342# a required name that is not bound
1343def miss.one(
1344 need: Bool,
1345 +name: String,
1346 rest: Maybe<&2, String>
1347) -> Maybe<&2, String>:
1348 match need:
1349 case True{}:
1350 Some{name}
1351 case False{}:
1352 rest
1353
1354# whether this Arg is required and unbound
1355def miss.arg(
1356 aa: Arg,
1357 +binds: List<&2, Bind>,
1358 rest: Maybe<&2, String>
1359) -> Maybe<&2, String>:
1360 Arg{+n, _s, _l, _k, _h, +req, _d, _c} = aa
1361 miss.one(Bool.and(req, Bool.not(bound(binds, n))), n, rest)
1362
1363# the first required name that is not bound
1364def miss.args(args: List<&2, Arg>, +binds: List<&2, Bind>) -> Maybe<&2, String>:
1365 match args:
1366 case Nil{}:
1367 None{}
1368 case Con{+h, t}:
1369 miss.arg(h, binds, miss.args(t, binds))
1370
1371# each command's defaults, filled in among its own bindings
1372def fill.levels(lvs: List<&2, Level>) -> List<&2, Level>:
1373 match lvs:
1374 case Nil{}:
1375 []
1376 case Con{hh, tt}:
1377 Level{+args, binds} = hh
1378 Level{args, fill.args(args, binds)} <> fill.levels(tt)
1379
1380# the first required argument a command left unbound, in list order
1381def miss.levels(lvs: List<&2, Level>) -> Maybe<&2, String>:
1382 match lvs:
1383 case Nil{}:
1384 None{}
1385 case Con{hh, tt}:
1386 Level{+args, +binds} = hh
1387 parse.first_req.at(miss.args(args, binds), miss.levels(tt))
1388
1389# the bindings a command made
1390def level.binds(lv: Level) -> List<&2, Bind>:
1391 Level{_a, binds} = lv
1392 binds
1393
1394# each command's Matched, root first, with the name selected under it
1395def matched.build(lvs: List<&2, Level>, names: List<&2, String>) -> Matched:
1396 match lvs:
1397 case Nil{}:
1398 Leaf{[]}
1399 case Con{hh, tt}:
1400 match names:
1401 case Nil{}:
1402 Leaf{List.reverse(&2, Bind, level.binds(hh))}
1403 case Con{nn, ns}:
1404 Node{List.reverse(&2, Bind, level.binds(hh)), nn, matched.build(tt, ns)}
1405
1406# a missing required name, or the matched result
1407def parse.finish.miss(
1408 mm: Maybe<&2, String>,
1409 path: List<&2, String>,
1410 +lvs: List<&2, Level>
1411) -> Result<&2, &2, ParseErr, Matched>:
1412 match mm:
1413 case Some{n}:
1414 Fail{Missing{path, n}}
1415 case None{}:
1416 Done{matched.build(lvs, path)}
1417
1418# every command's defaults, then its required arguments, root first
1419def parse.finish.ok(+path: List<&2, String>, +lvs: List<&2, Level>) -> Result<&2, &2, ParseErr, Matched>:
1420 parse.finish.miss(miss.levels(lvs), path, lvs)
1421
1422# a finished walker: help, a pending value, an error, or defaults + required
1423def parse.finish.mode(
1424 mode: Mode,
1425 +args: List<&2, Arg>,
1426 +up: List<&2, Level>,
1427 +path: List<&2, String>,
1428 +binds: List<&2, Bind>
1429) -> Result<&2, &2, ParseErr, Matched>:
1430 match mode:
1431 case Dead{e}:
1432 Fail{e}
1433 case Need{name}:
1434 Fail{NoValue{path, name}}
1435 case Help{}:
1436 Fail{NeedHelp{path}}
1437 case Free{}:
1438 parse.finish.ok(path, List.reverse(&2, Level, fill.levels(Level{args, binds} <> up)))
1439
1440# the walker, finished
1441def parse.finish(st: St) -> Result<&2, &2, ParseErr, Matched>:
1442 St{mode, args, up, _s, path, binds, _p, _r, _n} = st
1443 parse.finish.mode(mode, args, up, path, binds)
1444
1445# argv against a Cli
1446def parse(app: Cli, argv: List<&2, String>) -> Result<&2, &2, ParseErr, Matched>:
1447 parse.finish(parse.walk(argv, parse.start(app)))
1448
1449# the short/long label of an option, plus a metavar for Opt
1450def help.label.kind(kk: ArgKind, +name: String, +label: String) -> String:
1451 match kk:
1452 case Flag{}:
1453 label
1454 case Opt{}:
1455 label ++ " <" ++ String.to_upper(name) ++ ">"
1456 case Many{}:
1457 label ++ " <" ++ String.to_upper(name) ++ ">..."
1458 case Pos{}:
1459 label
1460 case Rest{}:
1461 label
1462
1463# `-s, --long` or one of them
1464def help.label.pair(+short: String, +long: String) -> String:
1465 Bool.pick(String, String.is_empty(short),
1466 Bool.pick(String, String.is_empty(long), "", " --" ++ long),
1467 Bool.pick(String, String.is_empty(long), " -" ++ short,
1468 " -" ++ short ++ ", --" ++ long))
1469
1470# the usage label of an option
1471def help.label(aa: Arg) -> String:
1472 Arg{+name, short, long, kind, _h, _r, _d, _c} = aa
1473 help.label.kind(kind, name, help.label.pair(text.of(short, ""), text.of(long, "")))
1474
1475# extras after the help string: default and choices
1476def help.extra.go(+dd: String, +cs: List<&2, String>) -> String:
1477 Bool.pick(String, String.is_empty(dd),
1478 Bool.pick(String, List.is_empty(&2, String, cs), "",
1479 " [possible values: " ++ String.join(cs, ", ") ++ "]"),
1480 Bool.pick(String, List.is_empty(&2, String, cs),
1481 " [default: " ++ dd ++ "]",
1482 " [default: " ++ dd ++ "] [possible values: " ++ String.join(cs, ", ") ++
1483 "]"))
1484
1485# extras after the help string
1486def help.extra(aa: Arg) -> String:
1487 Arg{_n, _s, _l, _k, _h, _r, default, cs} = aa
1488 help.extra.go(text.of(default, ""), cs)
1489
1490# one option line, given the label width
1491def help.opt_line(+aa: Arg, ww: Nat) -> String:
1492 Arg{_n, _s, _l, _k, +h, _r, _d, _c} = aa
1493 help.pad(help.label(aa), ww) ++ " " ++ h ++ help.extra(aa)
1494
1495# the max option-label width
1496def help.opt_width(args: List<&2, Arg>, nn: Nat) -> Nat:
1497 match args:
1498 case Nil{}:
1499 nn
1500 case Con{+h, t}:
1501 help.opt_width(t, help.max(nn, String.length(help.label(h))))
1502
1503# a non-positional option line
1504def help.opt_lines.at(skip: Bool, hh: Arg, rest: String, ww: Nat) -> String:
1505 match skip:
1506 case True{}:
1507 rest
1508 case False{}:
1509 help.opt_line(hh, ww) ++ "\n" ++ rest
1510
1511# option lines, skipping positionals
1512def help.opt_lines(args: List<&2, Arg>, +ww: Nat) -> String:
1513 match args:
1514 case Nil{}:
1515 ""
1516 case Con{+h, t}:
1517 help.opt_lines.at(pos.is(h), h, help.opt_lines(t, ww), ww)
1518
1519# wrap option lines in a heading
1520def help.opts.go(+body: String) -> String:
1521 Bool.pick(String, String.is_empty(body), "", "Options:\n" ++ body ++ "\n")
1522
1523# the Options block, or empty when there are none
1524def help.opts(+args: List<&2, Arg>) -> String:
1525 help.opts.go(help.opt_lines(args, help.opt_width(args, 0n)))
1526
1527# a command's name
1528def help.sub_name(ss: Sub) -> String:
1529 Sub{n, _a, _g, _c} = ss
1530 n
1531
1532# a command's about
1533def help.sub_about(ss: Sub) -> String:
1534 Sub{_n, a, _g, _c} = ss
1535 a
1536
1537# max command-name width, including `help`
1538def help.cmd_width(subs: List<&2, Sub>, nn: Nat) -> Nat:
1539 match subs:
1540 case Nil{}:
1541 help.max(nn, String.length("help"))
1542 case Con{+h, t}:
1543 help.cmd_width(t, help.max(nn, String.length(help.sub_name(h))))
1544
1545# one command line
1546def help.cmd_line(+name: String, +about: String, ww: Nat) -> String:
1547 " " ++ help.pad(name, ww) ++ " " ++ about ++ "\n"
1548
1549# command lines, then `help`
1550def help.cmd_lines(subs: List<&2, Sub>, +ww: Nat) -> String:
1551 match subs:
1552 case Nil{}:
1553 help.cmd_line("help",
1554 "Print this message or the help of the given subcommand(s)", ww)
1555 case Con{+h, t}:
1556 help.cmd_line(help.sub_name(h), help.sub_about(h), ww) ++ help.cmd_lines(t, ww)
1557
1558# the Commands block
1559def help.cmds(+subs: List<&2, Sub>) -> String:
1560 "Commands:\n" ++ help.cmd_lines(subs, help.cmd_width(subs, 0n)) ++ "\n"
1561
1562# a rest mark: `...`, or empty
1563def help.pos.dots(many: Bool) -> String:
1564 Bool.pick(String, many, "...", "")
1565
1566# a positional in a usage line: `<NAME>` or `[NAME]`
1567def help.pos.wrap(req: Bool, +nn: String) -> String:
1568 Bool.pick(String, req, " <" ++ nn ++ ">", " [" ++ nn ++ "]")
1569
1570# a positional in a usage line: `<NAME>` or `[NAME]`, plus `...` when rest
1571def help.pos.one(aa: Arg) -> String:
1572 Arg{+n, _s, _l, kind, _h, +req, _d, _c} = aa
1573 help.pos.wrap(req, String.to_upper(n)) ++ help.pos.dots(rest.is.kind(kind))
1574
1575# this positional's hint, else the rest
1576def help.pos.at(keep: Bool, hh: Arg, rest: String) -> String:
1577 match keep:
1578 case True{}:
1579 help.pos.one(hh) ++ rest
1580 case False{}:
1581 rest
1582
1583# every positional in a usage line
1584def help.pos(args: List<&2, Arg>) -> String:
1585 match args:
1586 case Nil{}:
1587 ""
1588 case Con{+h, t}:
1589 help.pos.at(pos.is(h), h, help.pos(t))
1590
1591# the bracketed options word when the spec has a flag or option
1592def help.opts_hint.go(has: Bool) -> String:
1593 Bool.pick(String, has, " [" ++ String.to_upper("options") ++ "]", "")
1594
1595# True when this Arg is a flag or option, else the rest
1596def help.has_opt.at(is_pos: Bool, rest: Bool) -> Bool:
1597 match is_pos:
1598 case True{}:
1599 rest
1600 case False{}:
1601 True{}
1602
1603# whether any Arg is not a positional
1604def help.has_opt(args: List<&2, Arg>) -> Bool:
1605 match args:
1606 case Nil{}:
1607 False{}
1608 case Con{+h, t}:
1609 help.has_opt.at(pos.is(h), help.has_opt(t))
1610
1611# the options hint when needed
1612def help.opts_hint(args: List<&2, Arg>) -> String:
1613 help.opts_hint.go(help.has_opt(args))
1614
1615# `[COMMAND]` when there are subcommands
1616def help.cmd_hint(+subs: List<&2, Sub>) -> String:
1617 Bool.pick(String, List.is_empty(&2, Sub, subs), "", " [COMMAND]")
1618
1619# `Usage: name ...`
1620def help.usage(
1621 +name: String,
1622 +args: List<&2, Arg>,
1623 +subs: List<&2, Sub>
1624) -> String:
1625 "Usage: " ++ name ++ help.opts_hint(args) ++ help.pos(args) ++ help.cmd_hint(subs)
1626
1627# `name` or `name version`
1628def title.go(+name: String, vv: Maybe<&2, String>) -> String:
1629 match vv:
1630 case None{}:
1631 name
1632 case Some{ver}:
1633 name ++ " " ++ ver
1634
1635# one help page
1636def help.page(
1637 +title: String,
1638 +about: String,
1639 +usage: String,
1640 +args: List<&2, Arg>,
1641 +subs: List<&2, Sub>
1642) -> String:
1643 title ++ "\n" ++ about ++ "\n\n" ++ usage ++ "\n\n" ++ help.cmds(subs) ++
1644 help.opts(args)
1645
1646# a found child becomes the page; an unknown name stays
1647def help.step.go(
1648 found: Maybe<&2, Sub>,
1649 +bin: String,
1650 +about: String,
1651 version: Maybe<&2, String>,
1652 args: List<&2, Arg>,
1653 subs: List<&2, Sub>,
1654 +next: String,
1655 +usage: String
1656) -> How:
1657 match found:
1658 case None{}:
1659 How{bin, about, version, args, subs, usage}
1660 case Some{s}:
1661 Sub{+name, +about2, args2, subs2} = s
1662 How{name, about2, None{}, args2, subs2, usage ++ " " ++ next}
1663
1664# one path segment against the current page
1665def help.step(how: How, +name: String) -> How:
1666 How{+bin, +about, version, args, +subs, +usage} = how
1667 help.step.go(find_sub(subs, name), bin, about, version, args, subs, name,
1668 usage)
1669
1670# the page at the end of a shrinking path
1671def help.go(path: List<&2, String>, how: How) -> String:
1672 match path:
1673 case Nil{}:
1674 How{+bin, +about, version, +args, +subs, +usage} = how
1675 help.page(title.go(bin, version), about, help.usage(usage, args, subs),
1676 args, subs)
1677 case Con{+h, t}:
1678 help.go(t, help.step(how, h))
1679
1680# usage for `path` under `app`; an unknown segment shows the last found page
1681def help(+app: Cli, path: List<&2, String>) -> String:
1682 Cli{+name, +about, version, args, subs} = app
1683 help.go(path, How{name, about, version, args, subs, name})
1684
1685# the usage line of the page at the end of a shrinking path
1686def usage.go(path: List<&2, String>, how: How) -> String:
1687 match path:
1688 case Nil{}:
1689 How{_b, _a, _v, +args, +subs, +usage} = how
1690 help.usage(usage, args, subs)
1691 case Con{+hh, tt}:
1692 usage.go(tt, help.step(how, hh))
1693
1694# the usage line of the command at `path`, as `help` shows it
1695def usage.at(+app: Cli, path: List<&2, String>) -> String:
1696 Cli{+name, +about, version, args, subs} = app
1697 usage.go(path, How{name, about, version, args, subs, name})
1698
1699# where to look for more: `help`, and the path when there is one
1700def err.hint(+path: List<&2, String>) -> String:
1701 "For more information, try 'help" ++
1702 Bool.pick(String, List.is_empty(&2, String, path), "", " " ++ String.join(path, " ")) ++ "'.\n"
1703
1704# a short error, the usage line of the command where it happened, and the hint
1705def err.wrap(+app: Cli, +at: List<&2, String>, +msg: String) -> String:
1706 msg ++ "\n\n" ++ usage.at(app, at) ++ "\n\n" ++ err.hint(at)
1707
1708# error text; NeedHelp is empty (the caller prints `help`)
1709def err_text(+app: Cli, ee: ParseErr) -> String:
1710 match ee:
1711 case UnknownFlag{+at, word}:
1712 err.wrap(app, at, "error: unexpected argument '" ++ word ++ "' found")
1713 case Missing{+at, name}:
1714 err.wrap(app, at, "error: the following required argument was not provided:\n " ++
1715 name)
1716 case NoValue{+at, name}:
1717 err.wrap(app, at, "error: a value is required for '" ++ name ++ "' but none was supplied")
1718 case BadValue{+at, name, value}:
1719 err.wrap(app, at, "error: invalid value '" ++ value ++ "' for '" ++ name ++ "'")
1720 case NeedHelp{_path}:
1721 ""
1722 case Unexpected{+at, arg}:
1723 err.wrap(app, at, "error: unexpected argument '" ++ arg ++ "' found")
1724 case Repeated{+at, name}:
1725 err.wrap(app, at, "error: the argument '" ++ name ++ "' cannot be used multiple times")