cli.bend
cli@0.1.0.0 · 7,654 lines · 0 definitions
1# Purpose: pure command definitions, validation, and text rendering for Bend CLIs.
2# Expected output: ALL PROOFS CHECK.
3# Run: bend cli.bend --check-only
4
5import Base
6
7type Style is Data:
8 Bold{}
9 Dim{}
10 Under{}
11 Red{}
12 Green{}
13 Yellow{}
14 Blue{}
15 Magenta{}
16 Cyan{}
17
18type Segment is Data:
19 Segment{text: String, styles: List<&2, Style>}
20
21def style_code(style: Style) -> String:
22 match style:
23 case Bold{}: "1"
24 case Dim{}: "2"
25 case Under{}: "4"
26 case Red{}: "31"
27 case Green{}: "32"
28 case Yellow{}: "33"
29 case Blue{}: "34"
30 case Magenta{}: "35"
31 case Cyan{}: "36"
32
33def style_codes_tail(styles: List<&2, Style>) -> String:
34 match styles:
35 case Nil{}: ""
36 case h <> t:
37 String.append(";", String.append(style_code(h), style_codes_tail(t)))
38
39def style_codes(styles: List<&2, Style>) -> String:
40 match styles:
41 case Nil{}: ""
42 case h <> t:
43 String.append(style_code(h), style_codes_tail(t))
44
45def paint(on: Bool, styles: List<&2, Style>, text: String) -> String:
46 match on styles:
47 case False{} _:
48 text
49 case True{} Nil{}:
50 text
51 case True{} h <> t:
52 String.append("\u{1B}[",
53 String.append(style_codes(h <> t),
54 String.append("m", String.append(text, "\u{1B}[0m"))))
55
56def emit_go(segments: List<&2, Segment>, +on: Bool) -> String:
57 match segments:
58 case Nil{}:
59 ""
60 case h <> t:
61 match h:
62 case Segment{text, styles}:
63 String.append(paint(on, styles, text), emit_go(t, on))
64
65def emit(on: Bool, segments: List<&2, Segment>) -> String:
66 emit_go(segments, on)
67
68def sgr_body_char(+c: Char) -> Bool:
69 Bool.or(Char.is_digit(c), Char.is_eq(c, ';'))
70
71type StripState is Data:
72 StripPlain{}
73 StripEsc{}
74 StripBody{pending: String}
75
76def strip_go(s: String, state: StripState) -> String:
77 match s:
78 case SNil{}:
79 match state:
80 case StripPlain{}: ""
81 case StripEsc{}: "\u{1B}"
82 case StripBody{pending}: pending
83 case SCon{+h, t}:
84 match state:
85 case StripPlain{}:
86 +esc = Char.is_eq(h, '\u{1B}')
87 String.append(Bool.pick(String, esc, "", Char.show(h)),
88 strip_go(t, Bool.pick(StripState, esc, StripEsc{}, StripPlain{})))
89 case StripEsc{}:
90 +bracket = Char.is_eq(h, '[')
91 +esc = Char.is_eq(h, '\u{1B}')
92 +prefix = Bool.pick(String, bracket, "",
93 Bool.pick(String, esc, "\u{1B}",
94 String.append("\u{1B}", Char.show(h))))
95 +next = Bool.pick(StripState, bracket, StripBody{"\u{1B}["},
96 Bool.pick(StripState, esc, StripEsc{}, StripPlain{}))
97 String.append(prefix, strip_go(t, next))
98 case StripBody{+pending}:
99 +end = Char.is_eq(h, 'm')
100 +esc = Char.is_eq(h, '\u{1B}')
101 +ok = sgr_body_char(h)
102 +grown = String.append(pending, Char.show(h))
103 +prefix = Bool.pick(String, end, "",
104 Bool.pick(String, esc, pending,
105 Bool.pick(String, ok, "", grown)))
106 +next = Bool.pick(StripState, end, StripPlain{},
107 Bool.pick(StripState, esc, StripEsc{},
108 Bool.pick(StripState, ok, StripBody{grown}, StripPlain{})))
109 String.append(prefix, strip_go(t, next))
110
111def strip(s: String) -> String:
112 strip_go(s, StripPlain{})
113
114type CodeRange is Data:
115 CodeRange{lo: U32, hi: U32}
116
117def contains_range(ranges: List<&2, CodeRange>, +code: U32) -> Bool:
118 match ranges:
119 case Nil{}:
120 False{}
121 case CodeRange{lo, hi} <> tail:
122 Bool.or(Bool.and(U32.is_ge(code, lo), U32.is_le(code, hi)),
123 contains_range(tail, code))
124
125# Unicode 15.1 East Asian Wide and Fullwidth ranges, including reserved ranges.
126# Source: https://www.unicode.org/Public/15.1.0/ucd/EastAsianWidth.txt
127def unicode_is_wide(code: U32) -> Bool:
128 contains_range([
129 CodeRange{4352, 4447},
130 CodeRange{8986, 8987},
131 CodeRange{9001, 9002},
132 CodeRange{9193, 9196},
133 CodeRange{9200, 9200},
134 CodeRange{9203, 9203},
135 CodeRange{9725, 9726},
136 CodeRange{9748, 9749},
137 CodeRange{9800, 9811},
138 CodeRange{9855, 9855},
139 CodeRange{9875, 9875},
140 CodeRange{9889, 9889},
141 CodeRange{9898, 9899},
142 CodeRange{9917, 9918},
143 CodeRange{9924, 9925},
144 CodeRange{9934, 9934},
145 CodeRange{9940, 9940},
146 CodeRange{9962, 9962},
147 CodeRange{9970, 9971},
148 CodeRange{9973, 9973},
149 CodeRange{9978, 9978},
150 CodeRange{9981, 9981},
151 CodeRange{9989, 9989},
152 CodeRange{9994, 9995},
153 CodeRange{10024, 10024},
154 CodeRange{10060, 10060},
155 CodeRange{10062, 10062},
156 CodeRange{10067, 10069},
157 CodeRange{10071, 10071},
158 CodeRange{10133, 10135},
159 CodeRange{10160, 10160},
160 CodeRange{10175, 10175},
161 CodeRange{11035, 11036},
162 CodeRange{11088, 11088},
163 CodeRange{11093, 11093},
164 CodeRange{11904, 11929},
165 CodeRange{11931, 12019},
166 CodeRange{12032, 12245},
167 CodeRange{12272, 12350},
168 CodeRange{12353, 12438},
169 CodeRange{12441, 12543},
170 CodeRange{12549, 12591},
171 CodeRange{12593, 12686},
172 CodeRange{12688, 12771},
173 CodeRange{12783, 12830},
174 CodeRange{12832, 12871},
175 CodeRange{12880, 19903},
176 CodeRange{19968, 42124},
177 CodeRange{42128, 42182},
178 CodeRange{43360, 43388},
179 CodeRange{44032, 55203},
180 CodeRange{63744, 64255},
181 CodeRange{65040, 65049},
182 CodeRange{65072, 65106},
183 CodeRange{65108, 65126},
184 CodeRange{65128, 65131},
185 CodeRange{65281, 65376},
186 CodeRange{65504, 65510},
187 CodeRange{94176, 94180},
188 CodeRange{94192, 94193},
189 CodeRange{94208, 100343},
190 CodeRange{100352, 101589},
191 CodeRange{101632, 101640},
192 CodeRange{110576, 110579},
193 CodeRange{110581, 110587},
194 CodeRange{110589, 110590},
195 CodeRange{110592, 110882},
196 CodeRange{110898, 110898},
197 CodeRange{110928, 110930},
198 CodeRange{110933, 110933},
199 CodeRange{110948, 110951},
200 CodeRange{110960, 111355},
201 CodeRange{126980, 126980},
202 CodeRange{127183, 127183},
203 CodeRange{127374, 127374},
204 CodeRange{127377, 127386},
205 CodeRange{127488, 127490},
206 CodeRange{127504, 127547},
207 CodeRange{127552, 127560},
208 CodeRange{127568, 127569},
209 CodeRange{127584, 127589},
210 CodeRange{127744, 127776},
211 CodeRange{127789, 127797},
212 CodeRange{127799, 127868},
213 CodeRange{127870, 127891},
214 CodeRange{127904, 127946},
215 CodeRange{127951, 127955},
216 CodeRange{127968, 127984},
217 CodeRange{127988, 127988},
218 CodeRange{127992, 128062},
219 CodeRange{128064, 128064},
220 CodeRange{128066, 128252},
221 CodeRange{128255, 128317},
222 CodeRange{128331, 128334},
223 CodeRange{128336, 128359},
224 CodeRange{128378, 128378},
225 CodeRange{128405, 128406},
226 CodeRange{128420, 128420},
227 CodeRange{128507, 128591},
228 CodeRange{128640, 128709},
229 CodeRange{128716, 128716},
230 CodeRange{128720, 128722},
231 CodeRange{128725, 128727},
232 CodeRange{128732, 128735},
233 CodeRange{128747, 128748},
234 CodeRange{128756, 128764},
235 CodeRange{128992, 129003},
236 CodeRange{129008, 129008},
237 CodeRange{129292, 129338},
238 CodeRange{129340, 129349},
239 CodeRange{129351, 129535},
240 CodeRange{129648, 129660},
241 CodeRange{129664, 129672},
242 CodeRange{129680, 129725},
243 CodeRange{129727, 129733},
244 CodeRange{129742, 129755},
245 CodeRange{129760, 129768},
246 CodeRange{129776, 129784},
247 CodeRange{131072, 196605},
248 CodeRange{196608, 262141},
249 ], code)
250
251# Unicode 15.1 nonspacing and enclosing mark ranges (Mn and Me).
252# Source: https://www.unicode.org/Public/15.1.0/ucd/UnicodeData.txt
253def unicode_is_zero_mark(code: U32) -> Bool:
254 contains_range([
255 CodeRange{768, 879},
256 CodeRange{1155, 1161},
257 CodeRange{1425, 1469},
258 CodeRange{1471, 1471},
259 CodeRange{1473, 1474},
260 CodeRange{1476, 1477},
261 CodeRange{1479, 1479},
262 CodeRange{1552, 1562},
263 CodeRange{1611, 1631},
264 CodeRange{1648, 1648},
265 CodeRange{1750, 1756},
266 CodeRange{1759, 1764},
267 CodeRange{1767, 1768},
268 CodeRange{1770, 1773},
269 CodeRange{1809, 1809},
270 CodeRange{1840, 1866},
271 CodeRange{1958, 1968},
272 CodeRange{2027, 2035},
273 CodeRange{2045, 2045},
274 CodeRange{2070, 2073},
275 CodeRange{2075, 2083},
276 CodeRange{2085, 2087},
277 CodeRange{2089, 2093},
278 CodeRange{2137, 2139},
279 CodeRange{2200, 2207},
280 CodeRange{2250, 2273},
281 CodeRange{2275, 2306},
282 CodeRange{2362, 2362},
283 CodeRange{2364, 2364},
284 CodeRange{2369, 2376},
285 CodeRange{2381, 2381},
286 CodeRange{2385, 2391},
287 CodeRange{2402, 2403},
288 CodeRange{2433, 2433},
289 CodeRange{2492, 2492},
290 CodeRange{2497, 2500},
291 CodeRange{2509, 2509},
292 CodeRange{2530, 2531},
293 CodeRange{2558, 2558},
294 CodeRange{2561, 2562},
295 CodeRange{2620, 2620},
296 CodeRange{2625, 2626},
297 CodeRange{2631, 2632},
298 CodeRange{2635, 2637},
299 CodeRange{2641, 2641},
300 CodeRange{2672, 2673},
301 CodeRange{2677, 2677},
302 CodeRange{2689, 2690},
303 CodeRange{2748, 2748},
304 CodeRange{2753, 2757},
305 CodeRange{2759, 2760},
306 CodeRange{2765, 2765},
307 CodeRange{2786, 2787},
308 CodeRange{2810, 2815},
309 CodeRange{2817, 2817},
310 CodeRange{2876, 2876},
311 CodeRange{2879, 2879},
312 CodeRange{2881, 2884},
313 CodeRange{2893, 2893},
314 CodeRange{2901, 2902},
315 CodeRange{2914, 2915},
316 CodeRange{2946, 2946},
317 CodeRange{3008, 3008},
318 CodeRange{3021, 3021},
319 CodeRange{3072, 3072},
320 CodeRange{3076, 3076},
321 CodeRange{3132, 3132},
322 CodeRange{3134, 3136},
323 CodeRange{3142, 3144},
324 CodeRange{3146, 3149},
325 CodeRange{3157, 3158},
326 CodeRange{3170, 3171},
327 CodeRange{3201, 3201},
328 CodeRange{3260, 3260},
329 CodeRange{3263, 3263},
330 CodeRange{3270, 3270},
331 CodeRange{3276, 3277},
332 CodeRange{3298, 3299},
333 CodeRange{3328, 3329},
334 CodeRange{3387, 3388},
335 CodeRange{3393, 3396},
336 CodeRange{3405, 3405},
337 CodeRange{3426, 3427},
338 CodeRange{3457, 3457},
339 CodeRange{3530, 3530},
340 CodeRange{3538, 3540},
341 CodeRange{3542, 3542},
342 CodeRange{3633, 3633},
343 CodeRange{3636, 3642},
344 CodeRange{3655, 3662},
345 CodeRange{3761, 3761},
346 CodeRange{3764, 3772},
347 CodeRange{3784, 3790},
348 CodeRange{3864, 3865},
349 CodeRange{3893, 3893},
350 CodeRange{3895, 3895},
351 CodeRange{3897, 3897},
352 CodeRange{3953, 3966},
353 CodeRange{3968, 3972},
354 CodeRange{3974, 3975},
355 CodeRange{3981, 3991},
356 CodeRange{3993, 4028},
357 CodeRange{4038, 4038},
358 CodeRange{4141, 4144},
359 CodeRange{4146, 4151},
360 CodeRange{4153, 4154},
361 CodeRange{4157, 4158},
362 CodeRange{4184, 4185},
363 CodeRange{4190, 4192},
364 CodeRange{4209, 4212},
365 CodeRange{4226, 4226},
366 CodeRange{4229, 4230},
367 CodeRange{4237, 4237},
368 CodeRange{4253, 4253},
369 CodeRange{4957, 4959},
370 CodeRange{5906, 5908},
371 CodeRange{5938, 5939},
372 CodeRange{5970, 5971},
373 CodeRange{6002, 6003},
374 CodeRange{6068, 6069},
375 CodeRange{6071, 6077},
376 CodeRange{6086, 6086},
377 CodeRange{6089, 6099},
378 CodeRange{6109, 6109},
379 CodeRange{6155, 6157},
380 CodeRange{6159, 6159},
381 CodeRange{6277, 6278},
382 CodeRange{6313, 6313},
383 CodeRange{6432, 6434},
384 CodeRange{6439, 6440},
385 CodeRange{6450, 6450},
386 CodeRange{6457, 6459},
387 CodeRange{6679, 6680},
388 CodeRange{6683, 6683},
389 CodeRange{6742, 6742},
390 CodeRange{6744, 6750},
391 CodeRange{6752, 6752},
392 CodeRange{6754, 6754},
393 CodeRange{6757, 6764},
394 CodeRange{6771, 6780},
395 CodeRange{6783, 6783},
396 CodeRange{6832, 6862},
397 CodeRange{6912, 6915},
398 CodeRange{6964, 6964},
399 CodeRange{6966, 6970},
400 CodeRange{6972, 6972},
401 CodeRange{6978, 6978},
402 CodeRange{7019, 7027},
403 CodeRange{7040, 7041},
404 CodeRange{7074, 7077},
405 CodeRange{7080, 7081},
406 CodeRange{7083, 7085},
407 CodeRange{7142, 7142},
408 CodeRange{7144, 7145},
409 CodeRange{7149, 7149},
410 CodeRange{7151, 7153},
411 CodeRange{7212, 7219},
412 CodeRange{7222, 7223},
413 CodeRange{7376, 7378},
414 CodeRange{7380, 7392},
415 CodeRange{7394, 7400},
416 CodeRange{7405, 7405},
417 CodeRange{7412, 7412},
418 CodeRange{7416, 7417},
419 CodeRange{7616, 7679},
420 CodeRange{8400, 8432},
421 CodeRange{11503, 11505},
422 CodeRange{11647, 11647},
423 CodeRange{11744, 11775},
424 CodeRange{12330, 12333},
425 CodeRange{12441, 12442},
426 CodeRange{42607, 42610},
427 CodeRange{42612, 42621},
428 CodeRange{42654, 42655},
429 CodeRange{42736, 42737},
430 CodeRange{43010, 43010},
431 CodeRange{43014, 43014},
432 CodeRange{43019, 43019},
433 CodeRange{43045, 43046},
434 CodeRange{43052, 43052},
435 CodeRange{43204, 43205},
436 CodeRange{43232, 43249},
437 CodeRange{43263, 43263},
438 CodeRange{43302, 43309},
439 CodeRange{43335, 43345},
440 CodeRange{43392, 43394},
441 CodeRange{43443, 43443},
442 CodeRange{43446, 43449},
443 CodeRange{43452, 43453},
444 CodeRange{43493, 43493},
445 CodeRange{43561, 43566},
446 CodeRange{43569, 43570},
447 CodeRange{43573, 43574},
448 CodeRange{43587, 43587},
449 CodeRange{43596, 43596},
450 CodeRange{43644, 43644},
451 CodeRange{43696, 43696},
452 CodeRange{43698, 43700},
453 CodeRange{43703, 43704},
454 CodeRange{43710, 43711},
455 CodeRange{43713, 43713},
456 CodeRange{43756, 43757},
457 CodeRange{43766, 43766},
458 CodeRange{44005, 44005},
459 CodeRange{44008, 44008},
460 CodeRange{44013, 44013},
461 CodeRange{64286, 64286},
462 CodeRange{65024, 65039},
463 CodeRange{65056, 65071},
464 CodeRange{66045, 66045},
465 CodeRange{66272, 66272},
466 CodeRange{66422, 66426},
467 CodeRange{68097, 68099},
468 CodeRange{68101, 68102},
469 CodeRange{68108, 68111},
470 CodeRange{68152, 68154},
471 CodeRange{68159, 68159},
472 CodeRange{68325, 68326},
473 CodeRange{68900, 68903},
474 CodeRange{69291, 69292},
475 CodeRange{69373, 69375},
476 CodeRange{69446, 69456},
477 CodeRange{69506, 69509},
478 CodeRange{69633, 69633},
479 CodeRange{69688, 69702},
480 CodeRange{69744, 69744},
481 CodeRange{69747, 69748},
482 CodeRange{69759, 69761},
483 CodeRange{69811, 69814},
484 CodeRange{69817, 69818},
485 CodeRange{69826, 69826},
486 CodeRange{69888, 69890},
487 CodeRange{69927, 69931},
488 CodeRange{69933, 69940},
489 CodeRange{70003, 70003},
490 CodeRange{70016, 70017},
491 CodeRange{70070, 70078},
492 CodeRange{70089, 70092},
493 CodeRange{70095, 70095},
494 CodeRange{70191, 70193},
495 CodeRange{70196, 70196},
496 CodeRange{70198, 70199},
497 CodeRange{70206, 70206},
498 CodeRange{70209, 70209},
499 CodeRange{70367, 70367},
500 CodeRange{70371, 70378},
501 CodeRange{70400, 70401},
502 CodeRange{70459, 70460},
503 CodeRange{70464, 70464},
504 CodeRange{70502, 70508},
505 CodeRange{70512, 70516},
506 CodeRange{70712, 70719},
507 CodeRange{70722, 70724},
508 CodeRange{70726, 70726},
509 CodeRange{70750, 70750},
510 CodeRange{70835, 70840},
511 CodeRange{70842, 70842},
512 CodeRange{70847, 70848},
513 CodeRange{70850, 70851},
514 CodeRange{71090, 71093},
515 CodeRange{71100, 71101},
516 CodeRange{71103, 71104},
517 CodeRange{71132, 71133},
518 CodeRange{71219, 71226},
519 CodeRange{71229, 71229},
520 CodeRange{71231, 71232},
521 CodeRange{71339, 71339},
522 CodeRange{71341, 71341},
523 CodeRange{71344, 71349},
524 CodeRange{71351, 71351},
525 CodeRange{71453, 71455},
526 CodeRange{71458, 71461},
527 CodeRange{71463, 71467},
528 CodeRange{71727, 71735},
529 CodeRange{71737, 71738},
530 CodeRange{71995, 71996},
531 CodeRange{71998, 71998},
532 CodeRange{72003, 72003},
533 CodeRange{72148, 72151},
534 CodeRange{72154, 72155},
535 CodeRange{72160, 72160},
536 CodeRange{72193, 72202},
537 CodeRange{72243, 72248},
538 CodeRange{72251, 72254},
539 CodeRange{72263, 72263},
540 CodeRange{72273, 72278},
541 CodeRange{72281, 72283},
542 CodeRange{72330, 72342},
543 CodeRange{72344, 72345},
544 CodeRange{72752, 72758},
545 CodeRange{72760, 72765},
546 CodeRange{72767, 72767},
547 CodeRange{72850, 72871},
548 CodeRange{72874, 72880},
549 CodeRange{72882, 72883},
550 CodeRange{72885, 72886},
551 CodeRange{73009, 73014},
552 CodeRange{73018, 73018},
553 CodeRange{73020, 73021},
554 CodeRange{73023, 73029},
555 CodeRange{73031, 73031},
556 CodeRange{73104, 73105},
557 CodeRange{73109, 73109},
558 CodeRange{73111, 73111},
559 CodeRange{73459, 73460},
560 CodeRange{73472, 73473},
561 CodeRange{73526, 73530},
562 CodeRange{73536, 73536},
563 CodeRange{73538, 73538},
564 CodeRange{78912, 78912},
565 CodeRange{78919, 78933},
566 CodeRange{92912, 92916},
567 CodeRange{92976, 92982},
568 CodeRange{94031, 94031},
569 CodeRange{94095, 94098},
570 CodeRange{94180, 94180},
571 CodeRange{113821, 113822},
572 CodeRange{118528, 118573},
573 CodeRange{118576, 118598},
574 CodeRange{119143, 119145},
575 CodeRange{119163, 119170},
576 CodeRange{119173, 119179},
577 CodeRange{119210, 119213},
578 CodeRange{119362, 119364},
579 CodeRange{121344, 121398},
580 CodeRange{121403, 121452},
581 CodeRange{121461, 121461},
582 CodeRange{121476, 121476},
583 CodeRange{121499, 121503},
584 CodeRange{121505, 121519},
585 CodeRange{122880, 122886},
586 CodeRange{122888, 122904},
587 CodeRange{122907, 122913},
588 CodeRange{122915, 122916},
589 CodeRange{122918, 122922},
590 CodeRange{123023, 123023},
591 CodeRange{123184, 123190},
592 CodeRange{123566, 123566},
593 CodeRange{123628, 123631},
594 CodeRange{124140, 124143},
595 CodeRange{125136, 125142},
596 CodeRange{125252, 125258},
597 CodeRange{917760, 917999},
598 ], code)
599
600# Unicode 15.1 Extended_Pictographic ranges, including reserved ranges.
601# Source: https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-data.txt
602def unicode_is_pictographic(code: U32) -> Bool:
603 contains_range([
604 CodeRange{169, 169},
605 CodeRange{174, 174},
606 CodeRange{8252, 8252},
607 CodeRange{8265, 8265},
608 CodeRange{8482, 8482},
609 CodeRange{8505, 8505},
610 CodeRange{8596, 8601},
611 CodeRange{8617, 8618},
612 CodeRange{8986, 8987},
613 CodeRange{9000, 9000},
614 CodeRange{9096, 9096},
615 CodeRange{9167, 9167},
616 CodeRange{9193, 9203},
617 CodeRange{9208, 9210},
618 CodeRange{9410, 9410},
619 CodeRange{9642, 9643},
620 CodeRange{9654, 9654},
621 CodeRange{9664, 9664},
622 CodeRange{9723, 9726},
623 CodeRange{9728, 9733},
624 CodeRange{9735, 9746},
625 CodeRange{9748, 9861},
626 CodeRange{9872, 9989},
627 CodeRange{9992, 10002},
628 CodeRange{10004, 10004},
629 CodeRange{10006, 10006},
630 CodeRange{10013, 10013},
631 CodeRange{10017, 10017},
632 CodeRange{10024, 10024},
633 CodeRange{10035, 10036},
634 CodeRange{10052, 10052},
635 CodeRange{10055, 10055},
636 CodeRange{10060, 10060},
637 CodeRange{10062, 10062},
638 CodeRange{10067, 10069},
639 CodeRange{10071, 10071},
640 CodeRange{10083, 10087},
641 CodeRange{10133, 10135},
642 CodeRange{10145, 10145},
643 CodeRange{10160, 10160},
644 CodeRange{10175, 10175},
645 CodeRange{10548, 10549},
646 CodeRange{11013, 11015},
647 CodeRange{11035, 11036},
648 CodeRange{11088, 11088},
649 CodeRange{11093, 11093},
650 CodeRange{12336, 12336},
651 CodeRange{12349, 12349},
652 CodeRange{12951, 12951},
653 CodeRange{12953, 12953},
654 CodeRange{126976, 127231},
655 CodeRange{127245, 127247},
656 CodeRange{127279, 127279},
657 CodeRange{127340, 127345},
658 CodeRange{127358, 127359},
659 CodeRange{127374, 127374},
660 CodeRange{127377, 127386},
661 CodeRange{127405, 127461},
662 CodeRange{127489, 127503},
663 CodeRange{127514, 127514},
664 CodeRange{127535, 127535},
665 CodeRange{127538, 127546},
666 CodeRange{127548, 127551},
667 CodeRange{127561, 127994},
668 CodeRange{128000, 128317},
669 CodeRange{128326, 128591},
670 CodeRange{128640, 128767},
671 CodeRange{128884, 128895},
672 CodeRange{128981, 129023},
673 CodeRange{129036, 129039},
674 CodeRange{129096, 129103},
675 CodeRange{129114, 129119},
676 CodeRange{129160, 129167},
677 CodeRange{129198, 129279},
678 CodeRange{129292, 129338},
679 CodeRange{129340, 129349},
680 CodeRange{129351, 129791},
681 CodeRange{130048, 131069},
682 ], code)
683
684# Unicode 15.1 Emoji_Modifier_Base ranges: the emoji a skin tone modifies.
685# Source: https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-data.txt
686def unicode_takes_tone(code: U32) -> Bool:
687 contains_range([
688 CodeRange{9757, 9757},
689 CodeRange{9977, 9977},
690 CodeRange{9994, 9997},
691 CodeRange{127877, 127877},
692 CodeRange{127938, 127940},
693 CodeRange{127943, 127943},
694 CodeRange{127946, 127948},
695 CodeRange{128066, 128067},
696 CodeRange{128070, 128080},
697 CodeRange{128102, 128120},
698 CodeRange{128124, 128124},
699 CodeRange{128129, 128131},
700 CodeRange{128133, 128135},
701 CodeRange{128143, 128143},
702 CodeRange{128145, 128145},
703 CodeRange{128170, 128170},
704 CodeRange{128372, 128373},
705 CodeRange{128378, 128378},
706 CodeRange{128400, 128400},
707 CodeRange{128405, 128406},
708 CodeRange{128581, 128583},
709 CodeRange{128587, 128591},
710 CodeRange{128675, 128675},
711 CodeRange{128692, 128694},
712 CodeRange{128704, 128704},
713 CodeRange{128716, 128716},
714 CodeRange{129292, 129292},
715 CodeRange{129295, 129295},
716 CodeRange{129304, 129311},
717 CodeRange{129318, 129318},
718 CodeRange{129328, 129337},
719 CodeRange{129340, 129342},
720 CodeRange{129399, 129399},
721 CodeRange{129461, 129462},
722 CodeRange{129464, 129465},
723 CodeRange{129467, 129467},
724 CodeRange{129485, 129487},
725 CodeRange{129489, 129501},
726 CodeRange{129731, 129733},
727 CodeRange{129776, 129784},
728 ], code)
729
730# Unicode 15.1 code points with an emoji presentation sequence (VS16) that
731# are not East Asian Wide or Fullwidth, so VS16 widens them to 2.
732# Sources: https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-variation-sequences.txt
733# and https://www.unicode.org/Public/15.1.0/ucd/EastAsianWidth.txt
734def unicode_widens_with_vs16(code: U32) -> Bool:
735 contains_range([
736 CodeRange{35, 35},
737 CodeRange{42, 42},
738 CodeRange{48, 57},
739 CodeRange{169, 169},
740 CodeRange{174, 174},
741 CodeRange{8252, 8252},
742 CodeRange{8265, 8265},
743 CodeRange{8482, 8482},
744 CodeRange{8505, 8505},
745 CodeRange{8596, 8601},
746 CodeRange{8617, 8618},
747 CodeRange{9000, 9000},
748 CodeRange{9167, 9167},
749 CodeRange{9197, 9199},
750 CodeRange{9201, 9202},
751 CodeRange{9208, 9210},
752 CodeRange{9410, 9410},
753 CodeRange{9642, 9643},
754 CodeRange{9654, 9654},
755 CodeRange{9664, 9664},
756 CodeRange{9723, 9724},
757 CodeRange{9728, 9732},
758 CodeRange{9742, 9742},
759 CodeRange{9745, 9745},
760 CodeRange{9752, 9752},
761 CodeRange{9757, 9757},
762 CodeRange{9760, 9760},
763 CodeRange{9762, 9763},
764 CodeRange{9766, 9766},
765 CodeRange{9770, 9770},
766 CodeRange{9774, 9775},
767 CodeRange{9784, 9786},
768 CodeRange{9792, 9792},
769 CodeRange{9794, 9794},
770 CodeRange{9823, 9824},
771 CodeRange{9827, 9827},
772 CodeRange{9829, 9830},
773 CodeRange{9832, 9832},
774 CodeRange{9851, 9851},
775 CodeRange{9854, 9854},
776 CodeRange{9874, 9874},
777 CodeRange{9876, 9879},
778 CodeRange{9881, 9881},
779 CodeRange{9883, 9884},
780 CodeRange{9888, 9888},
781 CodeRange{9895, 9895},
782 CodeRange{9904, 9905},
783 CodeRange{9928, 9928},
784 CodeRange{9935, 9935},
785 CodeRange{9937, 9937},
786 CodeRange{9939, 9939},
787 CodeRange{9961, 9961},
788 CodeRange{9968, 9969},
789 CodeRange{9972, 9972},
790 CodeRange{9975, 9977},
791 CodeRange{9986, 9986},
792 CodeRange{9992, 9993},
793 CodeRange{9996, 9997},
794 CodeRange{9999, 9999},
795 CodeRange{10002, 10002},
796 CodeRange{10004, 10004},
797 CodeRange{10006, 10006},
798 CodeRange{10013, 10013},
799 CodeRange{10017, 10017},
800 CodeRange{10035, 10036},
801 CodeRange{10052, 10052},
802 CodeRange{10055, 10055},
803 CodeRange{10083, 10084},
804 CodeRange{10145, 10145},
805 CodeRange{10548, 10549},
806 CodeRange{11013, 11015},
807 CodeRange{127344, 127345},
808 CodeRange{127358, 127359},
809 CodeRange{127777, 127777},
810 CodeRange{127780, 127788},
811 CodeRange{127798, 127798},
812 CodeRange{127869, 127869},
813 CodeRange{127894, 127895},
814 CodeRange{127897, 127899},
815 CodeRange{127902, 127903},
816 CodeRange{127947, 127950},
817 CodeRange{127956, 127967},
818 CodeRange{127987, 127987},
819 CodeRange{127989, 127989},
820 CodeRange{127991, 127991},
821 CodeRange{128063, 128063},
822 CodeRange{128065, 128065},
823 CodeRange{128253, 128253},
824 CodeRange{128329, 128330},
825 CodeRange{128367, 128368},
826 CodeRange{128371, 128377},
827 CodeRange{128391, 128391},
828 CodeRange{128394, 128397},
829 CodeRange{128400, 128400},
830 CodeRange{128421, 128421},
831 CodeRange{128424, 128424},
832 CodeRange{128433, 128434},
833 CodeRange{128444, 128444},
834 CodeRange{128450, 128452},
835 CodeRange{128465, 128467},
836 CodeRange{128476, 128478},
837 CodeRange{128481, 128481},
838 CodeRange{128483, 128483},
839 CodeRange{128488, 128488},
840 CodeRange{128495, 128495},
841 CodeRange{128499, 128499},
842 CodeRange{128506, 128506},
843 CodeRange{128715, 128715},
844 CodeRange{128717, 128719},
845 CodeRange{128736, 128741},
846 CodeRange{128745, 128745},
847 CodeRange{128752, 128752},
848 CodeRange{128755, 128755},
849 ], code)
850
851# A code point's own width: controls, marks, 200B-200F and tag characters
852# (E0000-E007F) are 0, East Asian Wide and Fullwidth are 2, the rest 1.
853def code_width_by(small: Bool, +code: U32) -> Nat:
854 match small:
855 case True{}:
856 Bool.pick(Nat, Bool.or(U32.is_le(code, 31),
857 Bool.and(U32.is_ge(code, 127), U32.is_le(code, 159))), 0n, 1n)
858 case False{}:
859 Bool.pick(Nat, Bool.or(unicode_is_zero_mark(code),
860 Bool.or(Bool.and(U32.is_ge(code, 8203), U32.is_le(code, 8207)),
861 Bool.and(U32.is_ge(code, 917504), U32.is_le(code, 917631)))), 0n,
862 Bool.pick(Nat, unicode_is_wide(code), 2n, 1n))
863
864def code_width(+code: U32) -> Nat:
865 code_width_by(U32.is_lt(code, 768), code)
866
867# What a code point can do in an emoji sequence (UTS #51): start a keycap
868# (#, * and 0-9), be a pictograph, a skin tone, a ZWJ or VS16, or none.
869type CodeClass is Data:
870 PlainCode{}
871 KeycapCode{}
872 PictoCode{}
873 ToneCode{}
874 JoinerCode{}
875 Vs16Code{}
876
877# Where width stands in an emoji sequence: outside one; after an emoji, with
878# the code a skin tone or VS16 applies to, whether it counts 1 so far, and
879# whether a ZWJ may join it; after such a ZWJ; or after a pictograph that a
880# ZWJ joined, which adds nothing.
881type EmojiRun is Data:
882 EmojiNone{}
883 EmojiBase{code: U32, narrow: Bool, join: Bool}
884 EmojiJoiner{}
885 EmojiJoined{code: U32}
886
887type WidthStep is Data:
888 WidthStep{width: Nat, run: EmojiRun}
889
890def keycap_base(+code: U32) -> Bool:
891 Bool.or(Bool.or(U32.is_eq(code, 35), U32.is_eq(code, 42)),
892 Bool.and(U32.is_ge(code, 48), U32.is_le(code, 57)))
893
894# may: whether code is in 203C-3299 or from 1F000, which hold every
895# pictograph from 768 on.
896def picto_class(may: Bool, +code: U32) -> CodeClass:
897 match may:
898 case True{}:
899 Bool.pick(CodeClass, unicode_is_pictographic(code), PictoCode{},
900 PlainCode{})
901 case False{}: PlainCode{}
902
903def code_class_by(small: Bool, +code: U32) -> CodeClass:
904 match small:
905 case True{}:
906 Bool.pick(CodeClass, keycap_base(code), KeycapCode{},
907 Bool.pick(CodeClass, Bool.or(U32.is_eq(code, 169),
908 U32.is_eq(code, 174)), PictoCode{}, PlainCode{}))
909 case False{}:
910 Bool.pick(CodeClass, U32.is_eq(code, 65039), Vs16Code{},
911 Bool.pick(CodeClass, U32.is_eq(code, 8205), JoinerCode{},
912 Bool.pick(CodeClass, Bool.and(U32.is_ge(code, 127995),
913 U32.is_le(code, 127999)), ToneCode{},
914 picto_class(Bool.or(Bool.and(U32.is_ge(code, 8252),
915 U32.is_le(code, 12953)), U32.is_ge(code, 126976)), code))))
916
917def code_class(+code: U32) -> CodeClass:
918 code_class_by(U32.is_lt(code, 768), code)
919
920def picto_alone(+wide: Bool, code: U32) -> WidthStep:
921 WidthStep{Bool.pick(Nat, wide, 2n, 1n),
922 EmojiBase{code, Bool.not(wide), True{}}}
923
924# A pictograph after a ZWJ joins the sequence; any other starts an emoji.
925def picto_step(run: EmojiRun, +code: U32) -> WidthStep:
926 match run:
927 case EmojiNone{}: picto_alone(unicode_is_wide(code), code)
928 case EmojiBase{base, narrow, join}:
929 picto_alone(unicode_is_wide(code), code)
930 case EmojiJoiner{}: WidthStep{0n, EmojiJoined{code}}
931 case EmojiJoined{base}: picto_alone(unicode_is_wide(code), code)
932
933# A skin tone after an emoji that takes one widens it to 2; any other is
934# 2 wide alone, as East Asian Wide.
935def tone_on(takes: Bool, narrow: Bool, join: Bool, code: U32) ->
936 WidthStep:
937 match takes:
938 case True{}:
939 WidthStep{Bool.pick(Nat, narrow, 1n, 0n), EmojiBase{code, False{}, join}}
940 case False{}: WidthStep{2n, EmojiNone{}}
941
942def tone_on_joined(takes: Bool, code: U32) -> WidthStep:
943 match takes:
944 case True{}: WidthStep{0n, EmojiJoined{code}}
945 case False{}: WidthStep{2n, EmojiNone{}}
946
947def tone_step(run: EmojiRun, code: U32) -> WidthStep:
948 match run:
949 case EmojiNone{}: WidthStep{2n, EmojiNone{}}
950 case EmojiBase{base, narrow, join}:
951 tone_on(unicode_takes_tone(base), narrow, join, code)
952 case EmojiJoiner{}: WidthStep{2n, EmojiNone{}}
953 case EmojiJoined{base}: tone_on_joined(unicode_takes_tone(base), code)
954
955# A ZWJ after an emoji that may join, or after a joined pictograph, waits
956# for the next pictograph.
957def joiner_run(run: EmojiRun) -> EmojiRun:
958 match run:
959 case EmojiNone{}: EmojiNone{}
960 case EmojiBase{code, narrow, join}:
961 Bool.pick(EmojiRun, join, EmojiJoiner{}, EmojiNone{})
962 case EmojiJoiner{}: EmojiNone{}
963 case EmojiJoined{code}: EmojiJoiner{}
964
965def vs16_widen(widens: Bool, code: U32, join: Bool) -> WidthStep:
966 match widens:
967 case True{}: WidthStep{1n, EmojiBase{code, False{}, join}}
968 case False{}: WidthStep{0n, EmojiBase{code, True{}, join}}
969
970def vs16_on(narrow: Bool, +code: U32, join: Bool) -> WidthStep:
971 match narrow:
972 case True{}: vs16_widen(unicode_widens_with_vs16(code), code, join)
973 case False{}: WidthStep{0n, EmojiBase{code, False{}, join}}
974
975# VS16 widens a 1-wide emoji with an emoji presentation to 2, once.
976def vs16_step(run: EmojiRun) -> WidthStep:
977 match run:
978 case EmojiNone{}: WidthStep{0n, EmojiNone{}}
979 case EmojiBase{code, narrow, join}: vs16_on(narrow, code, join)
980 case EmojiJoiner{}: WidthStep{0n, EmojiNone{}}
981 case EmojiJoined{code}: WidthStep{0n, EmojiJoined{code}}
982
983def code_step(cls: CodeClass, run: EmojiRun, code: U32) -> WidthStep:
984 match cls:
985 case PlainCode{}: WidthStep{code_width(code), EmojiNone{}}
986 case KeycapCode{}: WidthStep{1n, EmojiBase{code, True{}, False{}}}
987 case PictoCode{}: picto_step(run, code)
988 case ToneCode{}: tone_step(run, code)
989 case JoinerCode{}: WidthStep{0n, joiner_run(run)}
990 case Vs16Code{}: vs16_step(run)
991
992# acc, plus the width of step, the last code point's, and of s after it.
993def width_go(s: String, step: WidthStep, acc: Nat) -> Nat:
994 match s step:
995 case SNil{} WidthStep{w, run}: Nat.add(w, acc)
996 case SCon{head, tail} WidthStep{w, run}:
997 +code = Char.to_u32(head)
998 width_go(tail, code_step(code_class(code), run, code), Nat.add(w, acc))
999
1000# Display columns, as Rust's unicode-width 0.2 counts them: each code point's
1001# own width, except that an emoji sequence is one cell 2 wide. A pictograph
1002# that a ZWJ joins, a skin tone after an emoji that takes one, and tag
1003# characters add nothing; VS16 widens a 1-wide emoji to 2.
1004def width(s: String) -> Nat:
1005 width_go(s, WidthStep{0n, EmojiNone{}}, 0n)
1006
1007def leading_indent(s: String) -> String:
1008 match s:
1009 case SNil{}:
1010 ""
1011 case SCon{+head, tail}:
1012 Bool.pick(String, Char.is_eq(head, ' '),
1013 SCon{head, leading_indent(tail)}, "")
1014
1015type WrapState is Data:
1016 WrapState{indent: String, cols: Nat, line: String,
1017 started: Bool, done: List<&2, String>}
1018
1019def wrap_words(words: List<&2, String>, state: WrapState) -> String:
1020 match words state:
1021 case Nil{} WrapState{indent, cols, line, started, done}:
1022 Bool.pick(String, started,
1023 String.join(List.reverse(&2, String, line <> done), "\n"), "")
1024 case +word <> rest WrapState{+indent, +cols, +line, +started, +done}:
1025 +candidate = Bool.pick(String, started,
1026 String.append(line, String.append(" ", word)),
1027 String.append(indent, word))
1028 fits = Bool.or(Bool.not(started), Nat.is_le(width(strip(candidate)), cols))
1029 next = Bool.pick(WrapState, fits,
1030 WrapState{indent, cols, candidate, True{}, done},
1031 WrapState{indent, cols, String.append(indent, word), True{}, line <> done})
1032 wrap_words(rest, next)
1033
1034def words(s: String) -> List<&2, String>:
1035 List.filter(~String, ~(w => Bool.not(String.is_empty(w))),
1036 String.split(s, ' '))
1037
1038def wrap_line(+line: String, cols: Nat) -> String:
1039 +indent = leading_indent(line)
1040 body = String.drop(line, String.length(indent))
1041 parts = words(body)
1042 wrap_words(parts, WrapState{indent, cols, indent, False{}, Nil{}})
1043
1044def wrap_lines(lines: List<&2, String>, +cols: Nat) -> List<&2, String>:
1045 match lines:
1046 case Nil{}:
1047 Nil{}
1048 case head <> tail:
1049 wrap_line(head, cols) <> wrap_lines(tail, cols)
1050
1051def wrap(text: String, cols: Nat) -> String:
1052 String.join(wrap_lines(String.lines(text), cols), "\n")
1053
1054type ValueType is Data:
1055 TNat{}
1056 TU32{}
1057 TF32{}
1058 TBool{}
1059 TFile{}
1060 TDir{}
1061
1062def type_name(t: ValueType) -> String:
1063 match t:
1064 case TNat{}: "as_nat"
1065 case TU32{}: "as_u32"
1066 case TF32{}: "as_f32"
1067 case TBool{}: "as_bool"
1068 case TFile{}: "as_file"
1069 case TDir{}: "as_dir"
1070
1071type Item is Data:
1072 Arg{usage: String, help: String, items: List<&2, Item>}
1073 Sub{name: String, about: String, items: List<&2, Item>}
1074 Version{value: String}
1075 Alias{name: String}
1076 Hidden{}
1077 Example{line: String, what: String}
1078 After{text: String}
1079 SubOptional{}
1080 External{}
1081 Exclusive{group: String}
1082 NeedOne{group: String}
1083 Id{value: String}
1084 Required{}
1085 AttrDefault{value: String}
1086 AttrEnv{name: String}
1087 Global{}
1088 Heading{title: String}
1089 As{t: ValueType}
1090 OneOf{values: List<&2, String>}
1091 Delim{sep: Char}
1092 Hyphen{}
1093 Rest{}
1094 Bare{value: String}
1095 Conflicts{id: String}
1096 Requires{id: String}
1097 Group{name: String}
1098 Builtin{}
1099
1100type Cmd is Data:
1101 Cmd{name: String, about: String, items: List<&2, Item>}
1102
1103type Problem is Data:
1104 BadUsage{at: String, usage: String, col: Nat}
1105 BadName{at: String, name: String}
1106 Dup{at: String, name: String}
1107 Reserved{at: String, name: String}
1108 Misplaced{at: String, attr: String}
1109 NoHelp{at: String, what: String}
1110 BadDefault{at: String, id: String, value: String}
1111 BadRef{at: String, id: String}
1112 BadPositional{at: String, id: String, why: String}
1113 Mixed{at: String}
1114 BadChoices{at: String, id: String}
1115 BadChoice{at: String, id: String, value: String}
1116 NeedBare{at: String, id: String}
1117 DecodeBug{at: String, msg: String}
1118
1119def cmd(name: String, about: String, items: List<&2, Item>) -> Cmd:
1120 Cmd{name, about, items}
1121
1122def sub(name: String, about: String, items: List<&2, Item>) -> Item:
1123 Sub{name, about, items}
1124
1125def arg(usage: String, help: String, items: List<&2, Item>) -> Item:
1126 Arg{usage, help, items}
1127
1128def version(value: String) -> Item:
1129 Version{value}
1130
1131def alias(name: String) -> Item:
1132 Alias{name}
1133
1134def hidden() -> Item:
1135 Hidden{}
1136
1137def example(line: String, what: String) -> Item:
1138 Example{line, what}
1139
1140def after(text: String) -> Item:
1141 After{text}
1142
1143def sub_optional() -> Item:
1144 SubOptional{}
1145
1146def external() -> Item:
1147 External{}
1148
1149def exclusive(group: String) -> Item:
1150 Exclusive{group}
1151
1152def need_one(group: String) -> Item:
1153 NeedOne{group}
1154
1155def id(value: String) -> Item:
1156 Id{value}
1157
1158def required() -> Item:
1159 Required{}
1160
1161def default(value: String) -> Item:
1162 AttrDefault{value}
1163
1164def env(name: String) -> Item:
1165 AttrEnv{name}
1166
1167def global() -> Item:
1168 Global{}
1169
1170def heading(title: String) -> Item:
1171 Heading{title}
1172
1173def as_nat() -> Item:
1174 As{TNat{}}
1175
1176def as_u32() -> Item:
1177 As{TU32{}}
1178
1179def as_f32() -> Item:
1180 As{TF32{}}
1181
1182def as_bool() -> Item:
1183 As{TBool{}}
1184
1185def as_file() -> Item:
1186 As{TFile{}}
1187
1188def as_dir() -> Item:
1189 As{TDir{}}
1190
1191def one_of(values: List<&2, String>) -> Item:
1192 OneOf{values}
1193
1194def delim(sep: Char) -> Item:
1195 Delim{sep}
1196
1197def hyphen() -> Item:
1198 Hyphen{}
1199
1200def rest() -> Item:
1201 Rest{}
1202
1203def bare(value: String) -> Item:
1204 Bare{value}
1205
1206def conflicts(target: String) -> Item:
1207 Conflicts{target}
1208
1209def requires(target: String) -> Item:
1210 Requires{target}
1211
1212def group(name: String) -> Item:
1213 Group{name}
1214
1215def color() -> Item:
1216 arg("--color <WHEN>", "When to use color", [one_of(["auto", "always", "never"]), default("auto"), global()])
1217
1218# Builtin{} marks the subcommands that only eval acts on (§6.4).
1219def completions() -> Item:
1220 sub("completions", "Print a shell completion script", [Builtin{}, arg("<SHELL>", "Shell to generate for", [one_of(["bash", "zsh", "fish"])])])
1221
1222def man() -> Item:
1223 sub("man", "Print the man page", [Builtin{}, hidden(), arg("[COMMAND]...", "Subcommand path", [])])
1224
1225type UsageKind is Data:
1226 FlagKind{}
1227 CountKind{}
1228 NegatableKind{}
1229 OptionKind{many: Bool}
1230 BareOptionKind{}
1231 PosKind{required: Bool, many: Bool}
1232
1233type Usage is Data:
1234 Parsed{id: String, short: String, long: String, kind: UsageKind}
1235 Invalid{col: Nat}
1236
1237type NameScan is Data:
1238 NameOK{name: String, rest: String, next: Nat}
1239 NameBad{col: Nat}
1240
1241# PhOK's name is reversed: only a positional's name is read.
1242type PhScan is Data:
1243 PhOK{rev: String, rest: String, next: Nat}
1244 PhBad{col: Nat}
1245
1246def name_start(+c: Char) -> Bool:
1247 Bool.or(Char.is_alpha(c), Char.is_digit(c))
1248
1249def name_char(+c: Char) -> Bool:
1250 Bool.or(name_start(c), Bool.or(Char.is_eq(c, '-'),
1251 Bool.or(Char.is_eq(c, '_'), Char.is_eq(c, '.'))))
1252
1253# rev is the name so far, reversed.
1254def finish_name(rev: String, rest: String, +pos: Nat,
1255 +first: Bool, +dot_start: Nat) -> NameScan:
1256 Bool.pick(NameScan, Bool.or(first, Nat.is_lt(dot_start, pos)),
1257 NameBad{Bool.pick(Nat, first, pos, dot_start)},
1258 NameOK{String.reverse(rev), rest, pos})
1259
1260def scan_name(s: String, +pos: Nat, rev: String,
1261 +first: Bool, dot_start: Nat) -> NameScan:
1262 match s:
1263 case SNil{}:
1264 finish_name(rev, SNil{}, pos, first, dot_start)
1265 case SCon{Chr{32}, tail}:
1266 finish_name(rev, SCon{Char.from_u32(32), tail}, pos, first, dot_start)
1267 case SCon{Chr{91}, tail}:
1268 finish_name(rev, SCon{Char.from_u32(91), tail}, pos, first, dot_start)
1269 case SCon{+h, tail}:
1270 valid = Bool.pick(Bool, first, name_start(h), name_char(h))
1271 next_dot = Bool.pick(Nat, Char.is_eq(h, '.'), dot_start, Nat.add(pos, 1n))
1272 Bool.pick(NameScan, valid,
1273 scan_name(tail, Nat.add(pos, 1n), SCon{h, rev}, False{}, next_dot),
1274 NameBad{pos})
1275
1276# A placeholder ends at a character no name holds: at close after a name,
1277# else with an error there.
1278def ph_stop(at_close: Bool, rev: String, rest: String, +pos: Nat) -> PhScan:
1279 match at_close rev:
1280 case True{} SCon{h, t}: PhOK{SCon{h, t}, rest, Nat.add(pos, 1n)}
1281 case _ _: PhBad{pos}
1282
1283# The placeholder name in s up to close, reversed in rev. A name holds no
1284# space, '<', '>', '[', ']' or '=', and close is one of them.
1285def scan_ph(s: String, +pos: Nat, rev: String, +close: Char) -> PhScan:
1286 match s:
1287 case SNil{}: PhBad{pos}
1288 case SCon{Chr{32}, tail}: ph_stop(Char.is_eq(' ', close), rev, tail, pos)
1289 case SCon{Chr{60}, tail}: ph_stop(Char.is_eq('<', close), rev, tail, pos)
1290 case SCon{Chr{62}, tail}: ph_stop(Char.is_eq('>', close), rev, tail, pos)
1291 case SCon{Chr{91}, tail}: ph_stop(Char.is_eq('[', close), rev, tail, pos)
1292 case SCon{Chr{93}, tail}: ph_stop(Char.is_eq(']', close), rev, tail, pos)
1293 case SCon{Chr{61}, tail}: ph_stop(Char.is_eq('=', close), rev, tail, pos)
1294 case SCon{h, tail}: scan_ph(tail, Nat.add(pos, 1n), SCon{h, rev}, close)
1295
1296def finish_pos(found: PhScan, kind: UsageKind) -> Usage:
1297 match found:
1298 case PhBad{col}:
1299 Invalid{col}
1300 case PhOK{rev, SNil{}, next}:
1301 Parsed{String.to_lower(String.reverse(rev)), "", "", kind}
1302 case PhOK{rev, rest, next}:
1303 Invalid{next}
1304
1305def parse_pos(s: String, +repeat: Bool) -> Usage:
1306 match s:
1307 case SCon{Chr{60}, tail}:
1308 finish_pos(scan_ph(tail, 1n, "", '>'),
1309 PosKind{True{}, repeat})
1310 case SCon{Chr{91}, tail}:
1311 finish_pos(scan_ph(tail, 1n, "", ']'),
1312 PosKind{False{}, repeat})
1313 case _:
1314 Invalid{0n}
1315
1316def finish_required_value(found: PhScan, id: String, short: String,
1317 long: String, kind: UsageKind) -> Usage:
1318 match found:
1319 case PhBad{col}:
1320 Invalid{col}
1321 case PhOK{rev, SNil{}, next}:
1322 Parsed{id, short, long, kind}
1323 case PhOK{rev, rest, next}:
1324 Invalid{next}
1325
1326def finish_optional_value(found: PhScan, id: String, short: String,
1327 long: String) -> Usage:
1328 match found:
1329 case PhBad{col}:
1330 Invalid{col}
1331 case PhOK{rev, SCon{Chr{93}, SNil{}}, next}:
1332 Parsed{id, short, long, BareOptionKind{}}
1333 case PhOK{rev, rest, next}:
1334 Invalid{next}
1335
1336def finish_long(found: NameScan, short: String,
1337 +repeat: Bool, +negatable: Bool) -> Usage:
1338 match found:
1339 case NameBad{col}:
1340 Invalid{col}
1341 case NameOK{+name, SNil{}, next}:
1342 Parsed{name, short, String.append("--", name),
1343 Bool.pick(UsageKind, negatable, NegatableKind{},
1344 Bool.pick(UsageKind, repeat, CountKind{}, FlagKind{}))}
1345 case NameOK{+name, SCon{Chr{32}, SCon{Chr{60}, tail}}, +next}:
1346 Bool.pick(Usage, negatable, Invalid{next},
1347 finish_required_value(scan_ph(tail, Nat.add(next, 2n), "", '>'),
1348 name, short, String.append("--", name),
1349 OptionKind{repeat}))
1350 case NameOK{+name, SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, tail}}}, +next}:
1351 Bool.pick(Usage, Bool.or(negatable, repeat), Invalid{next},
1352 finish_optional_value(scan_ph(tail, Nat.add(next, 3n), "", '>'),
1353 name, short, String.append("--", name)))
1354 case NameOK{name, rest, next}:
1355 Invalid{next}
1356
1357# A long name after "--[no-]" or "--", at column start.
1358def parse_long_prefixed(s: String, short: String,
1359 repeat: Bool, +start: Nat) -> Usage:
1360 match s:
1361 case SCon{Chr{45}, SCon{Chr{45}, SCon{Chr{91}, SCon{Chr{110},
1362 SCon{Chr{111}, SCon{Chr{45}, SCon{Chr{93}, rest}}}}}}}:
1363 finish_long(scan_name(rest, Nat.add(start, 7n), "", True{},
1364 Nat.add(start, 7n)), short, repeat, True{})
1365 case SCon{Chr{45}, SCon{Chr{45}, rest}}:
1366 finish_long(scan_name(rest, Nat.add(start, 2n), "", True{},
1367 Nat.add(start, 2n)), short, repeat, False{})
1368 case _:
1369 Invalid{start}
1370
1371def parse_short_tail(tail: String, +short: String, +repeat: Bool) -> Usage:
1372 match tail:
1373 case SNil{}:
1374 Parsed{String.drop(short, 1n), short, "",
1375 Bool.pick(UsageKind, repeat, CountKind{}, FlagKind{})}
1376 case SCon{Chr{44}, SCon{Chr{32}, long_rest}}:
1377 parse_long_prefixed(long_rest, short, repeat, 4n)
1378 case SCon{Chr{32}, SCon{Chr{60}, ph_rest}}:
1379 finish_required_value(scan_ph(ph_rest, 4n, "", '>'),
1380 String.drop(short, 1n), short, "",
1381 OptionKind{repeat})
1382 case SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, ph_rest}}}:
1383 finish_optional_value(scan_ph(ph_rest, 5n, "", '>'),
1384 String.drop(short, 1n), short, "")
1385 case _:
1386 Invalid{2n}
1387
1388def parse_short(s: String, repeat: Bool) -> Usage:
1389 match s:
1390 case SCon{Chr{45}, SCon{+h, tail}}:
1391 Bool.pick(Usage, name_start(h),
1392 parse_short_tail(tail, String.append("-", Char.show(h)), repeat),
1393 Invalid{1n})
1394 case _:
1395 Invalid{1n}
1396
1397def parse_usage_body(s: String, +repeat: Bool) -> Usage:
1398 match s:
1399 case SCon{Chr{60}, tail}:
1400 parse_pos(SCon{Char.from_u32(60), tail}, repeat)
1401 case SCon{Chr{91}, tail}:
1402 parse_pos(SCon{Char.from_u32(91), tail}, repeat)
1403 case SCon{Chr{45}, SCon{Chr{45}, tail}}:
1404 parse_long_prefixed(String.append("--", tail), "", repeat, 0n)
1405 case SCon{Chr{45}, tail}:
1406 parse_short(SCon{Char.from_u32(45), tail}, repeat)
1407 case _:
1408 Invalid{0n}
1409
1410# Whether s ends in "...": run counts the dots just before s.
1411def ends_in_ellipsis(s: String, run: Nat) -> Bool:
1412 match s:
1413 case SNil{}: Nat.is_lt(2n, run)
1414 case SCon{h, t}:
1415 ends_in_ellipsis(t, Bool.pick(Nat, Char.is_eq(h, '.'), Nat.add(run, 1n), 0n))
1416
1417def parse_usage_repeated(repeated: Bool, +usage: String) -> Usage:
1418 match repeated:
1419 case False{}: parse_usage_body(usage, False{})
1420 case True{}:
1421 parse_usage_body(String.take(usage, Nat.sub(String.length(usage), 3n)),
1422 True{})
1423
1424def parse_usage(+usage: String) -> Usage:
1425 parse_usage_repeated(ends_in_ellipsis(usage, 0n), usage)
1426
1427def alias_valid_result(parsed: Usage, +raw: String) -> Bool:
1428 match parsed:
1429 case Parsed{id, short, long, FlagKind{}}:
1430 Bool.or(String.eq(raw, short), String.eq(raw, long))
1431 case _: False{}
1432
1433def valid_arg_alias(+raw: String) -> Bool:
1434 alias_valid_result(parse_usage(raw), raw)
1435
1436def kind_pos(kind: UsageKind) -> Bool:
1437 match kind:
1438 case PosKind{required, many}: True{}
1439 case _: False{}
1440
1441def kind_optional_pos(kind: UsageKind) -> Bool:
1442 match kind:
1443 case PosKind{required, many}: Bool.not(required)
1444 case _: False{}
1445
1446def kind_variadic_pos(kind: UsageKind) -> Bool:
1447 match kind:
1448 case PosKind{required, many}: many
1449 case _: False{}
1450
1451def kind_required_pos(kind: UsageKind) -> Bool:
1452 match kind:
1453 case PosKind{required, many}: required
1454 case _: False{}
1455
1456def kind_valued(kind: UsageKind) -> Bool:
1457 match kind:
1458 case OptionKind{many}: True{}
1459 case BareOptionKind{}: True{}
1460 case PosKind{required, many}: True{}
1461 case _: False{}
1462
1463def kind_bare(kind: UsageKind) -> Bool:
1464 match kind:
1465 case BareOptionKind{}: True{}
1466 case _: False{}
1467
1468def kind_default_allowed(kind: UsageKind) -> Bool:
1469 match kind:
1470 case OptionKind{many}: True{}
1471 case BareOptionKind{}: True{}
1472 case PosKind{required, many}: Bool.not(required)
1473 case NegatableKind{}: True{}
1474 case _: False{}
1475
1476def kind_required_allowed(kind: UsageKind) -> Bool:
1477 match kind:
1478 case OptionKind{many}: True{}
1479 case _: False{}
1480
1481def kind_delim_allowed(kind: UsageKind) -> Bool:
1482 match kind:
1483 case OptionKind{many}: many
1484 case PosKind{required, many}: many
1485 case _: False{}
1486
1487def bad_attr(cond: Bool, name: String) -> Maybe<&2, String>:
1488 Bool.pick(Maybe<&2, String>, cond, Some{name}, None{})
1489
1490def misplaced_attr_name(attr: Item, +kind: UsageKind) ->
1491 Maybe<&2, String>:
1492 match attr:
1493 case Global{}: bad_attr(kind_pos(kind), "global")
1494 case Rest{}: bad_attr(Bool.not(kind_variadic_pos(kind)), "rest")
1495 case AttrDefault{value}:
1496 bad_attr(Bool.not(kind_default_allowed(kind)), "default")
1497 case Required{}:
1498 bad_attr(Bool.not(kind_required_allowed(kind)), "required")
1499 case Heading{title}: bad_attr(kind_pos(kind), "heading")
1500 case Alias{name}: bad_attr(kind_pos(kind), "alias")
1501 case As{t}: bad_attr(Bool.not(kind_valued(kind)), type_name(t))
1502 case OneOf{values}:
1503 bad_attr(Bool.not(kind_valued(kind)), "one_of")
1504 case Delim{sep}: bad_attr(Bool.not(kind_delim_allowed(kind)), "delim")
1505 case Hyphen{}: bad_attr(Bool.not(kind_valued(kind)), "hyphen")
1506 case Arg{usage, help, items}: Some{"arg"}
1507 case Sub{name, about, items}: Some{"sub"}
1508 case Version{value}: Some{"version"}
1509 case Example{line, what}: Some{"example"}
1510 case After{text}: Some{"after"}
1511 case SubOptional{}: Some{"sub_optional"}
1512 case External{}: Some{"external"}
1513 case Exclusive{group}: Some{"exclusive"}
1514 case NeedOne{group}: Some{"need_one"}
1515 case _: None{}
1516
1517def maybe_misplaced(found: Maybe<&2, String>, at: String) ->
1518 List<&2, Problem>:
1519 match found:
1520 case None{}: Nil{}
1521 case Some{name}: [Misplaced{at, name}]
1522
1523def placement_issues(attrs: List<&2, Item>, +kind: UsageKind,
1524 +at: String) -> List<&2, Problem>:
1525 match attrs:
1526 case Nil{}: Nil{}
1527 case head <> tail:
1528 List.append(&2, Problem,
1529 maybe_misplaced(misplaced_attr_name(head, kind), at),
1530 placement_issues(tail, kind, at))
1531
1532def value_type_names(attrs: List<&2, Item>) -> List<&2, String>:
1533 match attrs:
1534 case Nil{}: Nil{}
1535 case As{t} <> tail: type_name(t) <> value_type_names(tail)
1536 case head <> tail: value_type_names(tail)
1537
1538def extra_type_issues(names: List<&2, String>, +at: String) ->
1539 List<&2, Problem>:
1540 match names:
1541 case Nil{}: Nil{}
1542 case name <> tail:
1543 Misplaced{at, name} <> extra_type_issues(tail, at)
1544
1545def duplicate_type_issues(names: List<&2, String>, at: String) ->
1546 List<&2, Problem>:
1547 match names:
1548 case Nil{}: Nil{}
1549 case head <> tail: extra_type_issues(tail, at)
1550
1551def command_attr_name(attr: Item, root: Bool) -> Maybe<&2, String>:
1552 match attr:
1553 case Arg{usage, help, attrs}: None{}
1554 case Sub{name, about, children}: None{}
1555 case Version{value}: bad_attr(Bool.not(root), "version")
1556 case Alias{name}: bad_attr(root, "alias")
1557 case Hidden{}: bad_attr(root, "hidden")
1558 case Example{line, what}: None{}
1559 case After{text}: None{}
1560 case SubOptional{}: None{}
1561 case External{}: None{}
1562 case Exclusive{group}: None{}
1563 case NeedOne{group}: None{}
1564 case Id{value}: Some{"id"}
1565 case Required{}: Some{"required"}
1566 case AttrDefault{value}: Some{"default"}
1567 case AttrEnv{name}: Some{"env"}
1568 case Global{}: Some{"global"}
1569 case Heading{title}: Some{"heading"}
1570 case As{t}: Some{type_name(t)}
1571 case OneOf{values}: Some{"one_of"}
1572 case Delim{sep}: Some{"delim"}
1573 case Hyphen{}: Some{"hyphen"}
1574 case Rest{}: Some{"rest"}
1575 case Bare{value}: Some{"bare"}
1576 case Conflicts{id}: Some{"conflicts"}
1577 case Requires{id}: Some{"requires"}
1578 case Group{name}: Some{"group"}
1579 case Builtin{}: None{}
1580
1581def has_rest(items: List<&2, Item>) -> Bool:
1582 match items:
1583 case Nil{}: False{}
1584 case Rest{} <> tail: True{}
1585 case head <> tail: has_rest(tail)
1586
1587def has_hidden(items: List<&2, Item>) -> Bool:
1588 match items:
1589 case Nil{}: False{}
1590 case Hidden{} <> tail: True{}
1591 case head <> tail: has_hidden(tail)
1592
1593def has_builtin(items: List<&2, Item>) -> Bool:
1594 match items:
1595 case Nil{}: False{}
1596 case Builtin{} <> tail: True{}
1597 case head <> tail: has_builtin(tail)
1598
1599def has_bare(items: List<&2, Item>) -> Bool:
1600 match items:
1601 case Nil{}: False{}
1602 case Bare{value} <> tail: True{}
1603 case head <> tail: has_bare(tail)
1604
1605def has_required(items: List<&2, Item>) -> Bool:
1606 match items:
1607 case Nil{}: False{}
1608 case Required{} <> tail: True{}
1609 case head <> tail: has_required(tail)
1610
1611def duplicate_strings(xs: List<&2, String>) -> Bool:
1612 match xs:
1613 case Nil{}: False{}
1614 case +head <> +tail:
1615 Bool.or(List.contains(~String, ~String.eq, tail, head),
1616 duplicate_strings(tail))
1617
1618def bad_choices(items: List<&2, Item>) -> Bool:
1619 match items:
1620 case Nil{}: False{}
1621 case OneOf{+values} <> tail:
1622 Bool.or(Bool.or(List.is_empty(&2, String, values),
1623 duplicate_strings(values)), bad_choices(tail))
1624 case head <> tail: bad_choices(tail)
1625
1626def find_default(items: List<&2, Item>) -> Maybe<&2, String>:
1627 match items:
1628 case Nil{}: None{}
1629 case AttrDefault{value} <> tail: Some{value}
1630 case head <> tail: find_default(tail)
1631
1632def find_delim(attrs: List<&2, Item>) -> Maybe<&2, Char>:
1633 match attrs:
1634 case Nil{}: None{}
1635 case Delim{sep} <> tail: Some{sep}
1636 case head <> tail: find_delim(tail)
1637
1638def split_each(values: List<&2, String>, +sep: Char) ->
1639 List<&2, String>:
1640 match values:
1641 case Nil{}: Nil{}
1642 case head <> tail:
1643 List.append(&2, String, String.split(head, sep),
1644 split_each(tail, sep))
1645
1646def apply_delim(found: Maybe<&2, Char>,
1647 values: List<&2, String>) -> List<&2, String>:
1648 match found:
1649 case None{}: values
1650 case Some{sep}: split_each(values, sep)
1651
1652def find_id(items: List<&2, Item>) -> Maybe<&2, String>:
1653 match items:
1654 case Nil{}: None{}
1655 case Id{value} <> tail: Some{value}
1656 case head <> tail: find_id(tail)
1657
1658def find_requires(items: List<&2, Item>) -> List<&2, String>:
1659 match items:
1660 case Nil{}: Nil{}
1661 case Requires{target} <> tail: target <> find_requires(tail)
1662 case head <> tail: find_requires(tail)
1663
1664def find_conflicts(items: List<&2, Item>) -> List<&2, String>:
1665 match items:
1666 case Nil{}: Nil{}
1667 case Conflicts{target} <> tail: target <> find_conflicts(tail)
1668 case head <> tail: find_conflicts(tail)
1669
1670def find_group_attrs(items: List<&2, Item>) -> List<&2, String>:
1671 match items:
1672 case Nil{}: Nil{}
1673 case Group{name} <> tail: name <> find_group_attrs(tail)
1674 case head <> tail: find_group_attrs(tail)
1675
1676def find_aliases(items: List<&2, Item>) -> List<&2, String>:
1677 match items:
1678 case Nil{}: Nil{}
1679 case Alias{name} <> tail: name <> find_aliases(tail)
1680 case head <> tail: find_aliases(tail)
1681
1682def scope_groups(items: List<&2, Item>) -> List<&2, String>:
1683 match items:
1684 case Nil{}: Nil{}
1685 case Arg{usage, help, attrs} <> tail:
1686 List.append(&2, String, find_group_attrs(attrs), scope_groups(tail))
1687 case head <> tail: scope_groups(tail)
1688
1689def scope_group_rules(items: List<&2, Item>) -> List<&2, String>:
1690 match items:
1691 case Nil{}: Nil{}
1692 case Exclusive{group} <> tail: group <> scope_group_rules(tail)
1693 case NeedOne{group} <> tail: group <> scope_group_rules(tail)
1694 case head <> tail: scope_group_rules(tail)
1695
1696def usage_kind(~f: UsageKind -> Bool, parsed: Usage) -> Bool:
1697 match parsed:
1698 case Parsed{id, short, long, kind}: f(kind)
1699 case Invalid{col}: False{}
1700
1701# An item as lint walks it: an Arg with its usage parsed once, a Sub with
1702# its children's too.
1703type LintItem is Data:
1704 LintArg{usage: String, help: String, attrs: List<&2, Item>, parsed: Usage}
1705 LintSub{name: String, about: String, children: List<&2, Item>,
1706 items: List<&2, LintItem>}
1707 LintOther{item: Item}
1708
1709def lint_items(items: List<&2, Item>) -> List<&2, LintItem>:
1710 match items:
1711 case Nil{}: Nil{}
1712 case Arg{+usage, help, attrs} <> tail:
1713 LintArg{usage, help, attrs, parse_usage(usage)} <> lint_items(tail)
1714 case Sub{name, about, +children} <> tail:
1715 LintSub{name, about, children, lint_items(children)} <> lint_items(tail)
1716 case head <> tail: LintOther{head} <> lint_items(tail)
1717
1718def has_pos_item(items: List<&2, LintItem>) -> Bool:
1719 match items:
1720 case Nil{}: False{}
1721 case LintArg{usage, help, attrs, parsed} <> tail:
1722 Bool.or(usage_kind(~kind_pos, parsed), has_pos_item(tail))
1723 case head <> tail: has_pos_item(tail)
1724
1725def has_sub_item(items: List<&2, Item>) -> Bool:
1726 match items:
1727 case Nil{}: False{}
1728 case Sub{name, about, children} <> tail: True{}
1729 case head <> tail: has_sub_item(tail)
1730
1731def has_version_item(items: List<&2, Item>) -> Bool:
1732 match items:
1733 case Nil{}: False{}
1734 case Version{value} <> tail: True{}
1735 case head <> tail: has_version_item(tail)
1736
1737def name_scan_valid(result: NameScan) -> Bool:
1738 match result:
1739 case NameOK{name, SNil{}, next}: True{}
1740 case _: False{}
1741
1742def valid_command_name(s: String) -> Bool:
1743 name_scan_valid(scan_name(s, 0n, "", True{}, 0n))
1744
1745def use_id(found: Maybe<&2, String>, derived: String) -> String:
1746 match found:
1747 case None{}: derived
1748 case Some{explicit}: explicit
1749
1750def usage_id(parsed: Usage, attrs: List<&2, Item>) -> String:
1751 match parsed:
1752 case Parsed{derived, short, long, kind}:
1753 use_id(find_id(attrs), derived)
1754 case Invalid{col}:
1755 ""
1756
1757def scope_ids(items: List<&2, LintItem>) -> List<&2, String>:
1758 match items:
1759 case Nil{}:
1760 Nil{}
1761 case LintArg{usage, help, attrs, parsed} <> tail:
1762 +name = usage_id(parsed, attrs)
1763 +rest = scope_ids(tail)
1764 Bool.pick(List<&2, String>, String.is_empty(name),
1765 rest, name <> rest)
1766 case head <> tail:
1767 scope_ids(tail)
1768
1769type Seen is Data:
1770 SeenArg{id: String, short: String, long: String}
1771 SeenSub{name: String}
1772 SeenArgAlias{name: String}
1773 SeenSubAlias{name: String}
1774
1775# String.eq, without rebuilding both strings to hand them back; the walk
1776# stops at the first difference.
1777def same_go(a: String, b: String, same: Bool) -> Bool:
1778 match a b same:
1779 case SNil{} SNil{} done: done
1780 case SCon{x, xt} SCon{y, yt} True{}: same_go(xt, yt, Char.is_eq(x, y))
1781 case _ _ _: False{}
1782
1783def same_text(a: String, b: String) -> Bool:
1784 same_go(a, b, True{})
1785
1786def same_nonempty(+a: String, b: String) -> Bool:
1787 Bool.and(Bool.not(String.is_empty(a)), same_text(a, b))
1788
1789def arg_collision(seen: List<&2, Seen>, +id: String,
1790 +short: String, +long: String) -> Maybe<&2, String>:
1791 match seen:
1792 case Nil{}:
1793 None{}
1794 case SeenArg{old_id, old_short, old_long} <> tail:
1795 here = Bool.pick(Maybe<&2, String>, same_nonempty(short, old_short),
1796 Some{short}, Bool.pick(Maybe<&2, String>,
1797 same_nonempty(long, old_long), Some{long},
1798 Bool.pick(Maybe<&2, String>, same_text(id, old_id), Some{id}, None{})))
1799 Maybe.or(&2, String, here, arg_collision(tail, id, short, long))
1800 case SeenArgAlias{+old} <> tail:
1801 Bool.pick(Maybe<&2, String>, same_nonempty(short, old),
1802 Some{short}, Bool.pick(Maybe<&2, String>,
1803 same_nonempty(long, old), Some{long},
1804 arg_collision(tail, id, short, long)))
1805 case head <> tail:
1806 arg_collision(tail, id, short, long)
1807
1808def sub_collision(seen: List<&2, Seen>, +name: String) -> Bool:
1809 match seen:
1810 case Nil{}:
1811 False{}
1812 case SeenSub{old} <> tail:
1813 Bool.or(same_text(name, old), sub_collision(tail, name))
1814 case SeenSubAlias{old} <> tail:
1815 Bool.or(same_text(name, old), sub_collision(tail, name))
1816 case head <> tail:
1817 sub_collision(tail, name)
1818
1819def arg_alias_collision(seen: List<&2, Seen>, +name: String) -> Bool:
1820 match seen:
1821 case Nil{}: False{}
1822 case SeenArg{id, short, long} <> tail:
1823 Bool.or(Bool.or(same_nonempty(name, short),
1824 same_nonempty(name, long)), arg_alias_collision(tail, name))
1825 case SeenArgAlias{old} <> tail:
1826 Bool.or(same_text(name, old), arg_alias_collision(tail, name))
1827 case head <> tail:
1828 arg_alias_collision(tail, name)
1829
1830def maybe_dup(found: Maybe<&2, String>, at: String) -> List<&2, Problem>:
1831 match found:
1832 case None{}: Nil{}
1833 case Some{name}: [Dup{at, name}]
1834
1835def when_problem(cond: Bool, problem: Problem) -> List<&2, Problem>:
1836 Bool.pick(List<&2, Problem>, cond, [problem], Nil{})
1837
1838def arg_alias_issue(+name: String, +at: String,
1839 seen: List<&2, Seen>, version: Bool) -> List<&2, Problem>:
1840 Bool.pick(List<&2, Problem>, valid_arg_alias(name),
1841 Bool.pick(List<&2, Problem>, Bool.or(String.eq(name, "--help"),
1842 Bool.and(version, String.eq(name, "--version"))),
1843 [Reserved{at, name}],
1844 when_problem(arg_alias_collision(seen, name), Dup{at, name})),
1845 [BadName{at, name}])
1846
1847def arg_alias_issues(aliases: List<&2, String>, +at: String,
1848 +seen: List<&2, Seen>, +version: Bool) -> List<&2, Problem>:
1849 match aliases:
1850 case Nil{}: Nil{}
1851 case +name <> tail:
1852 List.append(&2, Problem,
1853 arg_alias_issue(name, at, seen, version),
1854 arg_alias_issues(tail, at, SeenArgAlias{name} <> seen, version))
1855
1856def arg_alias_entries(aliases: List<&2, String>) -> List<&2, Seen>:
1857 match aliases:
1858 case Nil{}: Nil{}
1859 case name <> tail:
1860 SeenArgAlias{name} <> arg_alias_entries(tail)
1861
1862def arg_entries(parsed: Usage, +attrs: List<&2, Item>) -> List<&2, Seen>:
1863 match parsed:
1864 case Parsed{derived, short, long, kind}:
1865 SeenArg{use_id(find_id(attrs), derived), short, long} <>
1866 arg_alias_entries(find_aliases(attrs))
1867 case Invalid{col}:
1868 Nil{}
1869
1870def has_global(items: List<&2, Item>) -> Bool:
1871 match items:
1872 case Nil{}: False{}
1873 case Global{} <> tail: True{}
1874 case head <> tail: has_global(tail)
1875
1876def global_entries(items: List<&2, LintItem>) -> List<&2, Seen>:
1877 match items:
1878 case Nil{}: Nil{}
1879 case LintArg{usage, help, +attrs, parsed} <> tail:
1880 List.append(&2, Seen,
1881 Bool.pick(List<&2, Seen>, has_global(attrs),
1882 arg_entries(parsed, attrs), Nil{}),
1883 global_entries(tail))
1884 case head <> tail: global_entries(tail)
1885
1886def sub_alias_issue(+name: String, +at: String,
1887 seen: List<&2, Seen>) -> List<&2, Problem>:
1888 Bool.pick(List<&2, Problem>, valid_command_name(name),
1889 Bool.pick(List<&2, Problem>, String.eq(name, "help"),
1890 [Reserved{at, name}],
1891 when_problem(sub_collision(seen, name), Dup{at, name})),
1892 [BadName{at, name}])
1893
1894def sub_alias_issues(aliases: List<&2, String>, +at: String,
1895 +seen: List<&2, Seen>) -> List<&2, Problem>:
1896 match aliases:
1897 case Nil{}: Nil{}
1898 case +name <> tail:
1899 List.append(&2, Problem,
1900 sub_alias_issue(name, at, seen),
1901 sub_alias_issues(tail, at, SeenSubAlias{name} <> seen))
1902
1903def sub_alias_entries(aliases: List<&2, String>) -> List<&2, Seen>:
1904 match aliases:
1905 case Nil{}: Nil{}
1906 case name <> tail:
1907 SeenSubAlias{name} <> sub_alias_entries(tail)
1908
1909def missing_refs(targets: List<&2, String>, +ids: List<&2, String>,
1910 +at: String) -> List<&2, Problem>:
1911 match targets:
1912 case Nil{}:
1913 Nil{}
1914 case +target <> tail:
1915 List.append(&2, Problem,
1916 when_problem(Bool.not(List.contains(~String, ~String.eq, ids, target)),
1917 BadRef{at, target}), missing_refs(tail, ids, at))
1918
1919# A U32 value is what U32.read reads (P22).
1920def valid_u32(s: String) -> Bool:
1921 Maybe.is_some(&2, U32, U32.read(s))
1922
1923# Decimal digits by Nat.read's own test, which Char.is_digit agrees with.
1924def decimal_digits(s: String) -> Bool:
1925 match s:
1926 case SNil{}: True{}
1927 case SCon{Chr{x}, tail}:
1928 Bool.and(U32.is_lt(U32.sub(x, 48), 10), decimal_digits(tail))
1929
1930# A Nat value is what Nat.read reads (P22): digits that, less leading
1931# zeroes, fit under its cap of 2^48-1.
1932def valid_nat(+s: String) -> Bool:
1933 Bool.and(Bool.not(String.is_empty(s)),
1934 Bool.and(decimal_digits(s), Nat.read.fits(Nat.read.trim(s))))
1935
1936def valid_bool(s: String) -> Bool:
1937 List.contains(~String, ~String.eq,
1938 ["true", "false", "yes", "no", "on", "off", "1", "0"],
1939 String.to_lower(s))
1940
1941type DecimalState is Data:
1942 DecStart{}
1943 DecAfterMinus{}
1944 DecInt{}
1945 DecDotFirst{}
1946 DecDotAfterInt{}
1947 DecFrac{}
1948 DecExpStart{}
1949 DecExpSign{}
1950 DecExpDigits{}
1951 DecInvalid{}
1952
1953def is_exponent(+c: Char) -> Bool:
1954 Bool.or(Char.is_eq(c, 'e'), Char.is_eq(c, 'E'))
1955
1956def is_sign(+c: Char) -> Bool:
1957 Bool.or(Char.is_eq(c, '+'), Char.is_eq(c, '-'))
1958
1959def decimal_step(state: DecimalState, +c: Char) -> DecimalState:
1960 match state:
1961 case DecStart{}:
1962 Bool.pick(DecimalState, Char.is_eq(c, '-'), DecAfterMinus{},
1963 Bool.pick(DecimalState, Char.is_digit(c), DecInt{},
1964 Bool.pick(DecimalState, Char.is_eq(c, '.'), DecDotFirst{}, DecInvalid{})))
1965 case DecAfterMinus{}:
1966 Bool.pick(DecimalState, Char.is_digit(c), DecInt{},
1967 Bool.pick(DecimalState, Char.is_eq(c, '.'), DecDotFirst{}, DecInvalid{}))
1968 case DecInt{}:
1969 Bool.pick(DecimalState, Char.is_digit(c), DecInt{},
1970 Bool.pick(DecimalState, Char.is_eq(c, '.'), DecDotAfterInt{},
1971 Bool.pick(DecimalState, is_exponent(c), DecExpStart{}, DecInvalid{})))
1972 case DecDotFirst{}:
1973 Bool.pick(DecimalState, Char.is_digit(c), DecFrac{}, DecInvalid{})
1974 case DecDotAfterInt{}:
1975 Bool.pick(DecimalState, Char.is_digit(c), DecFrac{},
1976 Bool.pick(DecimalState, is_exponent(c), DecExpStart{}, DecInvalid{}))
1977 case DecFrac{}:
1978 Bool.pick(DecimalState, Char.is_digit(c), DecFrac{},
1979 Bool.pick(DecimalState, is_exponent(c), DecExpStart{}, DecInvalid{}))
1980 case DecExpStart{}:
1981 Bool.pick(DecimalState, is_sign(c), DecExpSign{},
1982 Bool.pick(DecimalState, Char.is_digit(c), DecExpDigits{}, DecInvalid{}))
1983 case DecExpSign{}:
1984 Bool.pick(DecimalState, Char.is_digit(c), DecExpDigits{}, DecInvalid{})
1985 case DecExpDigits{}:
1986 Bool.pick(DecimalState, Char.is_digit(c), DecExpDigits{}, DecInvalid{})
1987 case DecInvalid{}:
1988 DecInvalid{}
1989
1990def decimal_accept(state: DecimalState) -> Bool:
1991 match state:
1992 case DecInt{}: True{}
1993 case DecDotAfterInt{}: True{}
1994 case DecFrac{}: True{}
1995 case DecExpDigits{}: True{}
1996 case _: False{}
1997
1998def decimal_scan(s: String, state: DecimalState) -> Bool:
1999 match s:
2000 case SNil{}: decimal_accept(state)
2001 case SCon{head, tail}:
2002 decimal_scan(tail, decimal_step(state, head))
2003
2004def valid_f32(s: String) -> Bool:
2005 decimal_scan(s, DecStart{})
2006
2007def find_one_of(items: List<&2, Item>) -> Maybe<&2, List<&2, String>>:
2008 match items:
2009 case Nil{}: None{}
2010 case OneOf{values} <> tail: Some{values}
2011 case head <> tail: find_one_of(tail)
2012
2013# The first element of a value that fails to convert, and why.
2014type Bad is Data:
2015 Bad{value: String, reason: String}
2016
2017def find_as(attrs: List<&2, Item>) -> Maybe<&2, ValueType>:
2018 match attrs:
2019 case Nil{}: None{}
2020 case As{kind} <> tail: Some{kind}
2021 case head <> tail: find_as(tail)
2022
2023def type_reason(kind: ValueType, value: String) -> Maybe<&2, String>:
2024 match kind:
2025 case TNat{}:
2026 Bool.pick(Maybe<&2, String>, valid_nat(value), None{},
2027 Some{"expected a whole number from 0 to 281474976710655"})
2028 case TU32{}:
2029 Bool.pick(Maybe<&2, String>, valid_u32(value), None{},
2030 Some{"expected a whole number from 0 to 4294967295"})
2031 case TF32{}:
2032 Bool.pick(Maybe<&2, String>, valid_f32(value), None{},
2033 Some{"expected a decimal number"})
2034 case TBool{}:
2035 Bool.pick(Maybe<&2, String>, valid_bool(value), None{},
2036 Some{"expected true or false"})
2037 case TFile{}: None{}
2038 case TDir{}: None{}
2039
2040def quoted_choices(values: List<&2, String>) -> List<&2, String>:
2041 match values:
2042 case Nil{}: Nil{}
2043 case head <> tail:
2044 String.concat(["'", head, "'"]) <> quoted_choices(tail)
2045
2046def choices_reason(values: List<&2, String>) -> String:
2047 String.append("expected one of ",
2048 String.join(quoted_choices(values), ", "))
2049
2050def choice_reason(found: Maybe<&2, List<&2, String>>,
2051 value: String) -> Maybe<&2, String>:
2052 match found:
2053 case None{}: None{}
2054 case Some{+values}:
2055 Bool.pick(Maybe<&2, String>,
2056 List.contains(~String, ~String.eq, values, value),
2057 None{}, Some{choices_reason(values)})
2058
2059def reason_from_type(found: Maybe<&2, ValueType>,
2060 value: String) -> Maybe<&2, String>:
2061 match found:
2062 case None{}: None{}
2063 case Some{kind}: type_reason(kind, value)
2064
2065def first_reason(found: Maybe<&2, String>,
2066 other: Maybe<&2, String>) -> Maybe<&2, String>:
2067 match found:
2068 case None{}: other
2069 case Some{reason}: Some{reason}
2070
2071def value_reason(+attrs: List<&2, Item>, +value: String) ->
2072 Maybe<&2, String>:
2073 first_reason(reason_from_type(find_as(attrs), value),
2074 choice_reason(find_one_of(attrs), value))
2075
2076def bad_value(reason: Maybe<&2, String>, value: String) ->
2077 Maybe<&2, Bad>:
2078 match reason:
2079 case None{}: None{}
2080 case Some{why}: Some{Bad{value, why}}
2081
2082def first_bad(values: List<&2, String>, +attrs: List<&2, Item>) ->
2083 Maybe<&2, Bad>:
2084 match values:
2085 case Nil{}: None{}
2086 case +head <> tail:
2087 Maybe.or(&2, Bad, bad_value(value_reason(attrs, head), head),
2088 first_bad(tail, attrs))
2089
2090def split_bad(found: Maybe<&2, Char>, attrs: List<&2, Item>,
2091 +value: String) -> Maybe<&2, Bad>:
2092 match found:
2093 case None{}: bad_value(value_reason(attrs, value), value)
2094 case Some{sep}: first_bad(String.split(value, sep), attrs)
2095
2096# A delimited value splits first; the first element that fails (§8.2).
2097def value_bad(+attrs: List<&2, Item>, value: String) -> Maybe<&2, Bad>:
2098 split_bad(find_delim(attrs), attrs, value)
2099
2100# A value from env or a default converts each element on its own (§8.2).
2101def value_issue(kind: UsageKind, attrs: List<&2, Item>,
2102 +value: String) -> Maybe<&2, Bad>:
2103 match kind:
2104 case FlagKind{}: bad_value(type_reason(TBool{}, value), value)
2105 case NegatableKind{}: bad_value(type_reason(TBool{}, value), value)
2106 case CountKind{}: bad_value(type_reason(TNat{}, value), value)
2107 case _: value_bad(attrs, value)
2108
2109def bad_default(required: Bool, issue: Maybe<&2, Bad>, value: String,
2110 id: String, at: String) -> List<&2, Problem>:
2111 match required issue:
2112 case True{} _: [BadDefault{at, id, value}]
2113 case False{} Some{Bad{bad, why}}: [BadDefault{at, id, bad}]
2114 case False{} None{}: Nil{}
2115
2116def check_default(found: Maybe<&2, String>, kind: UsageKind,
2117 +attrs: List<&2, Item>, id: String, at: String) -> List<&2, Problem>:
2118 match found:
2119 case None{}:
2120 Nil{}
2121 case Some{+value}:
2122 bad_default(has_required(attrs), value_issue(kind, attrs, value),
2123 value, id, at)
2124
2125# The first choice the arg's type rejects; after a hit it only walks the rest.
2126def choice_bad(values: List<&2, String>, +found: Maybe<&2, ValueType>,
2127 bad: Maybe<&2, Bad>) -> Maybe<&2, Bad>:
2128 match values bad:
2129 case Nil{} hit: hit
2130 case value <> tail Some{hit}: Some{hit}
2131 case +value <> tail None{}:
2132 choice_bad(tail, found, bad_value(reason_from_type(found, value), value))
2133
2134def bad_choice(found: Maybe<&2, Bad>, id: String, at: String) ->
2135 List<&2, Problem>:
2136 match found:
2137 case None{}: Nil{}
2138 case Some{Bad{value, why}}: [BadChoice{at, id, value}]
2139
2140# Parse checks a value's type before its choices, so a choice the type
2141# rejects could never be given.
2142def check_choices(found: Maybe<&2, List<&2, String>>,
2143 kind: Maybe<&2, ValueType>, id: String, at: String) -> List<&2, Problem>:
2144 match found:
2145 case None{}: Nil{}
2146 case Some{values}: bad_choice(choice_bad(values, kind, None{}), id, at)
2147
2148def positional_issue(+kind: UsageKind, +id: String,
2149 attrs: List<&2, Item>, rest_items: List<&2, LintItem>,
2150 +seen_optional: Bool, +seen_variadic: Bool, +at: String) ->
2151 List<&2, Problem>:
2152 Bool.pick(List<&2, Problem>,
2153 Bool.and(seen_optional, kind_required_pos(kind)),
2154 [BadPositional{at, id, "required positional follows an optional positional"}],
2155 Bool.pick(List<&2, Problem>,
2156 Bool.and(seen_variadic, kind_variadic_pos(kind)),
2157 [BadPositional{at, id, "more than one variadic positional"}],
2158 Bool.pick(List<&2, Problem>,
2159 Bool.and(seen_variadic, kind_optional_pos(kind)),
2160 [BadPositional{at, id, "optional positional follows a variadic positional"}],
2161 when_problem(Bool.and(kind_variadic_pos(kind),
2162 Bool.and(has_rest(attrs), has_pos_item(rest_items))),
2163 BadPositional{at, id, "rest must be on the last positional"}))))
2164
2165def bare_issue(+kind: UsageKind, id: String, short: String,
2166 +attrs: List<&2, Item>, at: String) -> List<&2, Problem>:
2167 +bare_kind = kind_bare(kind)
2168 +has = has_bare(attrs)
2169 when_problem(Bool.or(
2170 Bool.and(bare_kind, Bool.or(Bool.not(has),
2171 Bool.not(String.is_empty(short)))),
2172 Bool.and(Bool.not(bare_kind), has)), NeedBare{at, id})
2173
2174def misplaced_arg(+kind: UsageKind, +attrs: List<&2, Item>,
2175 +at: String) -> List<&2, Problem>:
2176 List.append(&2, Problem, placement_issues(attrs, kind, at),
2177 duplicate_type_issues(value_type_names(attrs), at))
2178
2179def arg_reserved_dup(+at: String, +long: String, seen: List<&2, Seen>,
2180 id: String, short: String, version: Bool) -> List<&2, Problem>:
2181 reserved = Bool.or(String.eq(long, "--help"),
2182 Bool.and(version, String.eq(long, "--version")))
2183 Bool.pick(List<&2, Problem>, reserved,
2184 [Reserved{at, long}], maybe_dup(arg_collision(seen, id, short, long), at))
2185
2186def arg_issues(+kind: UsageKind, +id: String, +short: String,
2187 +long: String, usage: String, help: String, +attrs: List<&2, Item>,
2188 remaining: List<&2, LintItem>, +at: String, ids: List<&2, String>,
2189 seen: List<&2, Seen>, optional: Bool, variadic: Bool,
2190 version: Bool) -> List<&2, Problem>:
2191 List.concat(&2, Problem, [
2192 arg_reserved_dup(at, long, seen, id, short, version),
2193 misplaced_arg(kind, attrs, at),
2194 when_problem(Bool.and(String.is_empty(help),
2195 Bool.not(has_hidden(attrs))), NoHelp{at, usage}),
2196 check_default(find_default(attrs), kind, attrs, id, at),
2197 missing_refs(List.append(&2, String,
2198 find_requires(attrs), find_conflicts(attrs)), ids, at),
2199 positional_issue(kind, id, attrs, remaining, optional, variadic, at),
2200 when_problem(bad_choices(attrs), BadChoices{at, id}),
2201 check_choices(find_one_of(attrs), find_as(attrs), id, at),
2202 bare_issue(kind, id, short, attrs, at),
2203 ])
2204
2205def arg_problems(parsed: Usage, +usage: String, help: String,
2206 +attrs: List<&2, Item>, remaining: List<&2, LintItem>, +at: String,
2207 ids: List<&2, String>, +seen: List<&2, Seen>,
2208 optional: Bool, variadic: Bool, +version: Bool) -> List<&2, Problem>:
2209 match parsed:
2210 case Invalid{col}:
2211 List.append(&2, Problem, [BadUsage{at, usage, col}],
2212 when_problem(Bool.and(String.is_empty(help),
2213 Bool.not(has_hidden(attrs))), NoHelp{at, usage}))
2214 case Parsed{derived, +short, +long, +kind}:
2215 +id = use_id(find_id(attrs), derived)
2216 +aliases = find_aliases(attrs)
2217 List.append(&2, Problem,
2218 arg_issues(kind, id, short, long, usage, help, attrs,
2219 remaining, at, ids, seen, optional, variadic, version),
2220 arg_alias_issues(aliases, at, SeenArg{id, short, long} <> seen,
2221 version))
2222
2223def has_external(items: List<&2, Item>) -> Bool:
2224 match items:
2225 case Nil{}: False{}
2226 case External{} <> tail: True{}
2227 case head <> tail: has_external(tail)
2228
2229def has_sub_optional(items: List<&2, Item>) -> Bool:
2230 match items:
2231 case Nil{}: False{}
2232 case SubOptional{} <> tail: True{}
2233 case head <> tail: has_sub_optional(tail)
2234
2235# A command that cannot complete without a subcommand (§8.4).
2236def needs_sub(+items: List<&2, Item>) -> Bool:
2237 Bool.and(has_sub_item(items),
2238 Bool.and(Bool.not(has_sub_optional(items)),
2239 Bool.not(has_external(items))))
2240
2241# Each required() among attrs, as Misplaced at at.
2242def required_misplaced(attrs: List<&2, Item>, +at: String) ->
2243 List<&2, Problem>:
2244 match attrs:
2245 case Nil{}: Nil{}
2246 case Required{} <> tail:
2247 Misplaced{at, "required"} <> required_misplaced(tail, at)
2248 case head <> tail: required_misplaced(tail, at)
2249
2250def dead_required_option(option: Bool, attrs: List<&2, Item>, +at: String) ->
2251 List<&2, Problem>:
2252 match option:
2253 case True{}: required_misplaced(attrs, at)
2254 case False{}: Nil{}
2255
2256# An option's required() that descent drops; a flag's or a positional's is
2257# already misplaced by its kind.
2258def dead_required_kind(parsed: Usage, attrs: List<&2, Item>, +at: String) ->
2259 List<&2, Problem>:
2260 match parsed:
2261 case Parsed{derived, short, long, kind}:
2262 dead_required_option(kind_required_allowed(kind), attrs, at)
2263 case Invalid{col}: Nil{}
2264
2265def dead_required_arg(kept: Bool, usage: String, attrs: List<&2, Item>,
2266 +at: String) -> List<&2, Problem>:
2267 match kept:
2268 case True{}: Nil{}
2269 case False{}: dead_required_kind(parse_usage(usage), attrs, at)
2270
2271def dead_required_items(items: List<&2, Item>, +at: String) ->
2272 List<&2, Problem>:
2273 match items:
2274 case Nil{}: Nil{}
2275 case Arg{usage, help, +attrs} <> tail:
2276 List.append(&2, Problem,
2277 dead_required_arg(Bool.or(has_global(attrs),
2278 Bool.not(has_required(attrs))), usage, attrs, at),
2279 dead_required_items(tail, at))
2280 case head <> tail: dead_required_items(tail, at)
2281
2282# A command that needs a subcommand passes down only its global options'
2283# values (§8.4), so another option's required() can never be met.
2284def dead_required(dead: Bool, items: List<&2, Item>, +at: String) ->
2285 List<&2, Problem>:
2286 match dead:
2287 case True{}: dead_required_items(items, at)
2288 case False{}: Nil{}
2289
2290# has_pos: whether items has a positional.
2291def command_body_issues(+at: String, name: String, about: String,
2292 +items: List<&2, Item>, has_pos: Bool) -> List<&2, Problem>:
2293 List.concat(&2, Problem, [
2294 when_problem(Bool.and(String.is_empty(about),
2295 Bool.not(has_hidden(items))), NoHelp{at, name}),
2296 when_problem(Bool.and(has_pos, has_sub_item(items)), Mixed{at}),
2297 missing_refs(scope_group_rules(items), scope_groups(items), at),
2298 missing_refs(scope_groups(items), scope_group_rules(items), at),
2299 dead_required(needs_sub(items), items, at),
2300 ])
2301
2302def sub_name_issues(+at: String, +name: String,
2303 seen: List<&2, Seen>) -> List<&2, Problem>:
2304 List.append(&2, Problem,
2305 when_problem(Bool.not(valid_command_name(name)), BadName{at, name}),
2306 Bool.pick(List<&2, Problem>, String.eq(name, "help"),
2307 [Reserved{at, name}],
2308 when_problem(sub_collision(seen, name), Dup{at, name})))
2309
2310def lint(items: List<&2, LintItem>, +at: String, +ids: List<&2, String>,
2311 +seen: List<&2, Seen>, +inherited: List<&2, Seen>, +optional: Bool,
2312 +variadic: Bool, +version: Bool, +root: Bool) -> List<&2, Problem>:
2313 match items:
2314 case Nil{}:
2315 Nil{}
2316 case LintArg{+usage, help, +attrs, +parsed} <> +tail:
2317 List.append(&2, Problem,
2318 arg_problems(parsed, usage, help, attrs, tail, at, ids, seen,
2319 optional, variadic, version),
2320 lint(tail, at, ids,
2321 List.append(&2, Seen, arg_entries(parsed, attrs), seen),
2322 inherited,
2323 Bool.or(optional, usage_kind(~kind_optional_pos, parsed)),
2324 Bool.or(variadic, usage_kind(~kind_variadic_pos, parsed)),
2325 version, root))
2326 case LintSub{+name, about, +children, +kids} <> tail:
2327 +child_at = String.concat([at, " ", name])
2328 +aliases = find_aliases(children)
2329 List.concat(&2, Problem, [
2330 sub_name_issues(at, name, seen),
2331 sub_alias_issues(aliases, at, SeenSub{name} <> seen),
2332 command_body_issues(child_at, name, about, children,
2333 has_pos_item(kids)),
2334 lint(kids, child_at, scope_ids(kids), inherited,
2335 List.append(&2, Seen, global_entries(kids), inherited),
2336 False{}, False{}, version, False{}),
2337 lint(tail, at, ids,
2338 List.append(&2, Seen, sub_alias_entries(aliases),
2339 SeenSub{name} <> seen),
2340 inherited, optional, variadic, version, root),
2341 ])
2342 case LintOther{head} <> tail:
2343 List.append(&2, Problem,
2344 maybe_misplaced(command_attr_name(head, root), at),
2345 lint(tail, at, ids, seen, inherited, optional, variadic,
2346 version, root))
2347
2348def problems(c: Cmd) -> List<&2, Problem>:
2349 match c:
2350 case Cmd{+name, about, +items}:
2351 +linted = lint_items(items)
2352 List.concat(&2, Problem, [
2353 when_problem(Bool.not(valid_command_name(name)), BadName{name, name}),
2354 command_body_issues(name, name, about, items, has_pos_item(linted)),
2355 lint(linted, name, scope_ids(linted), Nil{}, global_entries(linted),
2356 False{}, False{}, has_version_item(items), True{}),
2357 ])
2358
2359type Src is Data:
2360 Argv{}
2361 Env{}
2362 Default{}
2363 Absent{}
2364
2365type Var is Data:
2366 Var{name: String, value: String}
2367
2368type Host is Data:
2369 Host{width: Nat, vars: List<&2, Var>, out_tty: Bool, err_tty: Bool}
2370
2371type Field is Data:
2372 Field{id: String, values: List<&2, String>, source: Src}
2373
2374type ScanEvent is Data:
2375 FlagHit{id: String, value: Bool}
2376 CountHit{id: String}
2377 ValueHit{id: String, value: String}
2378 Operand{token: String}
2379 Enter{name: String}
2380 HelpHit{long: Bool}
2381 VersionHit{}
2382
2383type ScanFault is Data:
2384 E01{token: String}
2385 E02{name: String}
2386 E03{arg: String}
2387 E04{arg: String, value: String, reason: String}
2388 E05{arg: String, value: String}
2389 E06{missing: List<&2, String>}
2390 E07{a: String, b: String}
2391 E08{a: String, b: String}
2392 E09{token: String}
2393 E10{}
2394 E11{arg: String, value: String, name: String, reason: String}
2395
2396type ExternalArgs is Data:
2397 ExternalArgs{name: String, args: List<&2, String>}
2398
2399type ScanTrace is Data:
2400 ScanTrace{path: List<&2, String>, events: List<&2, ScanEvent>,
2401 ended: Bool, external: Maybe<&2, ExternalArgs>,
2402 issue: Maybe<&2, ScanFault>}
2403
2404type ParseResult is Data:
2405 Accepted{path: List<&2, String>, fields: List<&2, Field>,
2406 external: Maybe<&2, ExternalArgs>}
2407 Rejected{path: List<&2, String>, fault: ScanFault}
2408 HelpRequested{path: List<&2, String>, long: Bool}
2409 VersionRequested{}
2410 SummaryRequired{}
2411
2412type Early is Data:
2413 EarlyHelp{long: Bool}
2414 EarlyVersion{}
2415
2416# An arg's hits so far: values holds a repeated option's newest first, so
2417# each hit conses, and resolve_argv_gather puts them back in argv order.
2418type Gather is Data:
2419 Gather{seen: Bool, values: List<&2, String>, count: Nat}
2420
2421type FieldBuild is Data:
2422 Built{fields: List<&2, Field>}
2423 BuildFault{fault: ScanFault}
2424
2425type EnvValue is Data:
2426 EnvValue{name: String, value: String}
2427
2428def host(width: Nat) -> Host:
2429 Host{width, [], False{}, False{}}
2430
2431def host_var(h: Host, name: String, value: String) -> Host:
2432 match h:
2433 case Host{width, vars, out_tty, err_tty}:
2434 Host{width, Var{name, value} <> vars, out_tty, err_tty}
2435
2436def var_value(vars: List<&2, Var>, +name: String) -> Maybe<&2, String>:
2437 match vars:
2438 case Nil{}: None{}
2439 case Var{key, value} <> tail:
2440 Bool.pick(Maybe<&2, String>, String.eq(key, name),
2441 Some{value}, var_value(tail, name))
2442
2443def host_value(h: Host, name: String) -> Maybe<&2, String>:
2444 match h:
2445 case Host{width, vars, out_tty, err_tty}:
2446 var_value(vars, name)
2447
2448type ArgInfo is Data:
2449 ArgInfo{id: String, usage: String, short: String, long: String,
2450 kind: UsageKind, attrs: List<&2, Item>}
2451
2452type OptFound is Data:
2453 OptFound{arg: ArgInfo, polarity: Bool}
2454
2455type SubFound is Data:
2456 SubFound{name: String, items: List<&2, Item>}
2457
2458# A subcommand a scope answers to, by name or alias.
2459type SubKey is Data:
2460 SubKey{name: String, aliases: List<&2, String>, children: List<&2, Item>}
2461
2462# An option name a scope answers to, with the match it makes.
2463type OptKey is Data:
2464 OptKey{name: String, found: OptFound}
2465
2466# An item resolve walks: an Arg with its usage parsed once, or another item.
2467type SpecItem is Data:
2468 SpecArg{info: ArgInfo}
2469 SpecOther{item: Item}
2470
2471# A scope with each usage parsed once: its option names (own, then global),
2472# the items resolve walks (global, then own), its positionals and
2473# subcommands, and the globals its subcommands inherit.
2474type Spec is Data:
2475 Spec{keys: List<&2, OptKey>, args: List<&2, SpecItem>,
2476 slots: List<&2, ArgInfo>, variadic: Bool, subs: List<&2, SubKey>,
2477 has_sub: Bool, external: Bool, needs_sub: Bool, version: Bool,
2478 digit_short: Bool, inherit: List<&2, SpecItem>}
2479
2480type Scope is Data:
2481 Scope{path: List<&2, String>, spec: Spec}
2482
2483type ScanState is Data:
2484 ScanState{scope: Scope, events: List<&2, ScanEvent>,
2485 resting: Bool, positional_count: Nat,
2486 external: Maybe<&2, ExternalArgs>, issue: Maybe<&2, ScanFault>}
2487
2488# Phases after which no token is an operand and `--` is plain.
2489type Tail is Data:
2490 Faulted{state: ScanState}
2491 Walk{state: ScanState}
2492 Stopped{state: ScanState}
2493
2494type Mode is Data:
2495 Open{state: ScanState}
2496 Fetch{state: ScanState, info: ArgInfo}
2497 Operands{state: ScanState, ended: Bool}
2498 Capture{state: ScanState, name: String}
2499 Closed{tail: Tail}
2500
2501def arg_info_parsed(parsed: Usage, usage: String,
2502 +attrs: List<&2, Item>) -> ArgInfo:
2503 match parsed:
2504 case Parsed{derived, short, long, kind}:
2505 ArgInfo{use_id(find_id(attrs), derived), usage, short, long,
2506 kind, attrs}
2507 case Invalid{col}:
2508 ArgInfo{"", usage, "", "", FlagKind{}, attrs}
2509
2510def arg_info(+usage: String, attrs: List<&2, Item>) -> ArgInfo:
2511 arg_info_parsed(parse_usage(usage), usage, attrs)
2512
2513def arg_display(info: ArgInfo) -> String:
2514 match info:
2515 case ArgInfo{id, +usage, +short, long, kind, attrs}:
2516 Bool.pick(String, Bool.and(Bool.not(String.is_empty(short)),
2517 Bool.not(String.is_empty(long))),
2518 String.drop(usage, Nat.add(String.length(short), 2n)), usage)
2519
2520def short_digit(short: String) -> Bool:
2521 match short:
2522 case SCon{Chr{45}, SCon{head, tail}}: Char.is_digit(head)
2523 case _: False{}
2524
2525def info_attrs(info: ArgInfo) -> List<&2, Item>:
2526 match info:
2527 case ArgInfo{id, usage, short, long, kind, attrs}: attrs
2528
2529def info_kind(info: ArgInfo) -> UsageKind:
2530 match info:
2531 case ArgInfo{id, usage, short, long, kind, attrs}: kind
2532
2533def info_id(info: ArgInfo) -> String:
2534 match info:
2535 case ArgInfo{id, usage, short, long, kind, attrs}: id
2536
2537def scope_path(scope: Scope) -> List<&2, String>:
2538 match scope:
2539 case Scope{path, spec}: path
2540
2541def arg_in_group(attrs: List<&2, Item>, name: String) -> Bool:
2542 List.contains(~String, ~String.eq,
2543 find_group_attrs(attrs), name)
2544
2545# Whether each arg items put in group name is global().
2546def group_global(items: List<&2, Item>, +name: String) -> Bool:
2547 match items:
2548 case Nil{}: True{}
2549 case Arg{usage, help, +attrs} <> tail:
2550 Bool.and(Bool.or(Bool.not(arg_in_group(attrs, name)), has_global(attrs)),
2551 group_global(tail, name))
2552 case head <> tail: group_global(tail, name)
2553
2554# What items pass down to their subcommands, in their order: each global()
2555# arg, and each exclusive or need_one rule over a group whose args in all are
2556# each global(), so that it holds in every subcommand below.
2557def global_go(items: List<&2, Item>, +all: List<&2, Item>) ->
2558 List<&2, Item>:
2559 match items:
2560 case Nil{}: Nil{}
2561 case Arg{usage, help, +attrs} <> tail:
2562 +rest = global_go(tail, all)
2563 Bool.pick(List<&2, Item>, has_global(attrs),
2564 Arg{usage, help, attrs} <> rest, rest)
2565 case Exclusive{+name} <> tail:
2566 +rest = global_go(tail, all)
2567 Bool.pick(List<&2, Item>, group_global(all, name),
2568 Exclusive{name} <> rest, rest)
2569 case NeedOne{+name} <> tail:
2570 +rest = global_go(tail, all)
2571 Bool.pick(List<&2, Item>, group_global(all, name),
2572 NeedOne{name} <> rest, rest)
2573 case head <> tail: global_go(tail, all)
2574
2575def global_items(+items: List<&2, Item>) -> List<&2, Item>:
2576 global_go(items, items)
2577
2578def kind_negatable(kind: UsageKind) -> Bool:
2579 match kind:
2580 case NegatableKind{}: True{}
2581 case _: False{}
2582
2583def negated_long(long: String) -> String:
2584 String.append("--no-", String.drop(long, 2n))
2585
2586def option_matches(+query: String, info: ArgInfo) ->
2587 Maybe<&2, OptFound>:
2588 match info:
2589 case ArgInfo{id, usage, +short, +long, +kind, +attrs}:
2590 +named = Bool.or(
2591 Bool.and(Bool.not(String.is_empty(short)), String.eq(query, short)),
2592 Bool.or(Bool.and(Bool.not(String.is_empty(long)),
2593 String.eq(query, long)),
2594 List.contains(~String, ~String.eq, find_aliases(attrs), query)))
2595 +negated = Bool.and(kind_negatable(kind),
2596 Bool.and(Bool.not(String.is_empty(long)),
2597 String.eq(query, negated_long(long))))
2598 Bool.pick(Maybe<&2, OptFound>, Bool.or(named, negated),
2599 Some{OptFound{ArgInfo{id, usage, short, long, kind, attrs},
2600 Bool.not(negated)}}, None{})
2601
2602def find_option(items: List<&2, Item>, +query: String) ->
2603 Maybe<&2, OptFound>:
2604 match items:
2605 case Nil{}: None{}
2606 case Arg{usage, help, attrs} <> tail:
2607 +found = option_matches(query, arg_info(usage, attrs))
2608 Bool.pick(Maybe<&2, OptFound>,
2609 Maybe.is_some(&2, OptFound, found), found,
2610 find_option(tail, query))
2611 case head <> tail: find_option(tail, query)
2612
2613def key_found(key: OptKey, query: String) -> Maybe<&2, OptFound>:
2614 match key:
2615 case OptKey{name, found}:
2616 Bool.pick(Maybe<&2, OptFound>, same_text(query, name),
2617 Some{found}, None{})
2618
2619# The first key named query; after a hit it only walks the rest.
2620def find_key(keys: List<&2, OptKey>, +query: String,
2621 found: Maybe<&2, OptFound>) -> Maybe<&2, OptFound>:
2622 match keys found:
2623 case Nil{} hit: hit
2624 case key <> tail Some{hit}: Some{hit}
2625 case key <> tail None{}: find_key(tail, query, key_found(key, query))
2626
2627# A scope's own args answer before its globals, as in find_option.
2628def find_option_scope(scope: Scope, query: String) ->
2629 Maybe<&2, OptFound>:
2630 match scope:
2631 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2632 external, needs_sub, version, digit_short, inherit}}:
2633 find_key(keys, query, None{})
2634
2635# Whether items alias query; after a hit it walks no further.
2636def has_alias(items: List<&2, Item>, +query: String, found: Bool) -> Bool:
2637 match items found:
2638 case _ True{}: True{}
2639 case Nil{} False{}: False{}
2640 case Alias{name} <> tail False{}:
2641 has_alias(tail, query, same_text(name, query))
2642 case head <> tail False{}: has_alias(tail, query, False{})
2643
2644def alias_hit(aliased: Bool, name: String, children: List<&2, Item>) ->
2645 Maybe<&2, SubFound>:
2646 match aliased:
2647 case True{}: Some{SubFound{name, children}}
2648 case False{}: None{}
2649
2650def sub_hit(same: Bool, name: String, +children: List<&2, Item>,
2651 query: String) -> Maybe<&2, SubFound>:
2652 match same:
2653 case True{}: Some{SubFound{name, children}}
2654 case False{}:
2655 alias_hit(has_alias(children, query, False{}), name, children)
2656
2657def sub_found(item: Item, +query: String) -> Maybe<&2, SubFound>:
2658 match item:
2659 case Sub{+name, about, children}:
2660 sub_hit(same_text(query, name), name, children, query)
2661 case _: None{}
2662
2663# The first Sub named or aliased query; after a hit it walks no further.
2664def find_sub_go(items: List<&2, Item>, +query: String,
2665 found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
2666 match items found:
2667 case _ Some{hit}: Some{hit}
2668 case Nil{} None{}: None{}
2669 case item <> tail None{}: find_sub_go(tail, query, sub_found(item, query))
2670
2671def find_sub(items: List<&2, Item>, +query: String) ->
2672 Maybe<&2, SubFound>:
2673 find_sub_go(items, query, None{})
2674
2675def positional_items(items: List<&2, Item>) -> List<&2, Item>:
2676 match items:
2677 case Nil{}: Nil{}
2678 case Arg{+usage, +help, +attrs} <> tail:
2679 +rest = positional_items(tail)
2680 Bool.pick(List<&2, Item>,
2681 usage_kind(~kind_pos, parse_usage(usage)),
2682 Arg{usage, help, attrs} <> rest, rest)
2683 case head <> tail: positional_items(tail)
2684
2685def has_variadic_pos(items: List<&2, Item>) -> Bool:
2686 match items:
2687 case Nil{}: False{}
2688 case Arg{usage, help, attrs} <> tail:
2689 Bool.or(usage_kind(~kind_variadic_pos, parse_usage(usage)),
2690 has_variadic_pos(tail))
2691 case head <> tail: has_variadic_pos(tail)
2692
2693def positional_can_take(items: List<&2, Item>, count: Nat) -> Bool:
2694 +slots = positional_items(items)
2695 Bool.or(Nat.is_lt(count, List.length(&2, Item, slots)),
2696 has_variadic_pos(slots))
2697
2698def next_slot(slots: List<&2, ArgInfo>, +count: Nat) ->
2699 Maybe<&2, ArgInfo>:
2700 match slots:
2701 case Nil{}: None{}
2702 case +info <> tail:
2703 Bool.pick(Maybe<&2, ArgInfo>,
2704 Bool.or(Nat.is_eq(count, 0n), kind_variadic_pos(info_kind(info))),
2705 Some{info}, next_slot(tail, Nat.sub(count, 1n)))
2706
2707def scope_next_slot(scope: Scope, count: Nat) -> Maybe<&2, ArgInfo>:
2708 match scope:
2709 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2710 external, needs_sub, version, digit_short, inherit}}:
2711 next_slot(slots, count)
2712
2713# name as a key of info; only info's --no- long clears the flag, as in
2714# option_matches.
2715def opt_key(+name: String, info: ArgInfo) -> OptKey:
2716 match info:
2717 case ArgInfo{id, usage, short, +long, +kind, attrs}:
2718 OptKey{name, OptFound{ArgInfo{id, usage, short, long, kind, attrs},
2719 Bool.not(Bool.and(kind_negatable(kind),
2720 Bool.and(Bool.not(String.is_empty(long)),
2721 String.eq(name, negated_long(long)))))}}
2722
2723def name_keys(names: List<&2, String>, +info: ArgInfo) ->
2724 List<&2, OptKey>:
2725 match names:
2726 case Nil{}: Nil{}
2727 case name <> tail: opt_key(name, info) <> name_keys(tail, info)
2728
2729def nonempty_name(+name: String) -> List<&2, String>:
2730 Bool.pick(List<&2, String>, String.is_empty(name), [], [name])
2731
2732# The names option_matches accepts for info, in its order.
2733def info_names(info: ArgInfo) -> List<&2, String>:
2734 match info:
2735 case ArgInfo{id, usage, short, +long, kind, attrs}:
2736 List.append(&2, String, nonempty_name(short),
2737 List.append(&2, String, nonempty_name(long),
2738 List.append(&2, String, find_aliases(attrs),
2739 Bool.pick(List<&2, String>, Bool.and(kind_negatable(kind),
2740 Bool.not(String.is_empty(long))),
2741 [negated_long(long)], []))))
2742
2743def spec_keys(items: List<&2, SpecItem>) -> List<&2, OptKey>:
2744 match items:
2745 case Nil{}: Nil{}
2746 case SpecArg{+info} <> tail:
2747 List.append(&2, OptKey, name_keys(info_names(info), info),
2748 spec_keys(tail))
2749 case head <> tail: spec_keys(tail)
2750
2751# items less their Subs, each Arg's usage parsed once.
2752def spec_items(items: List<&2, Item>) -> List<&2, SpecItem>:
2753 match items:
2754 case Nil{}: Nil{}
2755 case Arg{usage, help, attrs} <> tail:
2756 SpecArg{arg_info(usage, attrs)} <> spec_items(tail)
2757 case Sub{name, about, children} <> tail: spec_items(tail)
2758 case head <> tail: SpecOther{head} <> spec_items(tail)
2759
2760# global_go over items parsed, with all as written.
2761def global_spec(items: List<&2, SpecItem>, +all: List<&2, Item>) ->
2762 List<&2, SpecItem>:
2763 match items:
2764 case Nil{}: Nil{}
2765 case SpecArg{+info} <> tail:
2766 +rest = global_spec(tail, all)
2767 attrs = info_attrs(info)
2768 Bool.pick(List<&2, SpecItem>, has_global(attrs),
2769 SpecArg{info} <> rest, rest)
2770 case SpecOther{Exclusive{+name}} <> tail:
2771 +rest = global_spec(tail, all)
2772 Bool.pick(List<&2, SpecItem>, group_global(all, name),
2773 SpecOther{Exclusive{name}} <> rest, rest)
2774 case SpecOther{NeedOne{+name}} <> tail:
2775 +rest = global_spec(tail, all)
2776 Bool.pick(List<&2, SpecItem>, group_global(all, name),
2777 SpecOther{NeedOne{name}} <> rest, rest)
2778 case head <> tail: global_spec(tail, all)
2779
2780def spec_slots(items: List<&2, SpecItem>) -> List<&2, ArgInfo>:
2781 match items:
2782 case Nil{}: Nil{}
2783 case SpecArg{+info} <> tail:
2784 +rest = spec_slots(tail)
2785 Bool.pick(List<&2, ArgInfo>, kind_pos(info_kind(info)),
2786 info <> rest, rest)
2787 case head <> tail: spec_slots(tail)
2788
2789def slots_variadic(slots: List<&2, ArgInfo>) -> Bool:
2790 match slots:
2791 case Nil{}: False{}
2792 case info <> tail:
2793 Bool.or(kind_variadic_pos(info_kind(info)), slots_variadic(tail))
2794
2795def info_digit_short(info: ArgInfo) -> Bool:
2796 match info:
2797 case ArgInfo{id, usage, short, long, kind, attrs}: short_digit(short)
2798
2799def spec_digit_short(items: List<&2, SpecItem>) -> Bool:
2800 match items:
2801 case Nil{}: False{}
2802 case SpecArg{info} <> tail:
2803 Bool.or(info_digit_short(info), spec_digit_short(tail))
2804 case head <> tail: spec_digit_short(tail)
2805
2806def raw_subs(items: List<&2, Item>) -> List<&2, SubKey>:
2807 match items:
2808 case Nil{}: Nil{}
2809 case Sub{name, about, +children} <> tail:
2810 SubKey{name, find_aliases(children), children} <> raw_subs(tail)
2811 case head <> tail: raw_subs(tail)
2812
2813# items as a scope inheriting globals; its subcommands stay unparsed until
2814# the scan enters one.
2815def spec_node(+items: List<&2, Item>, +globals: List<&2, SpecItem>) ->
2816 Spec:
2817 +own = spec_items(items)
2818 +args = List.append(&2, SpecItem, globals, own)
2819 +slots = spec_slots(own)
2820 Spec{List.append(&2, OptKey, spec_keys(own), spec_keys(globals)),
2821 args, slots, slots_variadic(slots), raw_subs(items),
2822 has_sub_item(items), has_external(items), needs_sub(items),
2823 has_version_item(items), spec_digit_short(args),
2824 List.append(&2, SpecItem, global_spec(own, items), globals)}
2825
2826def scan_start(c: Cmd) -> ScanState:
2827 match c:
2828 case Cmd{name, about, items}:
2829 ScanState{Scope{[], spec_node(items, [])}, [], False{}, 0n, None{},
2830 None{}}
2831
2832def scan_event(state: ScanState, event: ScanEvent) -> ScanState:
2833 match state:
2834 case ScanState{scope, events, resting, positional_count,
2835 external, issue}:
2836 ScanState{scope, event <> events, resting,
2837 positional_count, external, issue}
2838
2839def first_issue(issue: Maybe<&2, ScanFault>, fault: ScanFault) ->
2840 Maybe<&2, ScanFault>:
2841 match issue:
2842 case None{}: Some{fault}
2843 case Some{old}: Some{old}
2844
2845def scan_error(state: ScanState, fault: ScanFault) -> ScanState:
2846 match state:
2847 case ScanState{scope, events, resting, positional_count,
2848 external, issue}:
2849 ScanState{scope, events, resting, positional_count,
2850 external, first_issue(issue, fault)}
2851
2852def has_hyphen(items: List<&2, Item>) -> Bool:
2853 match items:
2854 case Nil{}: False{}
2855 case Hyphen{} <> tail: True{}
2856 case head <> tail: has_hyphen(tail)
2857
2858def slot_rest(found: Maybe<&2, ArgInfo>) -> Bool:
2859 match found:
2860 case None{}: False{}
2861 case Some{ArgInfo{id, usage, short, long, kind, attrs}}:
2862 has_rest(attrs)
2863
2864def slot_hyphen(found: Maybe<&2, ArgInfo>) -> Bool:
2865 match found:
2866 case None{}: False{}
2867 case Some{ArgInfo{id, usage, short, long, kind, attrs}}:
2868 has_hyphen(attrs)
2869
2870def has_as_f32(items: List<&2, Item>) -> Bool:
2871 match items:
2872 case Nil{}: False{}
2873 case As{TF32{}} <> tail: True{}
2874 case head <> tail: has_as_f32(tail)
2875
2876def slot_f32(found: Maybe<&2, ArgInfo>) -> Bool:
2877 match found:
2878 case None{}: False{}
2879 case Some{ArgInfo{id, usage, short, long, kind, attrs}}:
2880 has_as_f32(attrs)
2881
2882def is_negative_number(+s: String) -> Bool:
2883 Bool.and(String.starts_with(s, "-"), valid_f32(s))
2884
2885def before_equal(s: String) -> String:
2886 match s:
2887 case SNil{}: SNil{}
2888 case SCon{Chr{61}, tail}: SNil{}
2889 case SCon{head, tail}: SCon{head, before_equal(tail)}
2890
2891def after_equal(s: String) -> Maybe<&2, String>:
2892 match s:
2893 case SNil{}: None{}
2894 case SCon{Chr{61}, tail}: Some{tail}
2895 case SCon{head, tail}: after_equal(tail)
2896
2897def first_option_name(+token: String) -> String:
2898 Bool.pick(String, String.starts_with(token, "--"),
2899 before_equal(token), String.take(token, 2n))
2900
2901def scope_option_known(scope: Scope, token: String) -> Bool:
2902 Maybe.is_some(&2, OptFound,
2903 find_option_scope(scope, first_option_name(token)))
2904
2905def scope_builtin_known(scope: Scope, +token: String) -> Bool:
2906 match scope:
2907 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2908 external, needs_sub, version, digit_short, inherit}}:
2909 +name = first_option_name(token)
2910 Bool.or(String.eq(name, "-h"),
2911 Bool.or(String.eq(name, "--help"),
2912 Bool.and(List.is_empty(&2, String, path),
2913 Bool.and(version,
2914 Bool.or(String.eq(name, "-V"),
2915 String.eq(name, "--version"))))))
2916
2917def scope_any_option_known(+scope: Scope, +token: String) -> Bool:
2918 Bool.or(scope_option_known(scope, token),
2919 scope_builtin_known(scope, token))
2920
2921def scan_operand_ok(state: ScanState, token: String) -> ScanState:
2922 match state:
2923 case ScanState{+scope, events, resting, +positional_count,
2924 external, issue}:
2925 ScanState{scope, Operand{token} <> events,
2926 Bool.or(resting,
2927 slot_rest(scope_next_slot(scope, positional_count))),
2928 Nat.add(positional_count, 1n), external, issue}
2929
2930def scan_operand_case(allowed: Bool, state: ScanState,
2931 token: String) -> ScanState:
2932 match allowed:
2933 case True{}: scan_operand_ok(state, token)
2934 case False{}: scan_error(state, E09{token})
2935
2936def scope_can_take(scope: Scope, count: Nat) -> Bool:
2937 match scope:
2938 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2939 external, needs_sub, version, digit_short, inherit}}:
2940 Bool.or(Nat.is_lt(count, List.length(&2, ArgInfo, slots)), variadic)
2941
2942def scan_operand(state: ScanState, token: String) -> ScanState:
2943 match state:
2944 case ScanState{+scope, events, resting, +positional_count,
2945 external, issue}:
2946 scan_operand_case(scope_can_take(scope, positional_count),
2947 ScanState{scope, events, resting, positional_count,
2948 external, issue}, token)
2949
2950def spec_inherit(spec: Spec) -> List<&2, SpecItem>:
2951 match spec:
2952 case Spec{keys, args, slots, variadic, subs, has_sub, external,
2953 needs_sub, version, digit_short, inherit}: inherit
2954
2955def event_id(event: ScanEvent) -> Maybe<&2, String>:
2956 match event:
2957 case FlagHit{id, value}: Some{id}
2958 case CountHit{id}: Some{id}
2959 case ValueHit{id, value}: Some{id}
2960 case _: None{}
2961
2962def spec_info_found(item: SpecItem, id: String) -> Maybe<&2, ArgInfo>:
2963 match item:
2964 case SpecArg{+info}:
2965 own = info_id(info)
2966 Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
2967 case SpecOther{other}: None{}
2968
2969# find_arg_info over parsed items; after a hit it only walks the rest.
2970def find_spec_info(items: List<&2, SpecItem>, +id: String,
2971 found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
2972 match items found:
2973 case Nil{} hit: hit
2974 case item <> tail Some{hit}: Some{hit}
2975 case item <> tail None{}:
2976 find_spec_info(tail, id, spec_info_found(item, id))
2977
2978# An arg's hits from before the scan enters a subcommand stay only when the
2979# scope being left passes the arg down as a global (SPEC 8.4); operands,
2980# entries, help and version stay.
2981def event_inherited(found: Maybe<&2, String>,
2982 +globals: List<&2, SpecItem>) -> Bool:
2983 match found:
2984 case None{}: True{}
2985 case Some{id}:
2986 Maybe.is_some(&2, ArgInfo, find_spec_info(globals, id, None{}))
2987
2988def events_kept(keep: Bool, event: ScanEvent,
2989 rest: List<&2, ScanEvent>) -> List<&2, ScanEvent>:
2990 match keep:
2991 case True{}: event <> rest
2992 case False{}: rest
2993
2994def events_inherited(events: List<&2, ScanEvent>,
2995 +globals: List<&2, SpecItem>) -> List<&2, ScanEvent>:
2996 match events:
2997 case Nil{}: Nil{}
2998 case +event <> tail:
2999 events_kept(event_inherited(event_id(event), globals), event,
3000 events_inherited(tail, globals))
3001
3002def scan_descend(state: ScanState, sub: SubKey) -> ScanState:
3003 match state:
3004 case ScanState{scope, events, resting, positional_count,
3005 external, issue}:
3006 match scope:
3007 case Scope{path, spec}:
3008 match sub:
3009 case SubKey{+name, aliases, children}:
3010 +globals = spec_inherit(spec)
3011 ScanState{Scope{
3012 List.append(&2, String, path, [name]),
3013 spec_node(children, globals)},
3014 Enter{name} <> events_inherited(events, globals), resting, 0n,
3015 external, issue}
3016
3017def scan_external(state: ScanState, name: String,
3018 args: List<&2, String>) -> ScanState:
3019 match state:
3020 case ScanState{scope, events, resting, positional_count,
3021 external, issue}:
3022 ScanState{scope, events, resting, positional_count,
3023 Some{ExternalArgs{name, args}}, issue}
3024
3025# E04 quotes the first element that fails.
3026def scan_value_result(found: Maybe<&2, Bad>, state: ScanState,
3027 info: ArgInfo, value: String) -> ScanState:
3028 match found:
3029 case None{}:
3030 match info:
3031 case ArgInfo{id, usage, short, long, kind, attrs}:
3032 scan_event(state, ValueHit{id, value})
3033 case Some{Bad{bad, why}}:
3034 scan_error(state, E04{arg_display(info), bad, why})
3035
3036def scan_value(state: ScanState, +info: ArgInfo,
3037 +value: String) -> ScanState:
3038 scan_value_result(value_bad(info_attrs(info), value), state, info, value)
3039
3040def scan_simple_hit(state: ScanState, info: ArgInfo,
3041 polarity: Bool) -> ScanState:
3042 match info:
3043 case ArgInfo{id, usage, short, long, kind, attrs}:
3044 match kind:
3045 case CountKind{}: scan_event(state, CountHit{id})
3046 case _: scan_event(state, FlagHit{id, polarity})
3047
3048def scan_settle_case(resting: Bool, issue: Maybe<&2, ScanFault>,
3049 scope: Scope, events: List<&2, ScanEvent>, positional_count: Nat,
3050 external: Maybe<&2, ExternalArgs>) -> Mode:
3051 match resting issue:
3052 case True{} None{}:
3053 Operands{ScanState{scope, events, True{}, positional_count, external,
3054 None{}}, False{}}
3055 case True{} Some{fault}:
3056 Operands{ScanState{scope, events, True{}, positional_count, external,
3057 Some{fault}}, False{}}
3058 case False{} None{}:
3059 Open{ScanState{scope, events, False{}, positional_count, external,
3060 None{}}}
3061 case False{} Some{fault}:
3062 Closed{Faulted{ScanState{scope, events, False{}, positional_count,
3063 external, Some{fault}}}}
3064
3065def scan_settle(state: ScanState) -> Mode:
3066 match state:
3067 case ScanState{scope, events, resting, positional_count, external,
3068 issue}:
3069 scan_settle_case(resting, issue, scope, events, positional_count,
3070 external)
3071
3072def help_flag_case(long: Bool, short: Bool) -> Maybe<&2, Bool>:
3073 match long short:
3074 case True{} _: Some{True{}}
3075 case False{} True{}: Some{False{}}
3076 case False{} False{}: None{}
3077
3078# Which help flag a token is: Some{True{}} for --help, Some{False{}} for -h.
3079def help_flag(+token: String) -> Maybe<&2, Bool>:
3080 help_flag_case(String.eq(token, "--help"), String.eq(token, "-h"))
3081
3082def scan_faulted_case(help: Maybe<&2, Bool>, state: ScanState) -> Tail:
3083 match help:
3084 case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3085 case None{}: Faulted{state}
3086
3087def scan_faulted(state: ScanState, token: String) -> Tail:
3088 scan_faulted_case(help_flag(token), state)
3089
3090def scope_digit_short(scope: Scope) -> Bool:
3091 match scope:
3092 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3093 external, needs_sub, version, digit_short, inherit}}: digit_short
3094
3095def arg_has_hyphen(info: ArgInfo) -> Bool:
3096 match info:
3097 case ArgInfo{id, usage, short, long, kind, attrs}:
3098 has_hyphen(attrs)
3099
3100def arg_is_f32(info: ArgInfo) -> Bool:
3101 match info:
3102 case ArgInfo{id, usage, short, long, kind, attrs}:
3103 has_as_f32(attrs)
3104
3105def fetch_allowed(scope: Scope, +info: ArgInfo,
3106 +token: String) -> Bool:
3107 Bool.and(Bool.not(String.eq(token, "--")),
3108 Bool.or(Bool.not(String.starts_with(token, "-")),
3109 Bool.or(String.eq(token, "-"),
3110 Bool.or(arg_has_hyphen(info),
3111 Bool.and(arg_is_f32(info),
3112 Bool.and(is_negative_number(token),
3113 Bool.not(scope_digit_short(scope))))))))
3114
3115def scan_fetch_result(allowed: Bool, state: ScanState,
3116 info: ArgInfo, token: String) -> Mode:
3117 match allowed:
3118 case True{}: scan_settle(scan_value(state, info, token))
3119 case False{}:
3120 Closed{scan_faulted(scan_error(state, E03{arg_display(info)}), token)}
3121
3122def scan_state_scope(state: ScanState) -> Scope:
3123 match state:
3124 case ScanState{scope, events, resting, positional_count,
3125 external, issue}: scope
3126
3127def scan_fetch(+state: ScanState, +info: ArgInfo,
3128 +token: String) -> Mode:
3129 scan_fetch_result(fetch_allowed(scan_state_scope(state), info, token),
3130 state, info, token)
3131
3132def bare_value(attrs: List<&2, Item>) -> String:
3133 match attrs:
3134 case Nil{}: ""
3135 case Bare{value} <> tail: value
3136 case head <> tail: bare_value(tail)
3137
3138def scan_option(attached: Maybe<&2, String>, state: ScanState,
3139 info: ArgInfo) -> Mode:
3140 match attached:
3141 case Some{value}: scan_settle(scan_value(state, info, value))
3142 case None{}: Fetch{state, info}
3143
3144def info_bare_value(info: ArgInfo) -> String:
3145 match info:
3146 case ArgInfo{id, usage, short, long, kind, attrs}:
3147 bare_value(attrs)
3148
3149def scan_bare_option(attached: Maybe<&2, String>, state: ScanState,
3150 +info: ArgInfo) -> Mode:
3151 match attached:
3152 case Some{value}: scan_settle(scan_value(state, info, value))
3153 case None{}:
3154 scan_settle(scan_value(state, info, info_bare_value(info)))
3155
3156def scan_simple_option(attached: Maybe<&2, String>, state: ScanState,
3157 info: ArgInfo, polarity: Bool) -> Mode:
3158 match attached:
3159 case Some{value}:
3160 scan_settle(scan_error(state, E05{arg_display(info), value}))
3161 case None{}: scan_settle(scan_simple_hit(state, info, polarity))
3162
3163def scan_found_kind(kind: UsageKind, attached: Maybe<&2, String>,
3164 state: ScanState, info: ArgInfo, polarity: Bool) -> Mode:
3165 match kind:
3166 case FlagKind{}:
3167 scan_simple_option(attached, state, info, polarity)
3168 case CountKind{}:
3169 scan_simple_option(attached, state, info, polarity)
3170 case NegatableKind{}:
3171 scan_simple_option(attached, state, info, polarity)
3172 case OptionKind{many}:
3173 scan_option(attached, state, info)
3174 case BareOptionKind{}:
3175 scan_bare_option(attached, state, info)
3176 case PosKind{required, many}:
3177 scan_settle(state)
3178
3179def scan_found(found: OptFound, attached: Maybe<&2, String>,
3180 state: ScanState) -> Mode:
3181 match found:
3182 case OptFound{+arg, polarity}:
3183 scan_found_kind(info_kind(arg), attached, state, arg, polarity)
3184
3185def scope_version(scope: Scope) -> Bool:
3186 match scope:
3187 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3188 external, needs_sub, version, digit_short, inherit}}:
3189 Bool.and(List.is_empty(&2, String, path), version)
3190
3191def scan_builtin_hit(attached: Maybe<&2, String>, state: ScanState,
3192 event: ScanEvent, name: String) -> Mode:
3193 match attached:
3194 case Some{value}: scan_settle(scan_error(state, E05{name, value}))
3195 case None{}: Closed{Stopped{scan_event(state, event)}}
3196
3197def scan_builtin_version(enabled: Bool, attached: Maybe<&2, String>,
3198 state: ScanState) -> Mode:
3199 match enabled:
3200 case True{}:
3201 scan_builtin_hit(attached, state, VersionHit{}, "--version")
3202 case False{}:
3203 scan_settle(scan_error(state, E01{"--version"}))
3204
3205def scan_builtin_other(version: Bool, name: String,
3206 attached: Maybe<&2, String>, +state: ScanState) -> Mode:
3207 match version:
3208 case True{}:
3209 scan_builtin_version(scope_version(scan_state_scope(state)),
3210 attached, state)
3211 case False{}: scan_settle(scan_error(state, E01{name}))
3212
3213def scan_builtin_long_case(help: Bool, +name: String,
3214 attached: Maybe<&2, String>, state: ScanState) -> Mode:
3215 match help:
3216 case True{}:
3217 scan_builtin_hit(attached, state, HelpHit{True{}}, "--help")
3218 case False{}:
3219 scan_builtin_other(String.eq(name, "--version"), name, attached, state)
3220
3221def scan_builtin_long(+name: String, attached: Maybe<&2, String>,
3222 state: ScanState) -> Mode:
3223 scan_builtin_long_case(String.eq(name, "--help"), name, attached, state)
3224
3225def scan_long_found(found: Maybe<&2, OptFound>, name: String,
3226 attached: Maybe<&2, String>, state: ScanState) -> Mode:
3227 match found:
3228 case Some{option}: scan_found(option, attached, state)
3229 case None{}: scan_builtin_long(name, attached, state)
3230
3231def scan_long(+token: String, +state: ScanState) -> Mode:
3232 +name = before_equal(token)
3233 scan_long_found(find_option_scope(scan_state_scope(state), name),
3234 name, after_equal(token), state)
3235
3236def find_short_for_head(chars: String, state: ScanState) ->
3237 Maybe<&2, OptFound>:
3238 match chars:
3239 case SNil{}: None{}
3240 case SCon{head, tail}:
3241 find_option_scope(scan_state_scope(state),
3242 String.append("-", Char.show(head)))
3243
3244def short_attached(chars: String) -> String:
3245 match chars:
3246 case SCon{Chr{61}, tail}: tail
3247 case _: chars
3248
3249def scan_short_value_attached(value: String, state: ScanState,
3250 info: ArgInfo) -> Mode:
3251 match value:
3252 case SNil{}: Fetch{state, info}
3253 case _: scan_settle(scan_value(state, info, value))
3254
3255def scan_short_value(chars: String, state: ScanState,
3256 info: ArgInfo) -> Mode:
3257 scan_short_value_attached(short_attached(chars), state, info)
3258
3259def scan_short_bare_value(chars: String, state: ScanState,
3260 +info: ArgInfo) -> Mode:
3261 match chars:
3262 case SNil{}:
3263 scan_settle(scan_value(state, info, info_bare_value(info)))
3264 case _:
3265 scan_settle(scan_value(state, info, short_attached(chars)))
3266
3267def scan_short_version(enabled: Bool, state: ScanState) -> Mode:
3268 match enabled:
3269 case True{}: Closed{Stopped{scan_event(state, VersionHit{})}}
3270 case False{}: scan_settle(scan_error(state, E01{"-V"}))
3271
3272def scan_short_go(chars: String, +state: ScanState,
3273 found: Maybe<&2, OptFound>) -> Mode:
3274 match chars found:
3275 case SNil{} _:
3276 scan_settle(state)
3277 case SCon{Chr{104}, +tail} None{}:
3278 Closed{Stopped{scan_event(state, HelpHit{False{}})}}
3279 case SCon{Chr{86}, tail} None{}:
3280 scan_short_version(scope_version(scan_state_scope(state)), state)
3281 case SCon{head, tail} None{}:
3282 scan_settle(scan_error(state,
3283 E01{String.append("-", Char.show(head))}))
3284 case SCon{head, tail} Some{OptFound{arg, polarity}}:
3285 match arg:
3286 case ArgInfo{id, usage, short, long, kind, attrs}:
3287 match kind:
3288 case FlagKind{}:
3289 +next = scan_event(state, FlagHit{id, polarity})
3290 scan_short_go(tail, next, find_short_for_head(tail, next))
3291 case CountKind{}:
3292 +next = scan_event(state, CountHit{id})
3293 scan_short_go(tail, next, find_short_for_head(tail, next))
3294 case NegatableKind{}:
3295 +next = scan_event(state, FlagHit{id, polarity})
3296 scan_short_go(tail, next, find_short_for_head(tail, next))
3297 case OptionKind{many}:
3298 scan_short_value(tail, state,
3299 ArgInfo{id, usage, short, long, OptionKind{many}, attrs})
3300 case BareOptionKind{}:
3301 scan_short_bare_value(tail, state,
3302 ArgInfo{id, usage, short, long, BareOptionKind{}, attrs})
3303 case PosKind{required, many}:
3304 scan_settle(state)
3305
3306def scan_short(token: String, +state: ScanState) -> Mode:
3307 +chars = String.drop(token, 1n)
3308 scan_short_go(chars, state, find_short_for_head(chars, state))
3309
3310def scope_external(scope: Scope) -> Bool:
3311 match scope:
3312 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3313 external, needs_sub, version, digit_short, inherit}}: external
3314
3315def scope_has_sub(scope: Scope) -> Bool:
3316 match scope:
3317 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3318 external, needs_sub, version, digit_short, inherit}}: has_sub
3319
3320def sub_key_aliased(aliased: Bool, name: String, aliases: List<&2, String>,
3321 children: List<&2, Item>) -> Maybe<&2, SubKey>:
3322 match aliased:
3323 case True{}: Some{SubKey{name, aliases, children}}
3324 case False{}: None{}
3325
3326def sub_key_hit(same: Bool, name: String, +aliases: List<&2, String>,
3327 children: List<&2, Item>, query: String) -> Maybe<&2, SubKey>:
3328 match same:
3329 case True{}: Some{SubKey{name, aliases, children}}
3330 case False{}:
3331 sub_key_aliased(List.contains(~String, ~String.eq, aliases, query),
3332 name, aliases, children)
3333
3334def sub_key_found(key: SubKey, +query: String) -> Maybe<&2, SubKey>:
3335 match key:
3336 case SubKey{+name, aliases, children}:
3337 sub_key_hit(same_text(query, name), name, aliases, children, query)
3338
3339# find_sub over a scope's subcommands; after a hit it only walks the rest.
3340def find_sub_key(subs: List<&2, SubKey>, +query: String,
3341 found: Maybe<&2, SubKey>) -> Maybe<&2, SubKey>:
3342 match subs found:
3343 case Nil{} hit: hit
3344 case key <> tail Some{hit}: Some{hit}
3345 case key <> tail None{}:
3346 find_sub_key(tail, query, sub_key_found(key, query))
3347
3348def scope_find_sub(scope: Scope, query: String) -> Maybe<&2, SubKey>:
3349 match scope:
3350 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3351 external, needs_sub, version, digit_short, inherit}}:
3352 find_sub_key(subs, query, None{})
3353
3354def scan_sub_unknown(allow_external: Bool, state: ScanState,
3355 token: String) -> Mode:
3356 match allow_external:
3357 case True{}: Capture{state, token}
3358 case False{}: Closed{Stopped{scan_error(state, E02{token})}}
3359
3360def scan_sub_found(found: Maybe<&2, SubKey>, +state: ScanState,
3361 token: String) -> Mode:
3362 match found:
3363 case Some{sub}: scan_settle(scan_descend(state, sub))
3364 case None{}:
3365 scan_sub_unknown(scope_external(scan_state_scope(state)),
3366 state, token)
3367
3368def unknown_fault(dash: Bool, token: String) -> ScanFault:
3369 match dash:
3370 case True{}: E01{token}
3371 case False{}: E02{token}
3372
3373def help_missing_case(help: Maybe<&2, Bool>, +token: String,
3374 state: ScanState) -> Tail:
3375 match help:
3376 case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3377 case None{}:
3378 Stopped{scan_error(state,
3379 unknown_fault(String.starts_with(token, "-"), token))}
3380
3381def help_missing(+token: String, state: ScanState) -> Tail:
3382 help_missing_case(help_flag(token), token, state)
3383
3384def help_walk(found: Maybe<&2, SubKey>, state: ScanState,
3385 token: String) -> Tail:
3386 match found:
3387 case Some{sub}: Walk{scan_descend(state, sub)}
3388 case None{}: help_missing(token, state)
3389
3390def scan_non_option_sub_case(help: Bool, +token: String,
3391 +state: ScanState) -> Mode:
3392 match help:
3393 case True{}: Closed{Walk{state}}
3394 case False{}:
3395 scan_sub_found(scope_find_sub(scan_state_scope(state), token),
3396 state, token)
3397
3398def scan_non_option_sub(+token: String, state: ScanState) -> Mode:
3399 scan_non_option_sub_case(String.eq(token, "help"), token, state)
3400
3401def scan_non_option_case(has_sub: Bool, token: String,
3402 state: ScanState) -> Mode:
3403 match has_sub:
3404 case True{}: scan_non_option_sub(token, state)
3405 case False{}: scan_settle(scan_operand(state, token))
3406
3407def scan_non_option(token: String, +state: ScanState) -> Mode:
3408 scan_non_option_case(scope_has_sub(scan_state_scope(state)), token, state)
3409
3410def scan_hyphen_candidate(state: ScanState, +token: String) -> Bool:
3411 match state:
3412 case ScanState{+scope, events, resting, positional_count,
3413 external, issue}:
3414 +slot = scope_next_slot(scope, positional_count)
3415 Bool.or(Bool.and(slot_hyphen(slot),
3416 Bool.not(scope_any_option_known(scope, token))),
3417 Bool.and(slot_f32(slot),
3418 Bool.and(is_negative_number(token),
3419 Bool.not(scope_digit_short(scope)))))
3420
3421def scan_dash_case(operand: Bool, token: String, state: ScanState) -> Mode:
3422 match operand:
3423 case True{}: scan_settle(scan_operand(state, token))
3424 case False{}: scan_short(token, state)
3425
3426def scan_dash_token(+token: String, +state: ScanState) -> Mode:
3427 scan_dash_case(scan_hyphen_candidate(state, token), token, state)
3428
3429# How an open scan reads a token: a long option, a dash token (a cluster of
3430# short options, or an operand a slot takes), or a word; "-" is a word.
3431type TokenClass is Data:
3432 LongToken{}
3433 DashToken{}
3434 WordToken{}
3435
3436def token_class(token: String) -> TokenClass:
3437 match token:
3438 case SCon{Chr{45}, SCon{Chr{45}, rest}}: LongToken{}
3439 case SCon{Chr{45}, SNil{}}: WordToken{}
3440 case SCon{Chr{45}, rest}: DashToken{}
3441 case _: WordToken{}
3442
3443def scan_token_case(kind: TokenClass, token: String,
3444 state: ScanState) -> Mode:
3445 match kind:
3446 case LongToken{}: scan_long(token, state)
3447 case DashToken{}: scan_dash_token(token, state)
3448 case WordToken{}: scan_non_option(token, state)
3449
3450def scan_token(+token: String, state: ScanState) -> Mode:
3451 scan_token_case(token_class(token), token, state)
3452
3453def scan_tail(tail: Tail, +token: String) -> Tail:
3454 match tail:
3455 case Faulted{state}: scan_faulted(state, token)
3456 case Walk{+state}:
3457 help_walk(scope_find_sub(scan_state_scope(state), token),
3458 state, token)
3459 case Stopped{state}: Stopped{state}
3460
3461# `--` ends options only here, while the scan is open (R1).
3462def scan_open(dash: Bool, state: ScanState, token: String) -> Mode:
3463 match dash:
3464 case True{}: Operands{state, True{}}
3465 case False{}: scan_token(token, state)
3466
3467# An external subcommand takes the rest of argv verbatim.
3468def scan_go(tokens: List<&2, String>, mode: Mode) -> Mode:
3469 match tokens mode:
3470 case argv Capture{state, name}:
3471 Closed{Stopped{scan_external(state, name, argv)}}
3472 case Nil{} _: mode
3473 case +token <> rest Open{state}:
3474 scan_go(rest, scan_open(String.eq(token, "--"), state, token))
3475 case token <> rest Fetch{state, info}:
3476 scan_go(rest, scan_fetch(state, info, token))
3477 case token <> rest Operands{state, ended}:
3478 scan_go(rest, Operands{scan_operand(state, token), ended})
3479 case token <> rest Closed{tail}:
3480 scan_go(rest, Closed{scan_tail(tail, token)})
3481
3482def scan_tail_close(tail: Tail) -> ScanState:
3483 match tail:
3484 case Faulted{state}: state
3485 case Walk{state}: scan_event(state, HelpHit{True{}})
3486 case Stopped{state}: state
3487
3488def scan_close(mode: Mode) -> ScanState:
3489 match mode:
3490 case Open{state}: state
3491 case Fetch{state, info}: scan_error(state, E03{arg_display(info)})
3492 case Operands{state, ended}: state
3493 case Capture{state, name}: scan_external(state, name, [])
3494 case Closed{tail}: scan_tail_close(tail)
3495
3496def scan_trace_of(state: ScanState, ended: Bool) -> ScanTrace:
3497 match state:
3498 case ScanState{scope, events, resting, positional_count,
3499 external, issue}:
3500 match scope:
3501 case Scope{path, spec}:
3502 ScanTrace{path, List.reverse(&2, ScanEvent, events),
3503 ended, external, issue}
3504
3505def scan_trace(mode: Mode) -> ScanTrace:
3506 match mode:
3507 case Operands{state, ended}: scan_trace_of(state, ended)
3508 case other: scan_trace_of(scan_close(other), False{})
3509
3510def scan(c: Cmd, argv: List<&2, String>) -> ScanTrace:
3511 scan_trace(scan_go(argv, Open{scan_start(c)}))
3512
3513def find_early(events: List<&2, ScanEvent>) -> Maybe<&2, Early>:
3514 match events:
3515 case Nil{}: None{}
3516 case HelpHit{long} <> tail: Some{EarlyHelp{long}}
3517 case VersionHit{} <> tail: Some{EarlyVersion{}}
3518 case event <> tail: find_early(tail)
3519
3520def scope_needs_sub(scope: Scope) -> Bool:
3521 match scope:
3522 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3523 external, needs_sub, version, digit_short, inherit}}: needs_sub
3524
3525def bool_text(value: Bool) -> String:
3526 Bool.pick(String, value, "true", "false")
3527
3528def gather_value(gather: Gather, +value: String,
3529 many: Bool) -> Gather:
3530 match gather:
3531 case Gather{seen, values, +count}:
3532 Bool.pick(Gather, many,
3533 Gather{True{}, value <> values, count},
3534 Gather{True{}, [value], count})
3535
3536def gather_flag(gather: Gather, value: Bool) -> Gather:
3537 match gather:
3538 case Gather{seen, values, count}:
3539 Gather{True{}, [bool_text(value)], count}
3540
3541def gather_count(gather: Gather) -> Gather:
3542 match gather:
3543 case Gather{seen, values, count}:
3544 +next = Nat.add(count, 1n)
3545 Gather{True{}, [Nat.show(next)], next}
3546
3547def kind_many(kind: UsageKind) -> Bool:
3548 match kind:
3549 case OptionKind{many}: many
3550 case PosKind{required, many}: many
3551 case _: False{}
3552
3553def gather_event(event: ScanEvent, id: String,
3554 kind: UsageKind, +gather: Gather) -> Gather:
3555 match event:
3556 case ValueHit{key, value}:
3557 Bool.pick(Gather, String.eq(key, id),
3558 gather_value(gather, value, kind_many(kind)), gather)
3559 case FlagHit{key, value}:
3560 Bool.pick(Gather, String.eq(key, id),
3561 gather_flag(gather, value), gather)
3562 case CountHit{key}:
3563 Bool.pick(Gather, String.eq(key, id),
3564 gather_count(gather), gather)
3565 case _: gather
3566
3567def gather_events(events: List<&2, ScanEvent>, +id: String,
3568 +kind: UsageKind, gather: Gather) -> Gather:
3569 match events:
3570 case Nil{}: gather
3571 case event <> tail:
3572 gather_events(tail, id, kind,
3573 gather_event(event, id, kind, gather))
3574
3575def gather_arg(events: List<&2, ScanEvent>, info: ArgInfo) -> Gather:
3576 match info:
3577 case ArgInfo{id, usage, short, long, kind, attrs}:
3578 gather_events(events, id, kind, Gather{False{}, [], 0n})
3579
3580# Each env() name, in order.
3581def find_envs(attrs: List<&2, Item>) -> List<&2, String>:
3582 match attrs:
3583 case Nil{}: Nil{}
3584 case AttrEnv{name} <> tail: name <> find_envs(tail)
3585 case head <> tail: find_envs(tail)
3586
3587def env_present(name: String, found: Maybe<&2, String>) ->
3588 Maybe<&2, EnvValue>:
3589 match found:
3590 case None{}: None{}
3591 case Some{+value}:
3592 Bool.pick(Maybe<&2, EnvValue>, String.is_empty(value),
3593 None{}, Some{EnvValue{name, value}})
3594
3595# found, or else name's value in vars if it is set and not empty.
3596def env_next(found: Maybe<&2, EnvValue>, +name: String,
3597 vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3598 match found:
3599 case Some{env}: Some{env}
3600 case None{}: env_present(name, var_value(vars, name))
3601
3602# found, or else the first of names that is set and not empty in vars.
3603def env_first(names: List<&2, String>, found: Maybe<&2, EnvValue>,
3604 +vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3605 match names:
3606 case Nil{}: found
3607 case name <> tail: env_first(tail, env_next(found, name, vars), vars)
3608
3609def host_env(h: Host, names: List<&2, String>) -> Maybe<&2, EnvValue>:
3610 match h:
3611 case Host{width, vars, out_tty, err_tty}:
3612 env_first(names, None{}, vars)
3613
3614def env_bool_true(value: String) -> Bool:
3615 List.contains(~String, ~String.eq,
3616 ["true", "yes", "on", "1"], String.to_lower(value))
3617
3618def env_values(kind: UsageKind, attrs: List<&2, Item>,
3619 value: String) -> List<&2, String>:
3620 match kind:
3621 case FlagKind{}: [bool_text(env_bool_true(value))]
3622 case NegatableKind{}: [bool_text(env_bool_true(value))]
3623 case CountKind{}: [value]
3624 case _: apply_delim(find_delim(attrs), [value])
3625
3626def resolve_env_valid(found: Maybe<&2, Bad>, info: ArgInfo,
3627 name: String, value: String) -> FieldBuild:
3628 match found:
3629 case Some{Bad{bad, why}}:
3630 BuildFault{E11{arg_display(info), bad, name, why}}
3631 case None{}:
3632 match info:
3633 case ArgInfo{id, usage, short, long, kind, attrs}:
3634 Built{[Field{id, env_values(kind, attrs, value), Env{}}]}
3635
3636def resolve_env(+info: ArgInfo, env: EnvValue) -> FieldBuild:
3637 match env:
3638 case EnvValue{name, +value}:
3639 resolve_env_valid(value_issue(info_kind(info), info_attrs(info), value),
3640 info, name, value)
3641
3642def resolve_default(found: Maybe<&2, String>,
3643 info: ArgInfo) -> FieldBuild:
3644 match found:
3645 case None{}: Built{[]}
3646 case Some{value}:
3647 match info:
3648 case ArgInfo{id, usage, short, long, kind, attrs}:
3649 Built{[Field{id, env_values(kind, attrs, value), Default{}}]}
3650
3651def info_default(info: ArgInfo) -> Maybe<&2, String>:
3652 match info:
3653 case ArgInfo{id, usage, short, long, kind, attrs}:
3654 find_default(attrs)
3655
3656def resolve_env_default(found: Maybe<&2, EnvValue>,
3657 +info: ArgInfo) -> FieldBuild:
3658 match found:
3659 case Some{env}: resolve_env(info, env)
3660 case None{}: resolve_default(info_default(info), info)
3661
3662def info_envs(info: ArgInfo) -> List<&2, String>:
3663 match info:
3664 case ArgInfo{id, usage, short, long, kind, attrs}:
3665 find_envs(attrs)
3666
3667def resolve_argv_gather(gather: Gather, +info: ArgInfo,
3668 h: Host) -> FieldBuild:
3669 match gather:
3670 case Gather{True{}, values, count}:
3671 match info:
3672 case ArgInfo{id, usage, short, long, kind, attrs}:
3673 Built{[Field{id, apply_delim(find_delim(attrs),
3674 List.reverse(&2, String, values)), Argv{}}]}
3675 case Gather{False{}, values, count}:
3676 resolve_env_default(host_env(h, info_envs(info)), info)
3677
3678def resolve_option(+info: ArgInfo,
3679 events: List<&2, ScanEvent>, h: Host) -> FieldBuild:
3680 resolve_argv_gather(gather_arg(events, info), info, h)
3681
3682def scope_arg_items(scope: Scope) -> List<&2, SpecItem>:
3683 match scope:
3684 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3685 external, needs_sub, version, digit_short, inherit}}: args
3686
3687def combine_built(fields: List<&2, Field>,
3688 next: FieldBuild) -> FieldBuild:
3689 match next:
3690 case Built{more}:
3691 Built{List.append(&2, Field, fields, more)}
3692 case BuildFault{fault}: BuildFault{fault}
3693
3694def combine_build(first: FieldBuild,
3695 second: FieldBuild) -> FieldBuild:
3696 match first:
3697 case Built{fields}: combine_built(fields, second)
3698 case BuildFault{fault}: BuildFault{fault}
3699
3700def resolve_arg(pos: Bool, +info: ArgInfo, events: List<&2, ScanEvent>,
3701 h: Host, later: FieldBuild) -> FieldBuild:
3702 match pos:
3703 case True{}: later
3704 case False{}: combine_build(resolve_option(info, events, h), later)
3705
3706def resolve_options(items: List<&2, SpecItem>,
3707 +events: List<&2, ScanEvent>, +h: Host) -> FieldBuild:
3708 match items:
3709 case Nil{}: Built{[]}
3710 case SpecArg{+info} <> tail:
3711 resolve_arg(kind_pos(info_kind(info)), info, events, h,
3712 resolve_options(tail, events, h))
3713 case head <> tail: resolve_options(tail, events, h)
3714
3715def scan_operands(events: List<&2, ScanEvent>) ->
3716 List<&2, String>:
3717 match events:
3718 case Nil{}: Nil{}
3719 case Operand{token} <> tail:
3720 token <> scan_operands(tail)
3721 case event <> tail: scan_operands(tail)
3722
3723def position_take_count(kind: UsageKind,
3724 after: List<&2, ArgInfo>, +tokens: List<&2, String>) -> Nat:
3725 match kind:
3726 case PosKind{required, True{}}:
3727 Nat.sub(List.length(&2, String, tokens),
3728 List.length(&2, ArgInfo, after))
3729 case _:
3730 Bool.pick(Nat, List.is_empty(&2, String, tokens), 0n, 1n)
3731
3732def pos_valid_result(found: Maybe<&2, Bad>,
3733 info: ArgInfo, values: List<&2, String>) -> FieldBuild:
3734 match found:
3735 case Some{Bad{bad, why}}:
3736 BuildFault{E04{arg_display(info), bad, why}}
3737 case None{}:
3738 match info:
3739 case ArgInfo{id, usage, short, long, kind, attrs}:
3740 Built{[Field{id, values, Argv{}}]}
3741
3742def resolve_pos(+info: ArgInfo,
3743 +values: List<&2, String>, h: Host) -> FieldBuild:
3744 match values:
3745 case Nil{}:
3746 resolve_env_default(host_env(h, info_envs(info)), info)
3747 case head <> tail:
3748 +attrs = info_attrs(info)
3749 +split = apply_delim(find_delim(attrs), values)
3750 pos_valid_result(first_bad(split, attrs), info, split)
3751
3752def resolve_positions(slots: List<&2, ArgInfo>,
3753 +tokens: List<&2, String>, +h: Host) -> FieldBuild:
3754 match slots:
3755 case Nil{}: Built{[]}
3756 case +info <> +tail:
3757 +amount = position_take_count(info_kind(info), tail, tokens)
3758 combine_build(
3759 resolve_pos(info, List.take(&2, String, tokens, amount), h),
3760 resolve_positions(tail, List.drop(&2, String, tokens, amount), h))
3761
3762def scope_positions(scope: Scope) -> List<&2, ArgInfo>:
3763 match scope:
3764 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3765 external, needs_sub, version, digit_short, inherit}}: slots
3766
3767def resolve_scope(+scope: Scope, +events: List<&2, ScanEvent>,
3768 +h: Host) -> FieldBuild:
3769 combine_build(
3770 resolve_options(scope_arg_items(scope), events, h),
3771 resolve_positions(scope_positions(scope),
3772 scan_operands(events), h))
3773
3774def source_given(source: Src) -> Bool:
3775 match source:
3776 case Argv{}: True{}
3777 case Env{}: True{}
3778 case Default{}: False{}
3779 case Absent{}: False{}
3780
3781def field_source(fields: List<&2, Field>, +id: String) -> Src:
3782 match fields:
3783 case Nil{}: Absent{}
3784 case Field{key, values, source} <> tail:
3785 Bool.pick(Src, String.eq(key, id), source,
3786 field_source(tail, id))
3787
3788def field_given(fields: List<&2, Field>, id: String) -> Bool:
3789 source_given(field_source(fields, id))
3790
3791def source_present(source: Src) -> Bool:
3792 match source:
3793 case Absent{}: False{}
3794 case _: True{}
3795
3796def field_present(fields: List<&2, Field>, id: String) -> Bool:
3797 source_present(field_source(fields, id))
3798
3799def group_usages(items: List<&2, Item>, +name: String) ->
3800 List<&2, String>:
3801 match items:
3802 case Nil{}: Nil{}
3803 case Arg{+usage, help, +attrs} <> tail:
3804 +later = group_usages(tail, name)
3805 Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3806 arg_display(arg_info(usage, attrs)) <> later, later)
3807 case head <> tail: group_usages(tail, name)
3808
3809def spec_group_usages(items: List<&2, SpecItem>, +name: String) ->
3810 List<&2, String>:
3811 match items:
3812 case Nil{}: Nil{}
3813 case SpecArg{+info} <> tail:
3814 +later = spec_group_usages(tail, name)
3815 attrs = info_attrs(info)
3816 Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3817 arg_display(info) <> later, later)
3818 case head <> tail: spec_group_usages(tail, name)
3819
3820def spec_group_display(items: List<&2, SpecItem>, name: String) -> String:
3821 String.concat(["<", String.join(spec_group_usages(items, name), "|"), ">"])
3822
3823def group_given(items: List<&2, SpecItem>,
3824 +fields: List<&2, Field>, +name: String) -> Bool:
3825 match items:
3826 case Nil{}: False{}
3827 case SpecArg{ArgInfo{id, usage, short, long, kind, attrs}} <> tail:
3828 Bool.or(Bool.and(arg_in_group(attrs, name), field_given(fields, id)),
3829 group_given(tail, fields, name))
3830 case head <> tail: group_given(tail, fields, name)
3831
3832def arg_required_missing(info: ArgInfo,
3833 +fields: List<&2, Field>) -> Bool:
3834 match info:
3835 case ArgInfo{+id, usage, short, long, kind, attrs}:
3836 Bool.or(Bool.and(has_required(attrs),
3837 Bool.not(field_given(fields, id))),
3838 Bool.and(kind_required_pos(kind),
3839 Bool.not(field_present(fields, id))))
3840
3841def group_display(items: List<&2, Item>, name: String) -> String:
3842 String.concat(["<", String.join(group_usages(items, name), "|"), ">"])
3843
3844def missing_arg(+info: ArgInfo,
3845 fields: List<&2, Field>) -> List<&2, String>:
3846 Bool.pick(List<&2, String>,
3847 arg_required_missing(info, fields),
3848 [arg_display(info)], [])
3849
3850def missing_group(+items: List<&2, SpecItem>,
3851 fields: List<&2, Field>, +name: String) -> List<&2, String>:
3852 Bool.pick(List<&2, String>,
3853 group_given(items, fields, name), [],
3854 [spec_group_display(items, name)])
3855
3856# A group's display the first time a need_one rule names it.
3857def missing_once(listed: Bool, items: List<&2, SpecItem>,
3858 fields: List<&2, Field>, name: String) -> List<&2, String>:
3859 match listed:
3860 case True{}: []
3861 case False{}: missing_group(items, fields, name)
3862
3863# What E06 lists, in items' order: each required arg not given, and each
3864# need_one group with no arg given that done does not name yet.
3865def missing_items(items: List<&2, SpecItem>,
3866 +all_items: List<&2, SpecItem>,
3867 +fields: List<&2, Field>, +done: List<&2, String>) ->
3868 List<&2, String>:
3869 match items:
3870 case Nil{}: Nil{}
3871 case SpecArg{info} <> tail:
3872 List.append(&2, String,
3873 missing_arg(info, fields),
3874 missing_items(tail, all_items, fields, done))
3875 case SpecOther{NeedOne{+name}} <> tail:
3876 List.append(&2, String,
3877 missing_once(List.contains(~String, ~String.eq, done, name),
3878 all_items, fields, name),
3879 missing_items(tail, all_items, fields, name <> done))
3880 case head <> tail:
3881 missing_items(tail, all_items, fields, done)
3882
3883def scope_missing(scope: Scope,
3884 fields: List<&2, Field>) -> List<&2, String>:
3885 +items = scope_arg_items(scope)
3886 missing_items(items, items, fields, [])
3887
3888def append_unique(+ids: List<&2, String>, +id: String) ->
3889 List<&2, String>:
3890 Bool.pick(List<&2, String>,
3891 List.contains(~String, ~String.eq, ids, id), ids,
3892 List.append(&2, String, ids, [id]))
3893
3894def append_event_id(found: Maybe<&2, String>,
3895 ids: List<&2, String>) -> List<&2, String>:
3896 match found:
3897 case None{}: ids
3898 case Some{id}: append_unique(ids, id)
3899
3900def argv_order(events: List<&2, ScanEvent>,
3901 ids: List<&2, String>) -> List<&2, String>:
3902 match events:
3903 case Nil{}: ids
3904 case event <> tail:
3905 argv_order(tail, append_event_id(event_id(event), ids))
3906
3907def source_env(source: Src) -> Bool:
3908 match source:
3909 case Env{}: True{}
3910 case _: False{}
3911
3912def env_order(fields: List<&2, Field>,
3913 +ids: List<&2, String>) -> List<&2, String>:
3914 match fields:
3915 case Nil{}: ids
3916 case Field{id, values, source} <> tail:
3917 env_order(tail, Bool.pick(List<&2, String>,
3918 source_env(source), append_unique(ids, id), ids))
3919
3920def present_order(events: List<&2, ScanEvent>,
3921 fields: List<&2, Field>) -> List<&2, String>:
3922 env_order(fields, argv_order(events, []))
3923
3924def arg_info_found(item: Item, id: String) -> Maybe<&2, ArgInfo>:
3925 match item:
3926 case Arg{usage, help, attrs}:
3927 +info = arg_info(usage, attrs)
3928 own = info_id(info)
3929 Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
3930 case _: None{}
3931
3932# The first Arg whose id is id; after a hit it walks no further.
3933def find_arg_go(items: List<&2, Item>, +id: String,
3934 found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
3935 match items found:
3936 case _ Some{hit}: Some{hit}
3937 case Nil{} None{}: None{}
3938 case item <> tail None{}: find_arg_go(tail, id, arg_info_found(item, id))
3939
3940def find_arg_info(items: List<&2, Item>, +id: String) ->
3941 Maybe<&2, ArgInfo>:
3942 find_arg_go(items, id, None{})
3943
3944def exclusive_groups(items: List<&2, SpecItem>) -> List<&2, String>:
3945 match items:
3946 case Nil{}: Nil{}
3947 case SpecOther{Exclusive{name}} <> tail:
3948 name <> exclusive_groups(tail)
3949 case head <> tail: exclusive_groups(tail)
3950
3951def common_exclusive(groups: List<&2, String>,
3952 +left: List<&2, String>, +right: List<&2, String>) -> Bool:
3953 match groups:
3954 case Nil{}: False{}
3955 case +name <> tail:
3956 Bool.or(Bool.and(
3957 List.contains(~String, ~String.eq, left, name),
3958 List.contains(~String, ~String.eq, right, name)),
3959 common_exclusive(tail, left, right))
3960
3961def pair_conflicts(+later: ArgInfo, +earlier: ArgInfo,
3962 groups: List<&2, String>) -> Bool:
3963 +later_attrs = info_attrs(later)
3964 +earlier_attrs = info_attrs(earlier)
3965 Bool.or(List.contains(~String, ~String.eq,
3966 find_conflicts(later_attrs), info_id(earlier)),
3967 Bool.or(List.contains(~String, ~String.eq,
3968 find_conflicts(earlier_attrs), info_id(later)),
3969 common_exclusive(groups,
3970 find_group_attrs(later_attrs),
3971 find_group_attrs(earlier_attrs))))
3972
3973def pair_fault(+later: ArgInfo, earlier: Maybe<&2, ArgInfo>,
3974 groups: List<&2, String>) -> Maybe<&2, ScanFault>:
3975 match earlier:
3976 case None{}: None{}
3977 case Some{+prior}:
3978 Bool.pick(Maybe<&2, ScanFault>,
3979 pair_conflicts(later, prior, groups),
3980 Some{E07{arg_display(later), arg_display(prior)}}, None{})
3981
3982def earlier_conflict(earlier_ids: List<&2, String>,
3983 +later: ArgInfo, +items: List<&2, SpecItem>,
3984 +groups: List<&2, String>,
3985 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
3986 match earlier_ids found:
3987 case Nil{} Some{fault}: Some{fault}
3988 case Nil{} None{}: None{}
3989 case id <> tail Some{fault}: Some{fault}
3990 case id <> tail None{}:
3991 earlier_conflict(tail, later, items, groups,
3992 pair_fault(later, find_spec_info(items, id, None{}), groups))
3993
3994def conflict_for_id(info: Maybe<&2, ArgInfo>,
3995 seen: List<&2, String>, items: List<&2, SpecItem>,
3996 groups: List<&2, String>) -> Maybe<&2, ScanFault>:
3997 match info:
3998 case None{}: None{}
3999 case Some{later}:
4000 earlier_conflict(seen, later, items, groups, None{})
4001
4002def conflict_go(ids: List<&2, String>,
4003 +seen: List<&2, String>, +items: List<&2, SpecItem>,
4004 +groups: List<&2, String>,
4005 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4006 match ids found:
4007 case Nil{} Some{fault}: Some{fault}
4008 case Nil{} None{}: None{}
4009 case id <> tail Some{fault}: Some{fault}
4010 case +id <> tail None{}:
4011 conflict_go(tail, List.append(&2, String, seen, [id]),
4012 items, groups,
4013 conflict_for_id(find_spec_info(items, id, None{}),
4014 seen, items, groups))
4015
4016def require_target_fault(target: Maybe<&2, ArgInfo>,
4017 +requester: ArgInfo, fields: List<&2, Field>) ->
4018 Maybe<&2, ScanFault>:
4019 match target:
4020 case None{}: None{}
4021 case Some{+needed}:
4022 Bool.pick(Maybe<&2, ScanFault>,
4023 Bool.not(field_given(fields, info_id(needed))),
4024 Some{E08{arg_display(requester), arg_display(needed)}},
4025 None{})
4026
4027def require_targets(targets: List<&2, String>,
4028 +requester: ArgInfo, +items: List<&2, SpecItem>,
4029 +fields: List<&2, Field>,
4030 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4031 match targets found:
4032 case Nil{} Some{fault}: Some{fault}
4033 case Nil{} None{}: None{}
4034 case name <> tail Some{fault}: Some{fault}
4035 case name <> tail None{}:
4036 require_targets(tail, requester, items, fields,
4037 require_target_fault(find_spec_info(items, name, None{}),
4038 requester, fields))
4039
4040def require_for_id(info: Maybe<&2, ArgInfo>,
4041 items: List<&2, SpecItem>,
4042 fields: List<&2, Field>) -> Maybe<&2, ScanFault>:
4043 match info:
4044 case None{}: None{}
4045 case Some{+requester}:
4046 require_targets(find_requires(info_attrs(requester)),
4047 requester, items, fields, None{})
4048
4049def require_go(ids: List<&2, String>,
4050 +items: List<&2, SpecItem>, +fields: List<&2, Field>,
4051 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4052 match ids found:
4053 case Nil{} Some{fault}: Some{fault}
4054 case Nil{} None{}: None{}
4055 case id <> tail Some{fault}: Some{fault}
4056 case id <> tail None{}:
4057 require_go(tail, items, fields,
4058 require_for_id(find_spec_info(items, id, None{}), items, fields))
4059
4060def parse_requires(fault: Maybe<&2, ScanFault>,
4061 path: List<&2, String>, fields: List<&2, Field>,
4062 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4063 match fault:
4064 case Some{problem}: Rejected{path, problem}
4065 case None{}: Accepted{path, fields, external}
4066
4067def parse_conflicts(fault: Maybe<&2, ScanFault>,
4068 +scope: Scope, +fields: List<&2, Field>,
4069 events: List<&2, ScanEvent>,
4070 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4071 match fault:
4072 case Some{problem}: Rejected{scope_path(scope), problem}
4073 case None{}:
4074 parse_requires(require_go(
4075 present_order(events, fields),
4076 scope_arg_items(scope), fields, None{}),
4077 scope_path(scope), fields, external)
4078
4079def parse_missing(missing: List<&2, String>,
4080 +scope: Scope, +fields: List<&2, Field>,
4081 +events: List<&2, ScanEvent>,
4082 empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4083 match missing:
4084 case Nil{}:
4085 +items = scope_arg_items(scope)
4086 parse_conflicts(conflict_go(present_order(events, fields),
4087 [], items, exclusive_groups(items), None{}),
4088 scope, fields, events, external)
4089 case head <> tail:
4090 Bool.pick(ParseResult, empty, SummaryRequired{},
4091 Rejected{scope_path(scope), E06{head <> tail}})
4092
4093def parse_built(build: FieldBuild, +scope: Scope,
4094 events: List<&2, ScanEvent>, empty: Bool,
4095 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4096 match build:
4097 case BuildFault{fault}: Rejected{scope_path(scope), fault}
4098 case Built{+fields}:
4099 parse_missing(scope_missing(scope, fields),
4100 scope, fields, events, empty, external)
4101
4102def parse_no_issue(needs_sub: Bool, +scope: Scope,
4103 +events: List<&2, ScanEvent>, +h: Host,
4104 +empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4105 Bool.pick(ParseResult, needs_sub,
4106 Bool.pick(ParseResult, empty, SummaryRequired{},
4107 Rejected{scope_path(scope), E10{}}),
4108 parse_built(resolve_scope(scope, events, h),
4109 scope, events, empty, external))
4110
4111def parse_issue(issue: Maybe<&2, ScanFault>,
4112 +scope: Scope, events: List<&2, ScanEvent>,
4113 h: Host, empty: Bool,
4114 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4115 match issue:
4116 case Some{fault}: Rejected{scope_path(scope), fault}
4117 case None{}:
4118 parse_no_issue(scope_needs_sub(scope), scope,
4119 events, h, empty, external)
4120
4121def parse_early(found: Maybe<&2, Early>,
4122 +scope: Scope, events: List<&2, ScanEvent>, h: Host,
4123 empty: Bool, issue: Maybe<&2, ScanFault>,
4124 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4125 match found:
4126 case Some{EarlyHelp{long}}:
4127 HelpRequested{scope_path(scope), long}
4128 case Some{EarlyVersion{}}:
4129 VersionRequested{}
4130 case None{}:
4131 parse_issue(issue, scope, events, h, empty, external)
4132
4133def parse_from_state(state: ScanState, h: Host,
4134 empty: Bool) -> ParseResult:
4135 match state:
4136 case ScanState{scope, events, resting, positional_count,
4137 external, issue}:
4138 +ordered: List<&2, ScanEvent> =
4139 List.reverse(&2, ScanEvent, events)
4140 parse_early(find_early(ordered), scope, ordered, h,
4141 empty, issue, external)
4142
4143def parse(c: Cmd, h: Host, +argv: List<&2, String>) -> ParseResult:
4144 parse_from_state(scan_close(scan_go(argv, Open{scan_start(c)})),
4145 h, List.is_empty(&2, String, argv))
4146
4147# ---- Reading values (§6.5) ----
4148
4149type Fault is Data:
4150 Bug{msg: String}
4151 Usage{msg: String}
4152
4153def Dec(-A: Type) -> Type:
4154 Result<&2, &1, Fault, A>
4155
4156def Dec.pure(-A: Type, x: A) -> Dec(A):
4157 Done{x}
4158
4159def Dec.bind(-A: Type, -B: Type, r: Dec(A), f: A -> Dec(B)) -> Dec(B):
4160 match r:
4161 case Fail{e}: Fail{e}
4162 case Done{x}: f(x)
4163
4164type Matches is Data:
4165 Matches{cmd: Cmd, path: List<&2, String>, fields: List<&2, Field>,
4166 external: Maybe<&2, ExternalArgs>}
4167
4168def matches(c: Cmd, r: ParseResult) -> Maybe<&2, Matches>:
4169 match r:
4170 case Accepted{path, fields, external}:
4171 Some{Matches{c, path, fields, external}}
4172 case _: None{}
4173
4174def path(m: Matches) -> List<&2, String>:
4175 match m:
4176 case Matches{cmd, path, fields, external}: path
4177
4178def external_args(m: Matches) -> Maybe<(String & List<&2, String>)>:
4179 match m:
4180 case Matches{cmd, path, fields, None{}}: None{}
4181 case Matches{cmd, path, fields, Some{ExternalArgs{name, args}}}:
4182 Some{(name, args)}
4183
4184def fail(-A: Type, msg: String) -> Dec(A):
4185 Fail{Usage{msg}}
4186
4187def bug(-A: Type, msg: String) -> Dec(A):
4188 Fail{Bug{msg}}
4189
4190def keep(+m: Matches) -> Dec(Matches):
4191 Done{m}
4192
4193# A command path as lint writes it: the root name, then each subcommand.
4194def path_text(name: String, path: List<&2, String>) -> String:
4195 String.join(name <> path, " ")
4196
4197def sub_items(found: Maybe<&2, SubFound>) -> List<&2, Item>:
4198 match found:
4199 case None{}: Nil{}
4200 case Some{SubFound{name, items}}: items
4201
4202# The args in scope at path (§8.1), as parse orders them: globals first.
4203def scope_args(path: List<&2, String>, +items: List<&2, Item>,
4204 globals: List<&2, Item>) -> List<&2, Item>:
4205 match path:
4206 case Nil{}: List.append(&2, Item, globals, items)
4207 case name <> tail:
4208 scope_args(tail, sub_items(find_sub(items, name)),
4209 List.append(&2, Item, global_items(items), globals))
4210
4211type Form is Data:
4212 ReadOne{}
4213 ReadOpt{}
4214 ReadMany{}
4215
4216# What a reader asks of an arg; Q-static checks it against the spec.
4217type Want is Data:
4218 WFlag{}
4219 WCount{}
4220 WValue{stem: String, form: Form}
4221 WAny{}
4222 WChoose{opt: Bool, keys: List<&2, String>}
4223
4224def kind_flag(kind: UsageKind) -> Bool:
4225 match kind:
4226 case FlagKind{}: True{}
4227 case NegatableKind{}: True{}
4228 case _: False{}
4229
4230def kind_count(kind: UsageKind) -> Bool:
4231 match kind:
4232 case CountKind{}: True{}
4233 case _: False{}
4234
4235# The reader name a value type reads with: str, nat, u32, f32 or bool.
4236def type_stem(found: Maybe<&2, ValueType>) -> String:
4237 match found:
4238 case Some{TNat{}}: "nat"
4239 case Some{TU32{}}: "u32"
4240 case Some{TF32{}}: "f32"
4241 case Some{TBool{}}: "bool"
4242 case _: "str"
4243
4244def stem_noun(+stem: String) -> String:
4245 Bool.pick(String, String.eq(stem, "str"), "string", stem)
4246
4247def stem_article(+stem: String) -> String:
4248 Bool.pick(String, String.eq(stem, "f32"), "an ", "a ")
4249
4250def always_present(kind: UsageKind, +attrs: List<&2, Item>) -> Bool:
4251 Bool.or(has_required(attrs), Bool.or(
4252 Maybe.is_some(&2, String, find_default(attrs)), kind_required_pos(kind)))
4253
4254def may_absent(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4255 Bool.and(Bool.not(kind_many(kind)),
4256 Bool.not(always_present(kind, attrs)))
4257
4258def kind_text(kind: UsageKind, +stem: String) -> String:
4259 match kind:
4260 case FlagKind{}: "a flag"
4261 case NegatableKind{}: "a flag"
4262 case CountKind{}: "a count"
4263 case _:
4264 Bool.pick(String, kind_many(kind),
4265 String.concat(["a list of ", stem_noun(stem), " values"]),
4266 String.concat([stem_article(stem), stem_noun(stem), " value"]))
4267
4268def fit_name(+kind: UsageKind, attrs: List<&2, Item>,
4269 +stem: String) -> String:
4270 match kind:
4271 case FlagKind{}: "flag"
4272 case NegatableKind{}: "flag"
4273 case CountKind{}: "count"
4274 case _:
4275 Bool.pick(String, kind_many(kind), String.append(stem, "s"),
4276 Bool.pick(String, always_present(kind, attrs), stem,
4277 String.append(stem, "_opt")))
4278
4279def kind_verdict(fits: Bool, id: String, +kind: UsageKind,
4280 +attrs: List<&2, Item>) -> Maybe<&2, String>:
4281 match fits:
4282 case True{}: None{}
4283 case False{}:
4284 +stem = type_stem(find_as(attrs))
4285 Some{String.concat(["'", id, "' is ", kind_text(kind, stem),
4286 "; read it with Cli.", fit_name(kind, attrs, stem)])}
4287
4288def absent_verdict(absent: Bool, id: String,
4289 reader: String) -> Maybe<&2, String>:
4290 match absent:
4291 case True{}:
4292 Some{String.concat(["'", id, "' may be absent; read it with Cli.",
4293 reader, "_opt"])}
4294 case False{}: None{}
4295
4296def all_in(xs: List<&2, String>, +ys: List<&2, String>) -> Bool:
4297 match xs:
4298 case Nil{}: True{}
4299 case head <> tail:
4300 Bool.and(List.contains(~String, ~String.eq, ys, head),
4301 all_in(tail, ys))
4302
4303def keys_issue(same: Bool, id: String) -> Maybe<&2, String>:
4304 match same:
4305 case True{}: None{}
4306 case False{}:
4307 Some{String.concat(["choose table for '", id,
4308 "' does not match its values"])}
4309
4310def keys_verdict(found: Maybe<&2, List<&2, String>>,
4311 +keys: List<&2, String>, id: String) -> Maybe<&2, String>:
4312 match found:
4313 case None{}: None{}
4314 case Some{+values}:
4315 keys_issue(Bool.and(Bool.not(List.is_empty(&2, String, keys)),
4316 Bool.and(all_in(keys, values), all_in(values, keys))), id)
4317
4318def form_fits(form: Form, many: Bool) -> Bool:
4319 match form:
4320 case ReadMany{}: many
4321 case _: Bool.not(many)
4322
4323def form_one(form: Form) -> Bool:
4324 match form:
4325 case ReadOne{}: True{}
4326 case _: False{}
4327
4328def value_fits(stem: String, form: Form, +kind: UsageKind,
4329 attrs: List<&2, Item>) -> Bool:
4330 Bool.and(kind_valued(kind), Bool.and(
4331 String.eq(stem, type_stem(find_as(attrs))),
4332 form_fits(form, kind_many(kind))))
4333
4334def choose_fits(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4335 Bool.and(kind_valued(kind), Bool.and(Bool.not(kind_many(kind)),
4336 Maybe.is_some(&2, List<&2, String>, find_one_of(attrs))))
4337
4338# The first Q-static message for an arg in scope: kind, absence, then keys.
4339def want_verdict(want: Want, +id: String, +kind: UsageKind,
4340 +attrs: List<&2, Item>) -> Maybe<&2, String>:
4341 match want:
4342 case WFlag{}:
4343 kind_verdict(kind_flag(kind), id, kind, attrs)
4344 case WCount{}:
4345 kind_verdict(kind_count(kind), id, kind, attrs)
4346 case WValue{+stem, +form}:
4347 Maybe.or(&2, String,
4348 kind_verdict(value_fits(stem, form, kind, attrs), id, kind, attrs),
4349 absent_verdict(Bool.and(form_one(form), may_absent(kind, attrs)),
4350 id, stem))
4351 case WAny{}:
4352 None{}
4353 case WChoose{opt, keys}:
4354 Maybe.or(&2, String,
4355 kind_verdict(choose_fits(kind, attrs), id, kind, attrs),
4356 Maybe.or(&2, String,
4357 absent_verdict(Bool.and(Bool.not(opt), may_absent(kind, attrs)),
4358 id, "choose"),
4359 keys_verdict(find_one_of(attrs), keys, id)))
4360
4361def arg_verdict(found: Maybe<&2, ArgInfo>, id: String, name: String,
4362 path: List<&2, String>, want: Want) -> Maybe<&2, String>:
4363 match found:
4364 case None{}:
4365 Some{String.concat(["'", id, "' is not an argument of '",
4366 path_text(name, path), "'"])}
4367 case Some{ArgInfo{key, usage, short, long, kind, attrs}}:
4368 want_verdict(want, id, kind, attrs)
4369
4370# Q-static: depends only on the spec, the path, the id and the want.
4371def verdict(cmd: Cmd, +path: List<&2, String>, +id: String,
4372 want: Want) -> Maybe<&2, String>:
4373 match cmd:
4374 case Cmd{name, about, items}:
4375 arg_verdict(find_arg_info(scope_args(path, items, []), id), id, name,
4376 path, want)
4377
4378def field_found(f: Field, id: String) -> Maybe<&2, Field>:
4379 match f:
4380 case Field{+key, values, source}:
4381 Bool.pick(Maybe<&2, Field>, same_text(key, id),
4382 Some{Field{key, values, source}}, None{})
4383
4384# The field for id, or an absent one; after a hit it walks no further.
4385def field_seek(fields: List<&2, Field>, +id: String,
4386 found: Maybe<&2, Field>) -> Field:
4387 match fields found:
4388 case _ Some{f}: f
4389 case Nil{} None{}: Field{id, [], Absent{}}
4390 case f <> tail None{}: field_seek(tail, id, field_found(f, id))
4391
4392def field_find(fields: List<&2, Field>, +id: String) -> Field:
4393 field_seek(fields, id, None{})
4394
4395# Validation first; reading a validated value never fails (§6.5).
4396def read_check(~T: Data, ~get: Field -> T, verdict: Maybe<&2, String>,
4397 fields: List<&2, Field>, id: String) -> Dec(T):
4398 match verdict:
4399 case None{}: Done{get(field_find(fields, id))}
4400 case Some{msg}: Fail{Bug{msg}}
4401
4402def read_with(~T: Data, ~get: Field -> T, want: Want, m: Matches,
4403 +id: String) -> Dec(T):
4404 match m:
4405 case Matches{cmd, path, fields, external}:
4406 read_check(~T, ~get, verdict(cmd, path, id, want), fields, id)
4407
4408def get_one(~T: Data, ~conv: String -> T, f: Field) -> T:
4409 match f:
4410 case Field{id, head <> tail, source}: conv(head)
4411 case _: conv("")
4412
4413def get_opt(~T: Data, ~conv: String -> T, f: Field) -> Maybe<&2, T>:
4414 match f:
4415 case Field{id, head <> tail, source}: Some{conv(head)}
4416 case _: None{}
4417
4418def convert_all(~T: Data, ~conv: String -> T, values: List<&2, String>) ->
4419 List<&2, T>:
4420 match values:
4421 case Nil{}: Nil{}
4422 case head <> tail: conv(head) <> convert_all(~T, ~conv, tail)
4423
4424def get_many(~T: Data, ~conv: String -> T, f: Field) -> List<&2, T>:
4425 match f:
4426 case Field{id, values, source}: convert_all(~T, ~conv, values)
4427
4428def true_of(s: String) -> Bool:
4429 String.eq(s, "true")
4430
4431def nat_of(s: String) -> Nat:
4432 Maybe.default(&2, Nat, Nat.read(s), 0n)
4433
4434def u32_of(s: String) -> U32:
4435 Maybe.default(&2, U32, U32.read(s), 0)
4436
4437def f32_of(s: String) -> F32:
4438 Maybe.default(&2, F32, F32.read(s), 0.0)
4439
4440def flag(m: Matches, id: String) -> Dec(Bool):
4441 read_with(~Bool, ~(f => get_one(~Bool, ~true_of, f)), WFlag{}, m, id)
4442
4443def count(m: Matches, id: String) -> Dec(Nat):
4444 read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4445 WCount{}, m, id)
4446
4447def str(m: Matches, id: String) -> Dec(String):
4448 read_with(~String, ~(f => get_one(~String, ~(s => s), f)),
4449 WValue{"str", ReadOne{}}, m, id)
4450
4451def str_opt(m: Matches, id: String) -> Dec(Maybe<&2, String>):
4452 read_with(~Maybe<&2, String>, ~(f => get_opt(~String, ~(s => s), f)),
4453 WValue{"str", ReadOpt{}}, m, id)
4454
4455def strs(m: Matches, id: String) -> Dec(List<&2, String>):
4456 read_with(~List<&2, String>, ~(f => get_many(~String, ~(s => s), f)),
4457 WValue{"str", ReadMany{}}, m, id)
4458
4459def nat(m: Matches, id: String) -> Dec(Nat):
4460 read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4461 WValue{"nat", ReadOne{}}, m, id)
4462
4463def nat_opt(m: Matches, id: String) -> Dec(Maybe<&2, Nat>):
4464 read_with(~Maybe<&2, Nat>, ~(f => get_opt(~Nat, ~nat_of, f)),
4465 WValue{"nat", ReadOpt{}}, m, id)
4466
4467def nats(m: Matches, id: String) -> Dec(List<&2, Nat>):
4468 read_with(~List<&2, Nat>, ~(f => get_many(~Nat, ~nat_of, f)),
4469 WValue{"nat", ReadMany{}}, m, id)
4470
4471def u32(m: Matches, id: String) -> Dec(U32):
4472 read_with(~U32, ~(f => get_one(~U32, ~u32_of, f)),
4473 WValue{"u32", ReadOne{}}, m, id)
4474
4475def u32_opt(m: Matches, id: String) -> Dec(Maybe<&2, U32>):
4476 read_with(~Maybe<&2, U32>, ~(f => get_opt(~U32, ~u32_of, f)),
4477 WValue{"u32", ReadOpt{}}, m, id)
4478
4479def u32s(m: Matches, id: String) -> Dec(List<&2, U32>):
4480 read_with(~List<&2, U32>, ~(f => get_many(~U32, ~u32_of, f)),
4481 WValue{"u32", ReadMany{}}, m, id)
4482
4483def f32(m: Matches, id: String) -> Dec(F32):
4484 read_with(~F32, ~(f => get_one(~F32, ~f32_of, f)),
4485 WValue{"f32", ReadOne{}}, m, id)
4486
4487def f32_opt(m: Matches, id: String) -> Dec(Maybe<&2, F32>):
4488 read_with(~Maybe<&2, F32>, ~(f => get_opt(~F32, ~f32_of, f)),
4489 WValue{"f32", ReadOpt{}}, m, id)
4490
4491def f32s(m: Matches, id: String) -> Dec(List<&2, F32>):
4492 read_with(~List<&2, F32>, ~(f => get_many(~F32, ~f32_of, f)),
4493 WValue{"f32", ReadMany{}}, m, id)
4494
4495def bool(m: Matches, id: String) -> Dec(Bool):
4496 read_with(~Bool, ~(f => get_one(~Bool, ~env_bool_true, f)),
4497 WValue{"bool", ReadOne{}}, m, id)
4498
4499def bool_opt(m: Matches, id: String) -> Dec(Maybe<&2, Bool>):
4500 read_with(~Maybe<&2, Bool>, ~(f => get_opt(~Bool, ~env_bool_true, f)),
4501 WValue{"bool", ReadOpt{}}, m, id)
4502
4503def bools(m: Matches, id: String) -> Dec(List<&2, Bool>):
4504 read_with(~List<&2, Bool>, ~(f => get_many(~Bool, ~env_bool_true, f)),
4505 WValue{"bool", ReadMany{}}, m, id)
4506
4507def field_src(f: Field) -> Src:
4508 match f:
4509 case Field{id, values, source}: source
4510
4511def given(m: Matches, id: String) -> Dec(Bool):
4512 read_with(~Bool, ~(f => source_given(field_src(f))), WAny{}, m, id)
4513
4514def source(m: Matches, id: String) -> Dec(Src):
4515 read_with(~Src, ~field_src, WAny{}, m, id)
4516
4517def by_key_hit(a, -A: Kind(a), same: Bool, value: A) -> Maybe<a, A>:
4518 match same:
4519 case True{}: Some{value}
4520 case False{}: None{}
4521
4522# The value under the first key equal to key; after a hit it walks no further.
4523def find_by_key_go(a, -A: Kind(a), keys: List<&2, String>,
4524 values: List<a, A>, +key: String, found: Maybe<a, A>) -> Maybe<a, A>:
4525 match keys values found:
4526 case _ _ Some{hit}: Some{hit}
4527 case head <> tail value <> rest None{}:
4528 find_by_key_go(a, A, tail, rest, key,
4529 by_key_hit(a, A, same_text(head, key), value))
4530 case _ _ None{}: None{}
4531
4532def find_by_key(a, -A: Kind(a), keys: List<&2, String>, values: List<a, A>,
4533 +key: String) -> Maybe<a, A>:
4534 find_by_key_go(a, A, keys, values, key, None{})
4535
4536def keyed_cons(a, -A: Kind(a), key: String, value: A,
4537 rest: (List<&2, String> & List<a, A>)) -> (List<&2, String> & List<a, A>):
4538 (keys, values) = rest
4539 (key <> keys, value <> values)
4540
4541# A choose or route table as its keys and its values.
4542def unzip_keyed(a, -A: Kind(a), table: List<(String & A)>) ->
4543 (List<&2, String> & List<a, A>):
4544 match table:
4545 case Nil{}: ([], [])
4546 case entry <> tail:
4547 (key, value) = entry
4548 keyed_cons(a, A, key, value, unzip_keyed(a, A, tail))
4549
4550# After validation the key exists: parse and samples keep one_of values.
4551def choice_dec(-A: Data, found: Maybe<&2, A>) -> Dec(Maybe<&2, A>):
4552 match found:
4553 case Some{value}: Done{Some{value}}
4554 case None{}: Fail{Usage{"the value has no choose entry"}}
4555
4556def choose_read(-A: Data, keys: List<&2, String>, values: List<&2, A>,
4557 f: Field) -> Dec(Maybe<&2, A>):
4558 match f:
4559 case Field{id, head <> tail, source}:
4560 choice_dec(A, find_by_key(&2, A, keys, values, head))
4561 case _: Done{None{}}
4562
4563def choose_check(-A: Data, verdict: Maybe<&2, String>,
4564 keys: List<&2, String>, values: List<&2, A>,
4565 fields: List<&2, Field>, id: String) -> Dec(Maybe<&2, A>):
4566 match verdict:
4567 case None{}: choose_read(A, keys, values, field_find(fields, id))
4568 case Some{msg}: Fail{Bug{msg}}
4569
4570def choose_pick(-A: Data, opt: Bool,
4571 choices: (List<&2, String> & List<&2, A>), m: Matches,
4572 +id: String) -> Dec(Maybe<&2, A>):
4573 (+keys, values) = choices
4574 match m:
4575 case Matches{cmd, path, fields, external}:
4576 choose_check(A, verdict(cmd, path, id, WChoose{opt, keys}),
4577 keys, values, fields, id)
4578
4579def choose_opt(-A: Data, m: Matches, id: String,
4580 table: List<(String & A)>) -> Dec(Maybe<&2, A>):
4581 choose_pick(A, True{}, unzip_keyed(&2, A, table), m, id)
4582
4583# After validation a bare choose always has a value.
4584def choice_must(-A: Data, d: Dec(Maybe<&2, A>)) -> Dec(A):
4585 match d:
4586 case Fail{e}: Fail{e}
4587 case Done{Some{value}}: Done{value}
4588 case Done{None{}}: Fail{Usage{"the value is absent"}}
4589
4590def choose(-A: Data, m: Matches, id: String,
4591 table: List<(String & A)>) -> Dec(A):
4592 choice_must(A, choose_pick(A, False{}, unzip_keyed(&2, A, table), m, id))
4593
4594def Call(-A: Type) -> Type:
4595 @+m: Matches -> Dec(A)
4596
4597# Some{None{}} when text is the word name, Some{Some{rest}} when it is name,
4598# a space and rest, None{} otherwise. Words are cut at spaces, so a name with
4599# a space (Chr{32}) is no word.
4600def word_rest(name: String, text: String, same: Bool) ->
4601 Maybe<&2, Maybe<&2, String>>:
4602 match name text same:
4603 case _ _ False{}: None{}
4604 case SNil{} SNil{} True{}: Some{None{}}
4605 case SNil{} SCon{Chr{32}, rest} True{}: Some{Some{rest}}
4606 case SCon{Chr{32}, xs} _ True{}: None{}
4607 case SCon{x, xs} SCon{y, ys} True{}: word_rest(xs, ys, Char.is_eq(x, y))
4608 case _ _ _: None{}
4609
4610# How a route key's next word meets an item: not its subcommand, the
4611# subcommand of a built-in, the command the key ends at, or a command whose
4612# subcommands take the rest (§6.4).
4613type KeyStep is Data:
4614 KeyMiss{}
4615 KeyStop{}
4616 KeyEnd{}
4617 KeyNext{rest: String}
4618
4619def key_user(builtin: Bool, rest: Maybe<&2, String>) -> KeyStep:
4620 match builtin rest:
4621 case True{} _: KeyStop{}
4622 case False{} None{}: KeyEnd{}
4623 case False{} Some{text}: KeyNext{text}
4624
4625def key_word(found: Maybe<&2, Maybe<&2, String>>,
4626 children: List<&2, Item>) -> KeyStep:
4627 match found:
4628 case None{}: KeyMiss{}
4629 case Some{rest}: key_user(has_builtin(children), rest)
4630
4631def key_step(items: List<&2, Item>, text: String) -> KeyStep:
4632 match items:
4633 case Sub{name, about, children} <> tail:
4634 key_word(word_rest(name, text, True{}), children)
4635 case _: KeyMiss{}
4636
4637# The first subcommand named by each word of text, from items down, is a
4638# command.
4639def key_go(items: List<&2, Item>, +text: String, step: KeyStep) -> Bool:
4640 match items step:
4641 case Nil{} _: False{}
4642 case head <> +tail KeyMiss{}: key_go(tail, text, key_step(tail, text))
4643 case head <> tail KeyStop{}: False{}
4644 case head <> tail KeyEnd{}: True{}
4645 case Sub{name, about, +children} <> tail KeyNext{+rest}:
4646 key_go(children, rest, key_step(children, rest))
4647 case _ _: False{}
4648
4649def route_text(empty: Bool, +key: String, +items: List<&2, Item>) -> Bool:
4650 match empty:
4651 case True{}: True{}
4652 case False{}: key_go(items, key, key_step(items, key))
4653
4654# A route key's words name a command; "" is the root.
4655def route_key(+key: String, items: List<&2, Item>) -> Bool:
4656 route_text(String.is_empty(key), key, items)
4657
4658def non_command(keys: List<&2, String>, +items: List<&2, Item>) ->
4659 Maybe<&2, String>:
4660 match keys:
4661 case Nil{}: None{}
4662 case +key <> tail:
4663 Bool.pick(Maybe<&2, String>, route_key(key, items),
4664 non_command(tail, items), Some{key})
4665
4666def route_call(-A: Type, bad: Maybe<&2, String>, call: Call(A),
4667 m: Matches) -> Dec(A):
4668 match bad:
4669 case None{}: call(m)
4670 case Some{key}:
4671 Fail{Bug{String.concat(["route '", key, "' is not a command"])}}
4672
4673# No route for the path is checked first, then every key names a command.
4674def route_found(-A: Type, found: Maybe<Call(A)>, keys: List<&2, String>,
4675 cmd: Cmd, path: List<&2, String>, m: Matches) -> Dec(A):
4676 match found cmd:
4677 case None{} Cmd{name, about, items}:
4678 Fail{Bug{String.concat(["no route for '", path_text(name, path), "'"])}}
4679 case Some{call} Cmd{name, about, items}:
4680 route_call(A, non_command(keys, items), call, m)
4681
4682def route_pick(-A: Type, routes: (List<&2, String> & List<Call(A)>),
4683 m: Matches) -> Dec(A):
4684 (+keys, calls) = routes
4685 match m:
4686 case Matches{+cmd, +path, fields, external}:
4687 route_found(A,
4688 find_by_key(&1, Call(A), keys, calls, String.join(path, " ")),
4689 keys, cmd, path, Matches{cmd, path, fields, external})
4690
4691def route(-A: Type, m: Matches, table: List<(String & Call(A))>) -> Dec(A):
4692 route_pick(A, unzip_keyed(&1, Call(A), table), m)
4693
4694# ---- Samples and check (§6.6) ----
4695
4696def sample_typed(found: Maybe<&2, ValueType>) -> String:
4697 match found:
4698 case Some{TNat{}}: "0"
4699 case Some{TU32{}}: "0"
4700 case Some{TF32{}}: "0.0"
4701 case Some{TBool{}}: "false"
4702 case _: "x"
4703
4704def sample_choice(found: Maybe<&2, List<&2, String>>,
4705 attrs: List<&2, Item>) -> String:
4706 match found:
4707 case Some{head <> tail}: head
4708 case _: sample_typed(find_as(attrs))
4709
4710def sample_value(kind: UsageKind, +attrs: List<&2, Item>) -> String:
4711 match kind:
4712 case FlagKind{}: "true"
4713 case NegatableKind{}: "true"
4714 case CountKind{}: "1"
4715 case _: sample_choice(find_one_of(attrs), attrs)
4716
4717def sample_field(build: FieldBuild, info: ArgInfo) -> List<&2, Field>:
4718 match build:
4719 case Built{Nil{}}:
4720 match info:
4721 case ArgInfo{id, usage, short, long, kind, attrs}:
4722 [Field{id, [sample_value(kind, attrs)], Argv{}}]
4723 case Built{fields}: fields
4724 case BuildFault{fault}: Nil{}
4725
4726# Each arg in scope: its default as parse resolves it, else a sample value.
4727def sample_args(items: List<&2, Item>) -> List<&2, Field>:
4728 match items:
4729 case Nil{}: Nil{}
4730 case Arg{usage, help, attrs} <> tail:
4731 +info = arg_info(usage, attrs)
4732 List.append(&2, Field,
4733 sample_field(resolve_default(info_default(info), info), info),
4734 sample_args(tail))
4735 case head <> tail: sample_args(tail)
4736
4737def sample_fields(c: Cmd, path: List<&2, String>) -> List<&2, Field>:
4738 match c:
4739 case Cmd{name, about, items}: sample_args(scope_args(path, items, []))
4740
4741def sample(+c: Cmd, +path: List<&2, String>) -> Matches:
4742 Matches{c, path, sample_fields(c, path), None{}}
4743
4744# The paths below prefix that can complete, in pre-order, without built-ins.
4745def sub_paths(items: List<&2, Item>, +prefix: List<&2, String>) ->
4746 List<&2, List<&2, String>>:
4747 match items:
4748 case Nil{}: Nil{}
4749 case Sub{name, about, +children} <> tail:
4750 +path = List.append(&2, String, prefix, [name])
4751 List.concat(&2, List<&2, String>, [
4752 Bool.pick(List<&2, List<&2, String>>,
4753 Bool.or(has_builtin(children), needs_sub(children)), [], [path]),
4754 sub_paths(children, path),
4755 sub_paths(tail, prefix),
4756 ])
4757 case head <> tail: sub_paths(tail, prefix)
4758
4759def sample_paths(c: Cmd) -> List<&2, List<&2, String>>:
4760 match c:
4761 case Cmd{name, about, +items}:
4762 List.append(&2, List<&2, String>,
4763 Bool.pick(List<&2, List<&2, String>>, needs_sub(items), [], [[]]),
4764 sub_paths(items, []))
4765
4766def decode_bug(-A: Type, d: Dec(A), at: String) -> List<&2, Problem>:
4767 match d:
4768 case Fail{Bug{msg}}: [DecodeBug{at, msg}]
4769 case _: Nil{}
4770
4771def cmd_name(c: Cmd) -> String:
4772 match c:
4773 case Cmd{name, about, items}: name
4774
4775def sample_bugs(~A: Type, ~decode: @+m: Matches -> Dec(A),
4776 paths: List<&2, List<&2, String>>, +c: Cmd) -> List<&2, Problem>:
4777 match paths:
4778 case Nil{}: Nil{}
4779 case +path <> tail:
4780 List.append(&2, Problem,
4781 decode_bug(A, decode(sample(c, path)), path_text(cmd_name(c), path)),
4782 sample_bugs(~A, ~decode, tail, c))
4783
4784def check(~A: Type, ~decode: @+m: Matches -> Dec(A), +c: Cmd) ->
4785 List<&2, Problem>:
4786 List.append(&2, Problem, problems(c),
4787 sample_bugs(~A, ~decode, sample_paths(c), c))
4788# ---- Color (§10.6) ----
4789
4790type Term is Data:
4791 Term{out_color: Bool, err_color: Bool, width: Nat}
4792
4793def host_tty(h: Host, out: Bool, err: Bool) -> Host:
4794 match h:
4795 case Host{width, vars, out_tty, err_tty}: Host{width, vars, out, err}
4796
4797# Rules 1 and 2 apply only to color() exactly as declared.
4798def color_attrs(attrs: List<&2, Item>) -> Bool:
4799 match attrs:
4800 case Con{OneOf{values}, Con{AttrDefault{value}, Con{Global{}, Nil{}}}}:
4801 Bool.and(String.eq(String.join(values, " "), "auto always never"),
4802 String.eq(value, "auto"))
4803 case _: False{}
4804
4805def is_color(item: Item) -> Bool:
4806 match item:
4807 case Arg{usage, help, attrs}:
4808 Bool.and(String.eq(usage, "--color <WHEN>"),
4809 Bool.and(String.eq(help, "When to use color"), color_attrs(attrs)))
4810 case _: False{}
4811
4812def has_color(items: List<&2, Item>) -> Bool:
4813 match items:
4814 case Nil{}: False{}
4815 case item <> tail: Bool.or(is_color(item), has_color(tail))
4816
4817# The pre-scan of argv: the last --color value before `--`, "" for a bare
4818# --color before a hyphen token.
4819type Prescan is Data:
4820 ColorSeek{last: Maybe<&2, String>}
4821 ColorWant{}
4822 ColorStop{last: Maybe<&2, String>}
4823
4824def color_seek(dash: Bool, bare: Bool, attached: Bool, token: String,
4825 last: Maybe<&2, String>) -> Prescan:
4826 match dash bare attached:
4827 case True{} _ _: ColorStop{last}
4828 case False{} True{} _: ColorWant{}
4829 case False{} False{} True{}: ColorSeek{Some{String.drop(token, 8n)}}
4830 case False{} False{} False{}: ColorSeek{last}
4831
4832def color_want(hyphen: Bool, +token: String) -> Prescan:
4833 match hyphen:
4834 case False{}: ColorSeek{Some{token}}
4835 case True{}:
4836 color_seek(String.eq(token, "--"), String.eq(token, "--color"),
4837 String.starts_with(token, "--color="), token, Some{""})
4838
4839def color_step(state: Prescan, +token: String) -> Prescan:
4840 match state:
4841 case ColorSeek{last}:
4842 color_seek(String.eq(token, "--"), String.eq(token, "--color"),
4843 String.starts_with(token, "--color="), token, last)
4844 case ColorWant{}: color_want(String.starts_with(token, "-"), token)
4845 case ColorStop{last}: ColorStop{last}
4846
4847def color_scan(argv: List<&2, String>, state: Prescan) -> Prescan:
4848 match argv:
4849 case Nil{}: state
4850 case token <> tail: color_scan(tail, color_step(state, token))
4851
4852def color_word(always: Bool, never: Bool) -> Maybe<&2, Bool>:
4853 match always never:
4854 case True{} _: Some{True{}}
4855 case False{} True{}: Some{False{}}
4856 case False{} False{}: None{}
4857
4858def color_value(found: Maybe<&2, String>) -> Maybe<&2, Bool>:
4859 match found:
4860 case None{}: None{}
4861 case Some{+value}:
4862 color_word(String.eq(value, "always"), String.eq(value, "never"))
4863
4864def color_pick(state: Prescan) -> Maybe<&2, Bool>:
4865 match state:
4866 case ColorSeek{last}: color_value(last)
4867 case ColorWant{}: None{}
4868 case ColorStop{last}: color_value(last)
4869
4870def color_flag(enabled: Bool, argv: List<&2, String>) -> Maybe<&2, Bool>:
4871 match enabled:
4872 case True{}: color_pick(color_scan(argv, ColorSeek{None{}}))
4873 case False{}: None{}
4874
4875# An empty variable counts as unset.
4876def var_set(found: Maybe<&2, String>) -> Bool:
4877 match found:
4878 case None{}: False{}
4879 case Some{value}: Bool.not(String.is_empty(value))
4880
4881def var_is(found: Maybe<&2, String>, text: String) -> Bool:
4882 match found:
4883 case None{}: False{}
4884 case Some{value}: String.eq(value, text)
4885
4886def force_set(found: Maybe<&2, String>) -> Bool:
4887 match found:
4888 case None{}: False{}
4889 case Some{+value}:
4890 Bool.and(Bool.not(String.is_empty(value)),
4891 Bool.not(String.eq(value, "0")))
4892
4893def env_off(off: Bool) -> Maybe<&2, Bool>:
4894 match off:
4895 case True{}: Some{False{}}
4896 case False{}: None{}
4897
4898def env_force(force: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
4899 match force:
4900 case True{}: Some{True{}}
4901 case False{}:
4902 env_off(Bool.or(var_is(var_value(vars, "CLICOLOR"), "0"),
4903 var_is(var_value(vars, "TERM"), "dumb")))
4904
4905def env_rules(no_color: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
4906 match no_color:
4907 case True{}: Some{False{}}
4908 case False{}:
4909 env_force(force_set(var_value(vars, "CLICOLOR_FORCE")), vars)
4910
4911# Rules 3-6; None leaves rule 7, each stream's TTY.
4912def env_color(+vars: List<&2, Var>) -> Maybe<&2, Bool>:
4913 env_rules(var_set(var_value(vars, "NO_COLOR")), vars)
4914
4915def term_env(found: Maybe<&2, Bool>, out_tty: Bool, err_tty: Bool,
4916 width: Nat) -> Term:
4917 match found:
4918 case Some{+on}: Term{on, on, width}
4919 case None{}: Term{out_tty, err_tty, width}
4920
4921def term_for(flag: Maybe<&2, Bool>, vars: List<&2, Var>, out_tty: Bool,
4922 err_tty: Bool, width: Nat) -> Term:
4923 match flag:
4924 case Some{+on}: Term{on, on, width}
4925 case None{}: term_env(env_color(vars), out_tty, err_tty, width)
4926
4927def term(c: Cmd, h: Host, argv: List<&2, String>) -> Term:
4928 match c h:
4929 case Cmd{name, about, items} Host{width, vars, out_tty, err_tty}:
4930 term_for(color_flag(has_color(items), argv), vars, out_tty, err_tty,
4931 width)
4932
4933# ---- Help (§10.2, §10.3) ----
4934
4935def plain(text: String) -> Segment:
4936 Segment{text, []}
4937
4938def page_width(width: Nat) -> Nat:
4939 Nat.min(100n, Nat.max(40n, width))
4940
4941# The first paragraph: the text before the first blank line.
4942def summary(s: String) -> String:
4943 match s:
4944 case SNil{}: SNil{}
4945 case SCon{Chr{10}, SCon{Chr{10}, tail}}: SNil{}
4946 case SCon{head, tail}: SCon{head, summary(tail)}
4947
4948def shown_text(long: Bool, text: String) -> String:
4949 match long:
4950 case True{}: text
4951 case False{}: summary(text)
4952
4953def filled(s: String) -> Bool:
4954 Bool.not(String.is_empty(s))
4955
4956def join_filled(parts: List<&2, String>) -> String:
4957 String.join(List.filter(~String, ~filled, parts), " ")
4958
4959# The command at the longest valid prefix of a path, with the globals it
4960# inherits, root first; stuck once a step names no subcommand.
4961type View is Data:
4962 View{path: String, about: String, items: List<&2, Item>,
4963 globals: List<&2, Item>, root: Bool, stuck: Bool}
4964
4965def find_command(items: List<&2, Item>, +query: String,
4966 found: Maybe<&2, Item>) -> Maybe<&2, Item>:
4967 match items found:
4968 case _ Some{item}: Some{item}
4969 case Nil{} None{}: None{}
4970 case Sub{+name, about, +children} <> tail None{}:
4971 find_command(tail, query, Bool.pick(Maybe<&2, Item>,
4972 Bool.or(String.eq(query, name),
4973 List.contains(~String, ~String.eq, find_aliases(children), query)),
4974 Some{Sub{name, about, children}}, None{}))
4975 case head <> tail None{}: find_command(tail, query, None{})
4976
4977def view_enter(found: Maybe<&2, Item>, path: String, about: String,
4978 items: List<&2, Item>, globals: List<&2, Item>, root: Bool) -> View:
4979 match found:
4980 case Some{Sub{name, sub_about, children}}:
4981 View{String.concat([path, " ", name]), sub_about, children,
4982 List.append(&2, Item, globals, global_items(items)), False{}, False{}}
4983 case _: View{path, about, items, globals, root, True{}}
4984
4985def view_step(view: View, step: String) -> View:
4986 match view:
4987 case View{path, about, items, globals, root, True{}}:
4988 View{path, about, items, globals, root, True{}}
4989 case View{path, about, +items, globals, root, False{}}:
4990 view_enter(find_command(items, step, None{}), path, about, items,
4991 globals, root)
4992
4993def view_walk(steps: List<&2, String>, view: View) -> View:
4994 match steps:
4995 case Nil{}: view
4996 case step <> tail: view_walk(tail, view_step(view, step))
4997
4998def view_of(c: Cmd, path: List<&2, String>) -> View:
4999 match c:
5000 case Cmd{name, about, items}:
5001 view_walk(path, View{name, about, items, [], True{}, False{}})
5002
5003def arg_pos(usage: String) -> Bool:
5004 usage_kind(~kind_pos, parse_usage(usage))
5005
5006def has_plain_option(items: List<&2, Item>) -> Bool:
5007 match items:
5008 case Nil{}: False{}
5009 case Arg{usage, help, +attrs} <> tail:
5010 Bool.or(Bool.and(Bool.not(arg_pos(usage)),
5011 Bool.and(Bool.not(has_hidden(attrs)), Bool.not(has_required(attrs)))),
5012 has_plain_option(tail))
5013 case head <> tail: has_plain_option(tail)
5014
5015def required_elem(required: Bool, usage: String, attrs: List<&2, Item>) ->
5016 List<&2, Segment>:
5017 match required:
5018 case True{}: [plain(arg_display(arg_info(usage, attrs)))]
5019 case False{}: []
5020
5021def required_options(items: List<&2, Item>) -> List<&2, Segment>:
5022 match items:
5023 case Nil{}: Nil{}
5024 case Arg{+usage, help, +attrs} <> tail:
5025 List.append(&2, Segment,
5026 required_elem(Bool.and(Bool.not(arg_pos(usage)), has_required(attrs)),
5027 usage, attrs),
5028 required_options(tail))
5029 case head <> tail: required_options(tail)
5030
5031def group_elem(listed: Bool, args: List<&2, Item>, group: String) ->
5032 List<&2, Segment>:
5033 match listed:
5034 case True{}: []
5035 case False{}: [plain(group_display(args, group))]
5036
5037# Each need_one group of items, over args, that done does not name yet.
5038def group_elems(items: List<&2, Item>, +args: List<&2, Item>,
5039 +done: List<&2, String>) -> List<&2, Segment>:
5040 match items:
5041 case Nil{}: Nil{}
5042 case NeedOne{+group} <> tail:
5043 List.append(&2, Segment,
5044 group_elem(List.contains(~String, ~String.eq, done, group), args,
5045 group),
5046 group_elems(tail, args, group <> done))
5047 case head <> tail: group_elems(tail, args, done)
5048
5049def positional_elems(items: List<&2, Item>) -> List<&2, Segment>:
5050 match items:
5051 case Nil{}: Nil{}
5052 case Arg{+usage, help, attrs} <> tail:
5053 +rest = positional_elems(tail)
5054 Bool.pick(List<&2, Segment>, arg_pos(usage), plain(usage) <> rest, rest)
5055 case head <> tail: positional_elems(tail)
5056
5057def command_elems(subs: Bool, need: Bool) -> List<&2, Segment>:
5058 match subs need:
5059 case False{} _: []
5060 case True{} True{}: [plain("<COMMAND>")]
5061 case True{} False{}: [plain("[COMMAND]")]
5062
5063def options_elem(present: Bool) -> List<&2, Segment>:
5064 match present:
5065 case True{}: [plain("[OPTIONS]")]
5066 case False{}: []
5067
5068# The path, [OPTIONS], required options, need_one groups as E06 writes them,
5069# positionals, then the subcommand slot.
5070def usage_elems(path: String, +items: List<&2, Item>,
5071 +globals: List<&2, Item>) -> List<&2, Segment>:
5072 +args = List.append(&2, Item, items, globals)
5073 +scope = List.append(&2, Item, globals, items)
5074 List.concat(&2, Segment, [
5075 [Segment{path, [Bold{}]}],
5076 options_elem(has_plain_option(args)),
5077 required_options(args),
5078 group_elems(scope, scope, []),
5079 positional_elems(items),
5080 command_elems(has_sub_item(items), needs_sub(items)),
5081 ])
5082
5083def segment_texts(segs: List<&2, Segment>) -> List<&2, String>:
5084 match segs:
5085 case Nil{}: Nil{}
5086 case Segment{text, styles} <> tail: text <> segment_texts(tail)
5087
5088def usage_view(view: View) -> String:
5089 match view:
5090 case View{path, about, items, globals, root, stuck}:
5091 String.join(segment_texts(usage_elems(path, items, globals)), " ")
5092
5093def usage(c: Cmd, path: List<&2, String>) -> String:
5094 usage_view(view_of(c, path))
5095
5096# Usage wraps between elements and continues at column 7.
5097def usage_rest(elems: List<&2, Segment>, +w: Nat, +col: Nat) ->
5098 List<&2, Segment>:
5099 match elems:
5100 case Nil{}: Nil{}
5101 case Segment{+text, styles} <> tail:
5102 +ew = width(text)
5103 +fits = Nat.is_le((col + 1n + ew : Nat), w)
5104 plain(Bool.pick(String, fits, " ", "\n ")) <>
5105 Segment{text, styles} <> usage_rest(tail, w,
5106 Bool.pick(Nat, fits, (col + 1n + ew : Nat), (7n + ew : Nat)))
5107
5108def usage_block(elems: List<&2, Segment>, w: Nat) -> List<&2, Segment>:
5109 match elems:
5110 case Nil{}: [Segment{"Usage:", [Bold{}, Under{}]}]
5111 case Segment{+text, styles} <> tail:
5112 Segment{"Usage:", [Bold{}, Under{}]} <> plain(" ") <>
5113 Segment{text, styles} <> usage_rest(tail, w, (7n + width(text) : Nat))
5114
5115# A help row: its left cell, whether it is an option with a short or a
5116# long-only option, and its right text.
5117type Row is Data:
5118 Row{left: List<&2, Segment>, short: Bool, long_only: Bool, right: String}
5119
5120type Section is Data:
5121 Section{title: String, rows: List<&2, Row>}
5122
5123def tag(name: String, values: List<&2, String>) -> String:
5124 match values:
5125 case Nil{}: ""
5126 case head <> tail:
5127 String.concat(["[", name, ": ", String.join(head <> tail, ", "), "]"])
5128
5129def maybe_list(found: Maybe<&2, String>) -> List<&2, String>:
5130 match found:
5131 case None{}: Nil{}
5132 case Some{value}: [value]
5133
5134def list_or_none(found: Maybe<&2, List<&2, String>>) -> List<&2, String>:
5135 match found:
5136 case None{}: Nil{}
5137 case Some{values}: values
5138
5139def right_text(long: Bool, help: String, +attrs: List<&2, Item>) -> String:
5140 join_filled([
5141 shown_text(long, help),
5142 tag("env", find_envs(attrs)),
5143 tag("default", maybe_list(find_default(attrs))),
5144 tag("possible values", list_or_none(find_one_of(attrs))),
5145 tag("aliases", find_aliases(attrs)),
5146 ])
5147
5148def bold_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5149 match names:
5150 case Nil{}: Nil{}
5151 case name <> tail:
5152 List.append(&2, Segment,
5153 Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5154 Segment{String.trim_start(name), [Bold{}]} <> bold_names(tail, False{}))
5155
5156# Where an option's value or repeat starts: " <", "[=", or a final "...".
5157def value_cut(s: String) -> Nat:
5158 match s:
5159 case SNil{}: 0n
5160 case SCon{Chr{32}, SCon{Chr{60}, tail}}: 0n
5161 case SCon{Chr{91}, SCon{Chr{61}, tail}}: 0n
5162 case SCon{Chr{46}, SCon{Chr{46}, SCon{Chr{46}, SNil{}}}}: 0n
5163 case SCon{head, tail}: 1n+value_cut(tail)
5164
5165# Option names bold; separators, value and repeat plain.
5166def option_left(+usage: String) -> List<&2, Segment>:
5167 +cut = value_cut(usage)
5168 List.append(&2, Segment,
5169 bold_names(String.split(String.take(usage, cut), ','), True{}),
5170 [plain(String.drop(usage, cut))])
5171
5172def arg_row(parsed: Usage, usage: String, help: String,
5173 attrs: List<&2, Item>, long: Bool) -> Row:
5174 match parsed:
5175 case Parsed{id, +short, name, kind}:
5176 Row{option_left(usage), filled(short), String.is_empty(short),
5177 right_text(long, help, attrs)}
5178 case Invalid{col}:
5179 Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}
5180
5181def find_heading(attrs: List<&2, Item>) -> String:
5182 match attrs:
5183 case Nil{}: ""
5184 case Heading{title} <> tail: title
5185 case head <> tail: find_heading(tail)
5186
5187def option_row(shown: Bool, +usage: String, help: String,
5188 attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5189 match shown:
5190 case True{}: [arg_row(parse_usage(usage), usage, help, attrs, long)]
5191 case False{}: []
5192
5193# The visible options under a heading, "" for Options.
5194def option_rows(items: List<&2, Item>, +long: Bool, +heading: String) ->
5195 List<&2, Row>:
5196 match items:
5197 case Nil{}: Nil{}
5198 case Arg{+usage, help, +attrs} <> tail:
5199 List.append(&2, Row,
5200 option_row(Bool.and(Bool.not(arg_pos(usage)),
5201 Bool.and(Bool.not(has_hidden(attrs)),
5202 String.eq(find_heading(attrs), heading))),
5203 usage, help, attrs, long),
5204 option_rows(tail, long, heading))
5205 case head <> tail: option_rows(tail, long, heading)
5206
5207def positional_row(shown: Bool, usage: String, help: String,
5208 attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5209 match shown:
5210 case True{}:
5211 [Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}]
5212 case False{}: []
5213
5214def positional_rows(items: List<&2, Item>, +long: Bool) -> List<&2, Row>:
5215 match items:
5216 case Nil{}: Nil{}
5217 case Arg{+usage, help, +attrs} <> tail:
5218 List.append(&2, Row,
5219 positional_row(Bool.and(arg_pos(usage), Bool.not(has_hidden(attrs))),
5220 usage, help, attrs, long),
5221 positional_rows(tail, long))
5222 case head <> tail: positional_rows(tail, long)
5223
5224def command_row(hidden: Bool, name: String, about: String,
5225 children: List<&2, Item>) -> List<&2, Row>:
5226 match hidden:
5227 case True{}: []
5228 case False{}:
5229 [Row{[Segment{name, [Bold{}]}], False{}, False{},
5230 join_filled([summary(about), tag("aliases", find_aliases(children))])}]
5231
5232def command_rows(items: List<&2, Item>) -> List<&2, Row>:
5233 match items:
5234 case Nil{}: Nil{}
5235 case Sub{name, about, +children} <> tail:
5236 List.append(&2, Row,
5237 command_row(has_hidden(children), name, about, children),
5238 command_rows(tail))
5239 case head <> tail: command_rows(tail)
5240
5241def help_sub_row(present: Bool) -> List<&2, Row>:
5242 match present:
5243 case True{}:
5244 [Row{[Segment{"help", [Bold{}]}], False{}, False{},
5245 "Print this message or the help of the given subcommand(s)"}]
5246 case False{}: []
5247
5248# A built-in option row, without its short when an app arg took it.
5249def builtin_row(taken: Bool, short: String, long: String,
5250 text: String) -> Row:
5251 match taken:
5252 case True{}: Row{bold_names([long], True{}), False{}, True{}, text}
5253 case False{}:
5254 Row{bold_names([short, long], True{}), True{}, False{}, text}
5255
5256def help_text(differs: Bool, long: Bool) -> String:
5257 match differs long:
5258 case False{} _: "Print help"
5259 case True{} False{}: "Print help (see more with '--help')"
5260 case True{} True{}: "Print help (see a summary with '-h')"
5261
5262def version_row(present: Bool, args: List<&2, Item>) -> List<&2, Row>:
5263 match present:
5264 case True{}:
5265 [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-V")),
5266 "-V", "--version", "Print version")]
5267 case False{}: []
5268
5269# Headings of visible args, in order of first use.
5270def heading_titles(items: List<&2, Item>, +seen: List<&2, String>) ->
5271 List<&2, String>:
5272 match items:
5273 case Nil{}: seen
5274 case Arg{usage, help, +attrs} <> tail:
5275 +title = find_heading(attrs)
5276 heading_titles(tail, Bool.pick(List<&2, String>,
5277 Bool.or(String.is_empty(title), Bool.or(has_hidden(attrs),
5278 List.contains(~String, ~String.eq, seen, title))),
5279 seen, List.append(&2, String, seen, [title])))
5280 case head <> tail: heading_titles(tail, seen)
5281
5282def heading_sections(titles: List<&2, String>, +args: List<&2, Item>,
5283 +long: Bool) -> List<&2, Section>:
5284 match titles:
5285 case Nil{}: Nil{}
5286 case +title <> tail:
5287 Section{title, option_rows(args, long, title)} <>
5288 heading_sections(tail, args, long)
5289
5290# Whether a visible arg's help has more than one paragraph.
5291def args_paragraphs(items: List<&2, Item>) -> Bool:
5292 match items:
5293 case Nil{}: False{}
5294 case Arg{usage, help, attrs} <> tail:
5295 Bool.or(Bool.and(Bool.not(has_hidden(attrs)),
5296 String.contains(help, "\n\n")), args_paragraphs(tail))
5297 case head <> tail: args_paragraphs(tail)
5298
5299def indent_cell(indent: Bool, left: List<&2, Segment>) -> List<&2, Segment>:
5300 match indent:
5301 case True{}: plain(" ") <> left
5302 case False{}: left
5303
5304# A left cell; long-only options indent 4 more on a page with a short.
5305def row_cell(row: Row, shorted: Bool) -> List<&2, Segment>:
5306 match row:
5307 case Row{left, short, long_only, right}:
5308 indent_cell(Bool.and(long_only, shorted), left)
5309
5310def cell_width(cell: List<&2, Segment>) -> Nat:
5311 match cell:
5312 case Nil{}: 0n
5313 case Segment{text, styles} <> tail: Nat.add(width(text), cell_width(tail))
5314
5315def row_short(row: Row) -> Bool:
5316 match row:
5317 case Row{left, short, long_only, right}: short
5318
5319def any_short(rows: List<&2, Row>) -> Bool:
5320 match rows:
5321 case Nil{}: False{}
5322 case row <> tail: Bool.or(row_short(row), any_short(tail))
5323
5324# L, the widest left cell w with 2 + w + 2 <= K.
5325def widest(rows: List<&2, Row>, +shorted: Bool, +k: Nat, +best: Nat) -> Nat:
5326 match rows:
5327 case Nil{}: best
5328 case row <> tail:
5329 +w = cell_width(row_cell(row, shorted))
5330 widest(tail, shorted, k,
5331 Bool.pick(Nat, Nat.is_le((w + 4n : Nat), k), Nat.max(w, best), best))
5332
5333def pad_line(line: String, col: Nat) -> String:
5334 match line:
5335 case SNil{}: "\n"
5336 case SCon{head, tail}:
5337 String.concat(["\n", String.repeat(" ", col), SCon{head, tail}])
5338
5339def pad_lines(lines: List<&2, String>, +col: Nat) -> List<&2, Segment>:
5340 match lines:
5341 case Nil{}: Nil{}
5342 case line <> tail: plain(pad_line(line, col)) <> pad_lines(tail, col)
5343
5344def first_then(lines: List<&2, String>, col: Nat) -> List<&2, Segment>:
5345 match lines:
5346 case Nil{}: Nil{}
5347 case line <> tail: plain(line) <> pad_lines(tail, col)
5348
5349# A cell that fits takes its text after padding to C; one that does not
5350# stands alone, its text on the next lines at max(C, 10).
5351def row_form(fits: Bool, cell: List<&2, Segment>, lw: Nat, right: String,
5352 w: Nat, +c: Nat) -> List<&2, Segment>:
5353 match fits:
5354 case True{}:
5355 plain(" ") <> List.append(&2, Segment, cell,
5356 plain(String.repeat(" ", (c - 2n - lw : Nat))) <>
5357 first_then(String.lines(wrap(right, (w - c : Nat))), c))
5358 case False{}:
5359 +col = Nat.max(c, 10n)
5360 plain(" ") <> List.append(&2, Segment, cell,
5361 pad_lines(String.lines(wrap(right, (w - col : Nat))), col))
5362
5363def row_lines(row: Row, shorted: Bool, w: Nat, k: Nat, c: Nat) ->
5364 List<&2, Segment>:
5365 match row:
5366 case Row{left, short, long_only, right}:
5367 +cell = indent_cell(Bool.and(long_only, shorted), left)
5368 +lw = cell_width(cell)
5369 row_form(Nat.is_le((lw + 4n : Nat), k), cell, lw, right, w, c)
5370
5371def rows_lines(rows: List<&2, Row>, +shorted: Bool, +w: Nat, +k: Nat,
5372 +c: Nat) -> List<&2, Segment>:
5373 match rows:
5374 case Nil{}: Nil{}
5375 case row <> tail:
5376 plain("\n") <> List.append(&2, Segment, row_lines(row, shorted, w, k, c),
5377 rows_lines(tail, shorted, w, k, c))
5378
5379def section_rows(sections: List<&2, Section>) -> List<&2, Row>:
5380 match sections:
5381 case Nil{}: Nil{}
5382 case Section{title, rows} <> tail:
5383 List.append(&2, Row, rows, section_rows(tail))
5384
5385def section_blocks(sections: List<&2, Section>, +shorted: Bool, +w: Nat,
5386 +k: Nat, +c: Nat) -> List<&2, List<&2, Segment>>:
5387 match sections:
5388 case Nil{}: Nil{}
5389 case Section{title, Nil{}} <> tail: section_blocks(tail, shorted, w, k, c)
5390 case Section{title, rows} <> tail:
5391 (Segment{String.append(title, ":"), [Bold{}, Under{}]} <>
5392 rows_lines(rows, shorted, w, k, c)) <>
5393 section_blocks(tail, shorted, w, k, c)
5394
5395# One column for the page: C = L + 4, with K = 2W/5.
5396def sections_blocks(+sections: List<&2, Section>, +w: Nat) ->
5397 List<&2, List<&2, Segment>>:
5398 +rows = section_rows(sections)
5399 +shorted = any_short(rows)
5400 +k = Nat.div((2n * w : Nat), 5n)
5401 section_blocks(sections, shorted, w, k,
5402 (widest(rows, shorted, k, 0n) + 4n : Nat))
5403
5404def text_block(text: String, w: Nat) -> List<&2, List<&2, Segment>>:
5405 match text:
5406 case SNil{}: Nil{}
5407 case SCon{head, tail}: [[plain(wrap(SCon{head, tail}, w))]]
5408
5409def example_lines(items: List<&2, Item>) -> List<&2, Segment>:
5410 match items:
5411 case Nil{}: Nil{}
5412 case Example{line, what} <> tail:
5413 plain(String.concat(["\n ", what, ":\n $ ", line])) <>
5414 example_lines(tail)
5415 case head <> tail: example_lines(tail)
5416
5417def examples_block(lines: List<&2, Segment>) -> List<&2, List<&2, Segment>>:
5418 match lines:
5419 case Nil{}: Nil{}
5420 case head <> tail: [Segment{"Examples:", [Bold{}, Under{}]} <> head <> tail]
5421
5422def after_texts(items: List<&2, Item>) -> List<&2, String>:
5423 match items:
5424 case Nil{}: Nil{}
5425 case After{text} <> tail: text <> after_texts(tail)
5426 case head <> tail: after_texts(tail)
5427
5428def more_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5429 match blocks:
5430 case Nil{}: [plain("\n")]
5431 case block <> tail:
5432 plain("\n\n") <> List.append(&2, Segment, block, more_blocks(tail))
5433
5434# Blocks apart by one blank line; the page ends with a newline.
5435def join_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5436 match blocks:
5437 case Nil{}: Nil{}
5438 case block <> tail: List.append(&2, Segment, block, more_blocks(tail))
5439
5440def help_page(view: View, +long: Bool, +w: Nat) -> List<&2, Segment>:
5441 match view:
5442 case View{path, +about, +items, +globals, root, stuck}:
5443 +args = List.append(&2, Item, items, globals)
5444 join_blocks(List.concat(&2, List<&2, Segment>, [
5445 text_block(shown_text(long, about), w),
5446 [usage_block(usage_elems(path, items, globals), w)],
5447 sections_blocks(List.append(&2, Section, [
5448 Section{"Commands", List.append(&2, Row, command_rows(items),
5449 help_sub_row(has_sub_item(items)))},
5450 Section{"Arguments", positional_rows(items, long)},
5451 Section{"Options", List.concat(&2, Row, [
5452 option_rows(args, long, ""),
5453 [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-h")),
5454 "-h", "--help", help_text(Bool.or(String.contains(about, "\n\n"),
5455 args_paragraphs(args)), long))],
5456 version_row(Bool.and(root, has_version_item(items)), args),
5457 ])},
5458 ], heading_sections(heading_titles(args, []), args, long)), w),
5459 examples_block(example_lines(items)),
5460 text_block(String.join(after_texts(items), "\n\n"), w),
5461 ]))
5462
5463def help_out(t: Term, view: View, long: Bool) -> String:
5464 match t:
5465 case Term{out_color, err_color, width}:
5466 emit(out_color, help_page(view, long, page_width(width)))
5467
5468# The help page for the longest valid prefix of path, in stdout's colors.
5469def help(+c: Cmd, path: List<&2, String>, long: Bool, h: Host) -> String:
5470 help_out(term(c, h, []), view_of(c, path), long)
5471
5472# ---- Suggestions (§10.4) ----
5473
5474type Scored is Data:
5475 Scored{dist: Nat, name: String}
5476
5477def row_zero(b: String, +k: Nat) -> List<&2, Nat>:
5478 match b:
5479 case SNil{}: [k]
5480 case SCon{head, tail}: k <> row_zero(tail, (k + 1n : Nat))
5481
5482def head_nat(xs: List<&2, Nat>) -> Nat:
5483 match xs:
5484 case Nil{}: 0n
5485 case head <> tail: head
5486
5487def tail_nats(xs: List<&2, Nat>) -> List<&2, Nat>:
5488 match xs:
5489 case Nil{}: Nil{}
5490 case head <> tail: tail
5491
5492def same_char(found: Maybe<&2, Char>, c: Char) -> Bool:
5493 match found:
5494 case None{}: False{}
5495 case Some{d}: Char.is_eq(d, c)
5496
5497# Row i of optimal string alignment for a[i]: up starts at d[i-1][j-1], up2
5498# at d[i-2][j-2], and left is d[i][j-1].
5499def osa_row(bs: String, bp: Maybe<&2, Char>, +a: Char,
5500 +ap: Maybe<&2, Char>, +up: List<&2, Nat>, +up2: List<&2, Nat>,
5501 left: Nat) -> List<&2, Nat>:
5502 match bs:
5503 case SNil{}: Nil{}
5504 case SCon{+b, tail}:
5505 +diag = head_nat(up)
5506 +rest = tail_nats(up)
5507 +edit = Nat.min(Nat.min((head_nat(rest) + 1n : Nat), (left + 1n : Nat)),
5508 Bool.pick(Nat, Char.is_eq(a, b), diag, (diag + 1n : Nat)))
5509 +best = Bool.pick(Nat, Bool.and(same_char(bp, a), same_char(ap, b)),
5510 Nat.min(edit, (head_nat(up2) + 1n : Nat)), edit)
5511 best <> osa_row(tail, Some{b}, a, ap, rest, tail_nats(up2), best)
5512
5513def osa_rows(s: String, ap: Maybe<&2, Char>, +i: Nat, +b: String,
5514 +prev: List<&2, Nat>, prev2: List<&2, Nat>) -> List<&2, Nat>:
5515 match s:
5516 case SNil{}: prev
5517 case SCon{+a, tail}:
5518 osa_rows(tail, Some{a}, (i + 1n : Nat), b,
5519 i <> osa_row(b, None{}, a, ap, prev, 0n <> prev2, i), prev)
5520
5521def last_nat(xs: List<&2, Nat>, last: Nat) -> Nat:
5522 match xs:
5523 case Nil{}: last
5524 case head <> tail: last_nat(tail, head)
5525
5526def osa(a: String, +b: String) -> Nat:
5527 +first = row_zero(b, 0n)
5528 last_nat(osa_rows(a, None{}, 1n, b, first, first), 0n)
5529
5530def undashed(s: String) -> String:
5531 match s:
5532 case SCon{Chr{45}, tail}: undashed(tail)
5533 case _: s
5534
5535def scored(names: List<&2, String>, +typed: String, +limit: Nat) ->
5536 List<&2, Scored>:
5537 match names:
5538 case Nil{}: Nil{}
5539 case +name <> tail:
5540 +dist = osa(typed, String.take(name, 64n))
5541 +rest = scored(tail, typed, limit)
5542 Bool.pick(List<&2, Scored>, Nat.is_le(dist, limit),
5543 Scored{dist, name} <> rest, rest)
5544
5545def at_distance(found: List<&2, Scored>, +d: Nat) -> List<&2, String>:
5546 match found:
5547 case Nil{}: Nil{}
5548 case Scored{dist, name} <> tail:
5549 +rest = at_distance(tail, d)
5550 Bool.pick(List<&2, String>, Nat.is_eq(dist, d), name <> rest, rest)
5551
5552def by_distance(levels: Nat, +d: Nat, +found: List<&2, Scored>) ->
5553 List<&2, String>:
5554 match levels:
5555 case 0n: Nil{}
5556 case 1n+more:
5557 List.append(&2, String, at_distance(found, d),
5558 by_distance(more, (d + 1n : Nat), found))
5559
5560# At most 3 names within max(1, n / 3) edits, by distance, then order.
5561# key is bound because a temporary argument makes String.length take its
5562# string, adding a keep to each caller that reuses one (Nat.read.fits,
5563# arg_display).
5564def suggest(+typed: String, names: List<&2, String>) -> List<&2, String>:
5565 key = undashed(typed)
5566 +limit = Nat.max(1n, Nat.div(String.length(key), 3n))
5567 List.take(&2, String, by_distance((limit + 1n : Nat), 0n,
5568 scored(names, String.take(typed, 64n), limit)), 3n)
5569
5570# ---- Errors (§10.4) ----
5571
5572def quoted(text: String, styles: List<&2, Style>) -> List<&2, Segment>:
5573 [plain("'"), Segment{text, styles}, plain("'")]
5574
5575def missing_lines(missing: List<&2, String>) -> String:
5576 match missing:
5577 case Nil{}: ""
5578 case head <> tail: String.concat(["\n ", head, missing_lines(tail)])
5579
5580def visible_subs(items: List<&2, Item>) -> List<&2, String>:
5581 match items:
5582 case Nil{}: ["help"]
5583 case Sub{name, about, children} <> tail:
5584 +rest = visible_subs(tail)
5585 Bool.pick(List<&2, String>, has_hidden(children), rest, name <> rest)
5586 case head <> tail: visible_subs(tail)
5587
5588def fault_message(fault: ScanFault, path: String,
5589 items: List<&2, Item>) -> List<&2, Segment>:
5590 match fault:
5591 case E01{token}:
5592 List.concat(&2, Segment, [[plain("unexpected argument ")],
5593 quoted(token, [Yellow{}]), [plain(" found")]])
5594 case E02{name}:
5595 plain("unrecognized subcommand ") <> quoted(name, [Yellow{}])
5596 case E03{arg}:
5597 [plain(String.concat(["a value is required for '", arg,
5598 "' but none was supplied"]))]
5599 case E04{arg, value, reason}:
5600 List.concat(&2, Segment, [[plain("invalid value ")],
5601 quoted(value, [Yellow{}]),
5602 [plain(String.concat([" for '", arg, "': ", reason]))]])
5603 case E05{arg, value}:
5604 List.concat(&2, Segment, [[plain("unexpected value ")],
5605 quoted(value, [Yellow{}]),
5606 [plain(String.concat([" for '", arg,
5607 "' found; no more were expected"]))]])
5608 case E06{missing}:
5609 [plain(String.append(
5610 "the following required arguments were not provided:",
5611 missing_lines(missing)))]
5612 case E07{a, b}:
5613 [plain(String.concat(["the argument '", a, "' cannot be used with '", b,
5614 "'"]))]
5615 case E08{a, b}:
5616 [plain(String.concat(["the argument '", a, "' requires '", b, "'"]))]
5617 case E09{token}:
5618 List.concat(&2, Segment, [[plain("unexpected argument ")],
5619 quoted(token, [Yellow{}]), [plain(" found")]])
5620 case E10{}:
5621 [plain(String.concat(["'", path,
5622 "' requires a subcommand but none was provided\n [subcommands: ",
5623 String.join(visible_subs(items), ", "), "]"]))]
5624 case E11{arg, value, name, reason}:
5625 List.concat(&2, Segment, [[plain("invalid value ")],
5626 quoted(value, [Yellow{}]),
5627 [plain(String.concat([" for '", arg, "' from environment variable ",
5628 name, ": ", reason]))]])
5629
5630def green_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5631 match names:
5632 case Nil{}: Nil{}
5633 case name <> tail:
5634 List.concat(&2, Segment, [
5635 Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5636 quoted(name, [Green{}]), green_names(tail, False{})])
5637
5638def tip(text: String) -> List<&2, List<&2, Segment>>:
5639 [[plain(text)]]
5640
5641# T4: one suggestion or several.
5642def similar(kind: String, found: List<&2, String>) ->
5643 List<&2, List<&2, Segment>>:
5644 match found:
5645 case Nil{}: Nil{}
5646 case name <> Nil{}:
5647 [plain(String.concat(["a similar ", kind, " exists: "])) <>
5648 quoted(name, [Green{}])]
5649 case name <> more:
5650 [plain(String.concat(["some similar ", kind, "s exist: "])) <>
5651 green_names(name <> more, True{})]
5652
5653def long_name(s: String) -> Bool:
5654 String.starts_with(s, "--")
5655
5656def option_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
5657 match parsed:
5658 case Parsed{id, short, +long, kind}:
5659 List.concat(&2, String, [
5660 Bool.pick(List<&2, String>, String.is_empty(long), [], [long]),
5661 Bool.pick(List<&2, String>,
5662 Bool.and(kind_negatable(kind), filled(long)),
5663 [negated_long(long)], []),
5664 List.filter(~String, ~long_name, find_aliases(attrs)),
5665 ])
5666 case Invalid{col}: []
5667
5668def visible_names(hidden: Bool, usage: String, attrs: List<&2, Item>) ->
5669 List<&2, String>:
5670 match hidden:
5671 case True{}: []
5672 case False{}: option_names(parse_usage(usage), attrs)
5673
5674# T4 for E01: visible longs, their no- forms and long aliases.
5675def long_candidates(items: List<&2, Item>) -> List<&2, String>:
5676 match items:
5677 case Nil{}: Nil{}
5678 case Arg{usage, help, +attrs} <> tail:
5679 List.append(&2, String, visible_names(has_hidden(attrs), usage, attrs),
5680 long_candidates(tail))
5681 case head <> tail: long_candidates(tail)
5682
5683# T4 for E02: visible subcommand names and aliases, then help.
5684def sub_candidates(items: List<&2, Item>) -> List<&2, String>:
5685 match items:
5686 case Nil{}: ["help"]
5687 case Sub{name, about, +children} <> tail:
5688 List.append(&2, String,
5689 Bool.pick(List<&2, String>, has_hidden(children), [],
5690 name <> find_aliases(children)),
5691 sub_candidates(tail))
5692 case head <> tail: sub_candidates(tail)
5693
5694# T4 for E04: the one_of values of the arg E04 names.
5695def display_choices(items: List<&2, Item>, +arg: String,
5696 found: List<&2, String>) -> List<&2, String>:
5697 match items found:
5698 case _ head <> tail: head <> tail
5699 case Nil{} Nil{}: Nil{}
5700 case Arg{usage, help, +attrs} <> tail Nil{}:
5701 display_choices(tail, arg, Bool.pick(List<&2, String>,
5702 String.eq(arg_display(arg_info(usage, attrs)), arg),
5703 list_or_none(find_one_of(attrs)), []))
5704 case head <> tail Nil{}: display_choices(tail, arg, [])
5705
5706def owned(found: Maybe<&2, OptFound>, text: String) -> Maybe<&2, String>:
5707 match found:
5708 case None{}: None{}
5709 case Some{option}: Some{text}
5710
5711def owned_by(hit: Maybe<&2, OptFound>, token: String, prefix: String,
5712 step: String, found: Maybe<&2, String>) -> Maybe<&2, String>:
5713 match hit:
5714 case None{}: found
5715 case Some{option}:
5716 Some{String.concat(["'", token, "' belongs to '", prefix,
5717 "'; place it before '", step, "'"])}
5718
5719# T1: the deepest ancestor whose args take the long, and its child on path.
5720def owner(path: List<&2, String>, +items: List<&2, Item>, +prefix: String,
5721 +token: String, +long: String, found: Maybe<&2, String>) ->
5722 Maybe<&2, String>:
5723 match path:
5724 case Nil{}: found
5725 case +step <> tail:
5726 owner(tail, sub_items(find_sub(items, step)),
5727 String.concat([prefix, " ", step]), token, long,
5728 owned_by(find_option(items, long), token, prefix, step, found))
5729
5730# T2: the first subcommand in pre-order whose args take the long.
5731def descendant(items: List<&2, Item>, +prefix: String, +long: String,
5732 found: Maybe<&2, String>) -> Maybe<&2, String>:
5733 match items found:
5734 case _ Some{path}: Some{path}
5735 case Nil{} None{}: None{}
5736 case Sub{name, about, +children} <> tail None{}:
5737 +path = String.concat([prefix, " ", name])
5738 descendant(tail, prefix, long, descendant(children, path, long,
5739 owned(find_option(children, long), path)))
5740 case head <> tail None{}: descendant(tail, prefix, long, None{})
5741
5742def maybe_tip(found: Maybe<&2, String>) -> List<&2, List<&2, Segment>>:
5743 match found:
5744 case None{}: Nil{}
5745 case Some{text}: tip(text)
5746
5747def belongs(found: Maybe<&2, String>, token: String) ->
5748 List<&2, List<&2, Segment>>:
5749 match found:
5750 case None{}: Nil{}
5751 case Some{path}:
5752 tip(String.concat(["'", token, "' belongs to subcommand '", path, "'"]))
5753
5754def sub_exists(found: Maybe<&2, SubFound>, name: String) ->
5755 List<&2, List<&2, Segment>>:
5756 match found:
5757 case None{}: Nil{}
5758 case Some{sub}:
5759 tip(String.concat(["subcommand '", name,
5760 "' exists; to use it, remove the '--'"]))
5761
5762def before_eq(s: String) -> String:
5763 match s:
5764 case SNil{}: SNil{}
5765 case SCon{Chr{61}, tail}: SNil{}
5766 case SCon{head, tail}: SCon{head, before_eq(tail)}
5767
5768# T1, T2 and T3, for E01 on a long; --name=value looks up --name.
5769def long_tips(is_long: Bool, +token: String, root: String,
5770 path: List<&2, String>, +items: List<&2, Item>,
5771 root_items: List<&2, Item>, here: String) ->
5772 List<&2, List<&2, Segment>>:
5773 match is_long:
5774 case False{}: Nil{}
5775 case True{}:
5776 +long = before_eq(token)
5777 +name = String.drop(long, 2n)
5778 List.concat(&2, List<&2, Segment>, [
5779 maybe_tip(owner(path, root_items, root, token, long, None{})),
5780 belongs(descendant(items, here, long, None{}), token),
5781 sub_exists(find_sub(items, name), name),
5782 ])
5783
5784def operand_count(events: List<&2, ScanEvent>, count: Nat) -> Nat:
5785 match events:
5786 case Nil{}: count
5787 case Enter{name} <> tail: operand_count(tail, 0n)
5788 case Operand{token} <> tail: operand_count(tail, (count + 1n : Nat))
5789 case event <> tail: operand_count(tail, count)
5790
5791def trace_operands(trace: ScanTrace) -> Nat:
5792 match trace:
5793 case ScanTrace{path, events, ended, external, issue}:
5794 operand_count(events, 0n)
5795
5796def dash_slot(open: Bool, +token: String) -> List<&2, List<&2, Segment>>:
5797 match open:
5798 case False{}: Nil{}
5799 case True{}:
5800 tip(String.concat(["to pass '", token, "' as a value, use '-- ", token,
5801 "'"]))
5802
5803# T5: a hyphen token that the next positional slot could take after `--`;
5804# only then is argv scanned again to count the operands in scope.
5805def dash_tip(hyphen: Bool, token: String, items: List<&2, Item>, c: Cmd,
5806 argv: List<&2, String>) -> List<&2, List<&2, Segment>>:
5807 match hyphen:
5808 case False{}: Nil{}
5809 case True{}:
5810 dash_slot(positional_can_take(items, trace_operands(scan(c, argv))),
5811 token)
5812
5813def cmd_items(c: Cmd) -> List<&2, Item>:
5814 match c:
5815 case Cmd{name, about, items}: items
5816
5817# Every tip that applies, in table order.
5818def fault_tips(fault: ScanFault, +c: Cmd, +path: List<&2, String>,
5819 argv: List<&2, String>, here: String, +items: List<&2, Item>,
5820 globals: List<&2, Item>) -> List<&2, List<&2, Segment>>:
5821 match fault:
5822 case E01{+token}:
5823 List.concat(&2, List<&2, Segment>, [
5824 long_tips(long_name(token), token, cmd_name(c), path, items,
5825 cmd_items(c), here),
5826 similar("argument", suggest(token, List.concat(&2, String, [
5827 long_candidates(List.append(&2, Item, items, globals)),
5828 ["--help"],
5829 Bool.pick(List<&2, String>, Bool.and(List.is_empty(&2, String, path),
5830 has_version_item(items)), ["--version"], []),
5831 ]))),
5832 dash_tip(String.starts_with(token, "-"), token, items, c, argv),
5833 ])
5834 case E02{+name}: similar("subcommand", suggest(name, sub_candidates(items)))
5835 case E04{+arg, +value, reason}:
5836 similar("value", suggest(value,
5837 display_choices(List.append(&2, Item, items, globals), arg, [])))
5838 case E09{+token}:
5839 dash_tip(String.starts_with(token, "-"), token, items, c, argv)
5840 case _: Nil{}
5841
5842def tip_spans(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5843 match tips:
5844 case Nil{}: Nil{}
5845 case line <> tail:
5846 List.concat(&2, Segment, [
5847 [plain(" "), Segment{"tip:", [Bold{}, Green{}]}, plain(" ")],
5848 line, [plain("\n")], tip_spans(tail)])
5849
5850def tip_block(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5851 match tips:
5852 case Nil{}: Nil{}
5853 case head <> tail: plain("\n") <> tip_spans(head <> tail)
5854
5855def error_page(message: List<&2, Segment>,
5856 tips: List<&2, List<&2, Segment>>, usage: List<&2, Segment>) ->
5857 List<&2, Segment>:
5858 List.concat(&2, Segment, [
5859 [Segment{"error:", [Bold{}, Red{}]}, plain(" ")], message, [plain("\n")],
5860 tip_block(tips), [plain("\n")], usage,
5861 [plain("\n\nFor more information, try '"), Segment{"--help", [Bold{}]},
5862 plain("'.\n")],
5863 ])
5864
5865def fault_view(view: View, +fault: ScanFault, c: Cmd, path: List<&2, String>,
5866 argv: List<&2, String>, w: Nat) -> List<&2, Segment>:
5867 match view:
5868 case View{+here, about, +items, +globals, root, stuck}:
5869 error_page(fault_message(fault, here, items),
5870 fault_tips(fault, c, path, argv, here, items, globals),
5871 usage_block(usage_elems(here, items, globals), w))
5872
5873# An error at the scope where scanning stopped, in stderr's colors.
5874def fault_err(t: Term, fault: ScanFault, +c: Cmd, +path: List<&2, String>,
5875 argv: List<&2, String>) -> String:
5876 match t:
5877 case Term{out_color, err_color, width}:
5878 emit(err_color, fault_view(view_of(c, path), fault, c, path, argv,
5879 page_width(width)))
5880
5881def summary_err(t: Term, view: View) -> String:
5882 match t:
5883 case Term{out_color, err_color, width}:
5884 emit(err_color, help_page(view, False{}, page_width(width)))
5885
5886# ---- Lint lines (§9) ----
5887
5888def problem_line(p: Problem) -> String:
5889 match p:
5890 case BadUsage{at, usage, col}:
5891 String.concat([" ", at, ": usage '", usage, "' is invalid at column ",
5892 Nat.show((col + 1n : Nat))])
5893 case BadName{at, name}:
5894 String.concat([" ", at, ": '", name, "' is not a valid name"])
5895 case Dup{at, name}:
5896 String.concat([" ", at, ": '", name, "' is declared more than once"])
5897 case Reserved{at, name}:
5898 String.concat([" ", at, ": '", name, "' is reserved"])
5899 case Misplaced{at, attr}:
5900 String.concat([" ", at, ": '", attr, "' is not allowed here"])
5901 case NoHelp{at, what}:
5902 String.concat([" ", at, ": '", what, "' has no help text"])
5903 case BadDefault{at, id, value}:
5904 String.concat([" ", at, ": '", id, "' cannot have the default '", value,
5905 "'"])
5906 case BadRef{at, id}:
5907 String.concat([" ", at, ": '", id,
5908 "' is an unknown id or an unused or empty group"])
5909 case BadPositional{at, id, why}:
5910 String.concat([" ", at, ": positional '", id, "': ", why])
5911 case Mixed{at}:
5912 String.concat([" ", at, ": has both positionals and subcommands"])
5913 case BadChoices{at, id}:
5914 String.concat([" ", at, ": one_of for '", id,
5915 "' is empty or repeats a value"])
5916 case BadChoice{at, id, value}:
5917 String.concat([" ", at, ": '", id, "' cannot have the choice '", value,
5918 "'"])
5919 case NeedBare{at, id}:
5920 String.concat([" ", at, ": '", id,
5921 "' needs [=<V>] and bare() together, with no short"])
5922 case DecodeBug{at, msg}:
5923 String.concat([" ", at, ": decoder bug: ", msg])
5924
5925def problem_lines(found: List<&2, Problem>) -> String:
5926 match found:
5927 case Nil{}: ""
5928 case p <> tail: String.concat([problem_line(p), "\n", problem_lines(tail)])
5929
5930# ---- Completions and man pages (§11) ----
5931
5932type Shell is Data:
5933 Bash{}
5934 Zsh{}
5935 Fish{}
5936
5937def sh_body(s: String) -> String:
5938 match s:
5939 case SNil{}: SNil{}
5940 case SCon{Chr{39}, tail}: String.append("'\\''", sh_body(tail))
5941 case SCon{c, tail}: SCon{c, sh_body(tail)}
5942
5943# POSIX single quotes, with ' written as '\'' (P3).
5944def Sh.quote(s: String) -> String:
5945 SCon{'\'', String.append(sh_body(s), "'")}
5946
5947def fish_body(s: String) -> String:
5948 match s:
5949 case SNil{}: SNil{}
5950 case SCon{Chr{39}, tail}: String.append("\\'", fish_body(tail))
5951 case SCon{Chr{92}, tail}: String.append("\\\\", fish_body(tail))
5952 case SCon{c, tail}: SCon{c, fish_body(tail)}
5953
5954# fish single quotes, with \ and ' backslash-escaped (P4).
5955def Fish.quote(s: String) -> String:
5956 SCon{'\'', String.append(fish_body(s), "'")}
5957
5958# Text in an _arguments spec or a _describe entry, with \, [, ] and :
5959# backslash-escaped (P5).
5960def Zsh.esc(s: String) -> String:
5961 match s:
5962 case SNil{}: SNil{}
5963 case SCon{Chr{92}, tail}: String.append("\\\\", Zsh.esc(tail))
5964 case SCon{Chr{91}, tail}: String.append("\\[", Zsh.esc(tail))
5965 case SCon{Chr{93}, tail}: String.append("\\]", Zsh.esc(tail))
5966 case SCon{Chr{58}, tail}: String.append("\\:", Zsh.esc(tail))
5967 case SCon{c, tail}: SCon{c, Zsh.esc(tail)}
5968
5969# Text for man(7), start telling whether a line starts here: \, -, ", '
5970# and ` as escapes, and \& before a . or a space that starts a line.
5971def roff_body(s: String, start: Bool) -> String:
5972 match s start:
5973 case SNil{} _: SNil{}
5974 case SCon{Chr{46}, tail} True{}: String.append("\\&.", roff_body(tail, False{}))
5975 case SCon{Chr{32}, tail} True{}: String.append("\\& ", roff_body(tail, False{}))
5976 case SCon{Chr{10}, tail} _: SCon{'\n', roff_body(tail, True{})}
5977 case SCon{Chr{92}, tail} _: String.append("\\e", roff_body(tail, False{}))
5978 case SCon{Chr{45}, tail} _: String.append("\\-", roff_body(tail, False{}))
5979 case SCon{Chr{34}, tail} _: String.append("\\(dq", roff_body(tail, False{}))
5980 case SCon{Chr{39}, tail} _: String.append("\\(aq", roff_body(tail, False{}))
5981 case SCon{Chr{96}, tail} _: String.append("\\(ga", roff_body(tail, False{}))
5982 case SCon{c, tail} _: SCon{c, roff_body(tail, False{})}
5983
5984# Text that starts lines (P6, P15).
5985def Roff.esc(s: String) -> String:
5986 roff_body(s, True{})
5987
5988# Text after the start of a line.
5989def roff_text(s: String) -> String:
5990 roff_body(s, False{})
5991
5992def text_lines(lines: List<&2, String>) -> String:
5993 match lines:
5994 case Nil{}: ""
5995 case line <> tail: String.concat([line, "\n", text_lines(tail)])
5996
5997# The summary with its hard line breaks as spaces.
5998def one_line(s: String) -> String:
5999 String.join(String.lines(summary(s)), " ")
6000
6001def ident_char(+c: Char) -> Char:
6002 Bool.pick(Char, Bool.or(name_start(c), Char.is_eq(c, '_')), c, '_')
6003
6004# A name with every char outside [A-Za-z0-9_] as _, for shell identifiers.
6005def ident(s: String) -> String:
6006 match s:
6007 case SNil{}: SNil{}
6008 case SCon{c, tail}: SCon{ident_char(c), ident(tail)}
6009
6010def same_words(a: List<&2, String>, b: List<&2, String>) -> Bool:
6011 match a b:
6012 case Nil{} Nil{}: True{}
6013 case x <> xs y <> ys: Bool.and(String.eq(x, y), same_words(xs, ys))
6014 case _ _: False{}
6015
6016def last_with(lines: List<&2, String>, +suffix: String) -> List<&2, String>:
6017 match lines:
6018 case Nil{}: Nil{}
6019 case line <> Nil{}: [String.append(line, suffix)]
6020 case line <> tail: line <> last_with(tail, suffix)
6021
6022# A command on a visible path, in pre-order from the root: the words of its
6023# path below the root, its items, and the globals it inherits, root first.
6024type Place is Data:
6025 Place{words: List<&2, String>, items: List<&2, Item>,
6026 globals: List<&2, Item>}
6027
6028# The visible commands below items, whose subcommands inherit globals.
6029def places(items: List<&2, Item>, +words: List<&2, String>,
6030 +globals: List<&2, Item>) -> List<&2, Place>:
6031 match items:
6032 case Nil{}: Nil{}
6033 case Sub{name, about, +children} <> tail:
6034 +here = List.append(&2, String, words, [name])
6035 List.append(&2, Place,
6036 Bool.pick(List<&2, Place>, has_hidden(children), [],
6037 Place{here, children, globals} <> places(children, here,
6038 List.append(&2, Item, globals, global_items(children)))),
6039 places(tail, words, globals))
6040 case head <> tail: places(tail, words, globals)
6041
6042def all_places(+items: List<&2, Item>) -> List<&2, Place>:
6043 Place{[], items, []} <> places(items, [], global_items(items))
6044
6045# What a value completes to: a word of the one_of list numbered list, a
6046# file, a directory, or nothing.
6047type Comp is Data:
6048 Words{list: Nat, values: List<&2, String>}
6049 Files{}
6050 Dirs{}
6051 Free{}
6052
6053# An option as a script offers it: its names, shorts first, its kind, its
6054# placeholder, what its value completes to, and its help on one line.
6055type Opt is Data:
6056 Opt{names: List<&2, String>, kind: UsageKind, ph: String, comp: Comp,
6057 help: String}
6058
6059# A positional slot, numbered from 1.
6060type Slot is Data:
6061 Slot{k: Nat, hidden: Bool, required: Bool, many: Bool, rest: Bool,
6062 ph: String, comp: Comp}
6063
6064# A visible subcommand: its name, its names with aliases, and its summary.
6065type Entry is Data:
6066 Entry{name: String, names: List<&2, String>, about: String}
6067
6068# The number of values in lists, counting from k + 1 after a miss, or 0.
6069def list_number(lists: List<&2, List<&2, String>>,
6070 +values: List<&2, String>, hit: Bool, +k: Nat) -> Nat:
6071 match lists hit:
6072 case _ True{}: k
6073 case Nil{} False{}: 0n
6074 case head <> tail False{}:
6075 list_number(tail, values, same_words(head, values), (k + 1n : Nat))
6076
6077def add_list(found: Maybe<&2, List<&2, String>>,
6078 +lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6079 match found:
6080 case None{}: lists
6081 case Some{+values}:
6082 Bool.pick(List<&2, List<&2, String>>,
6083 List.contains(~List<&2, String>, ~same_words, lists, values), lists,
6084 List.append(&2, List<&2, String>, lists, [values]))
6085
6086def visible_one_of(hidden: Bool, attrs: List<&2, Item>) ->
6087 Maybe<&2, List<&2, String>>:
6088 match hidden:
6089 case True{}: None{}
6090 case False{}: find_one_of(attrs)
6091
6092def item_lists(items: List<&2, Item>, lists: List<&2, List<&2, String>>) ->
6093 List<&2, List<&2, String>>:
6094 match items:
6095 case Nil{}: lists
6096 case Arg{usage, help, +attrs} <> tail:
6097 item_lists(tail, add_list(visible_one_of(has_hidden(attrs), attrs), lists))
6098 case head <> tail: item_lists(tail, lists)
6099
6100# The one_of lists of visible args, each once, in pre-order.
6101def place_lists(places: List<&2, Place>,
6102 lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6103 match places:
6104 case Nil{}: lists
6105 case Place{words, items, globals} <> tail:
6106 place_lists(tail, item_lists(items, lists))
6107
6108def type_comp(found: Maybe<&2, ValueType>) -> Comp:
6109 match found:
6110 case Some{TFile{}}: Files{}
6111 case Some{TDir{}}: Dirs{}
6112 case _: Free{}
6113
6114def comp_of(found: Maybe<&2, List<&2, String>>, t: Maybe<&2, ValueType>,
6115 lists: List<&2, List<&2, String>>) -> Comp:
6116 match found:
6117 case Some{+values}: Words{list_number(lists, values, False{}, 0n), values}
6118 case None{}: type_comp(t)
6119
6120def head_part(parts: List<&2, String>) -> String:
6121 match parts:
6122 case head <> tail: head
6123 case Nil{}: ""
6124
6125def next_part(parts: List<&2, String>) -> String:
6126 match parts:
6127 case head <> next <> tail: next
6128 case _: ""
6129
6130def between(s: String, +open: Char, close: Char) -> String:
6131 head_part(String.split(next_part(String.split(s, open)), close))
6132
6133# The name in a usage's <>, or in an optional positional's [].
6134def placeholder(+usage: String) -> String:
6135 Bool.pick(String, String.contains(usage, "<"), between(usage, '<', '>'),
6136 between(usage, '[', ']'))
6137
6138def short_alias(s: String) -> Bool:
6139 Bool.not(long_name(s))
6140
6141def short_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
6142 match parsed:
6143 case Parsed{id, +short, long, kind}:
6144 List.append(&2, String,
6145 Bool.pick(List<&2, String>, String.is_empty(short), [], [short]),
6146 List.filter(~String, ~short_alias, find_aliases(attrs)))
6147 case Invalid{col}: []
6148
6149def parsed_kind(parsed: Usage) -> UsageKind:
6150 match parsed:
6151 case Parsed{id, short, long, kind}: kind
6152 case Invalid{col}: FlagKind{}
6153
6154def arg_opt(+parsed: Usage, usage: String, help: String,
6155 +attrs: List<&2, Item>, lists: List<&2, List<&2, String>>) -> Opt:
6156 Opt{List.append(&2, String, short_names(parsed, attrs),
6157 option_names(parsed, attrs)),
6158 parsed_kind(parsed), placeholder(usage),
6159 comp_of(find_one_of(attrs), find_as(attrs), lists), one_line(help)}
6160
6161# The visible options in items.
6162def arg_opts(items: List<&2, Item>, +lists: List<&2, List<&2, String>>) ->
6163 List<&2, Opt>:
6164 match items:
6165 case Nil{}: Nil{}
6166 case Arg{+usage, help, +attrs} <> tail:
6167 +rest = arg_opts(tail, lists)
6168 +parsed = parse_usage(usage)
6169 Bool.pick(List<&2, Opt>,
6170 Bool.or(usage_kind(~kind_pos, parsed), has_hidden(attrs)), rest,
6171 arg_opt(parsed, usage, help, attrs, lists) <> rest)
6172 case head <> tail: arg_opts(tail, lists)
6173
6174def flag_opt(names: List<&2, String>, help: String) -> Opt:
6175 Opt{names, FlagKind{}, "", Free{}, help}
6176
6177def version_opts(present: Bool, args: List<&2, Item>) -> List<&2, Opt>:
6178 match present:
6179 case True{}:
6180 [flag_opt(Bool.pick(List<&2, String>,
6181 Maybe.is_some(&2, OptFound, find_option(args, "-V")),
6182 ["--version"], ["-V", "--version"]), "Print version")]
6183 case False{}: []
6184
6185# -h and --help, without -h when an arg took it, then at a root with a
6186# version -V and --version likewise.
6187def builtin_opts(+items: List<&2, Item>, globals: List<&2, Item>,
6188 root: Bool) -> List<&2, Opt>:
6189 +args = List.append(&2, Item, items, globals)
6190 flag_opt(Bool.pick(List<&2, String>,
6191 Maybe.is_some(&2, OptFound, find_option(args, "-h")),
6192 ["--help"], ["-h", "--help"]), "Print help") <>
6193 version_opts(Bool.and(root, has_version_item(items)), args)
6194
6195def pos_slot(parsed: Usage, k: Nat, usage: String, +attrs: List<&2, Item>,
6196 lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6197 match parsed:
6198 case Parsed{id, short, long, PosKind{required, many}}:
6199 [Slot{k, has_hidden(attrs), required, many, has_rest(attrs),
6200 placeholder(usage), comp_of(find_one_of(attrs), find_as(attrs), lists)}]
6201 case _: []
6202
6203# The positional slots in items, hidden ones too, numbered from k.
6204def arg_slots(items: List<&2, Item>, +k: Nat,
6205 +lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6206 match items:
6207 case Nil{}: Nil{}
6208 case Arg{+usage, help, attrs} <> tail:
6209 +parsed = parse_usage(usage)
6210 List.append(&2, Slot, pos_slot(parsed, k, usage, attrs, lists),
6211 arg_slots(tail, Bool.pick(Nat, usage_kind(~kind_pos, parsed),
6212 (k + 1n : Nat), k), lists))
6213 case head <> tail: arg_slots(tail, k, lists)
6214
6215# The slots up to the variadic one, which takes every later value.
6216def completing(slots: List<&2, Slot>) -> List<&2, Slot>:
6217 match slots:
6218 case Nil{}: Nil{}
6219 case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6220 [Slot{k, hidden, required, True{}, rest, ph, comp}]
6221 case head <> tail: head <> completing(tail)
6222
6223def variadic(slots: List<&2, Slot>) -> Maybe<&2, Slot>:
6224 match slots:
6225 case Nil{}: None{}
6226 case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6227 Some{Slot{k, hidden, required, True{}, rest, ph, comp}}
6228 case head <> tail: variadic(tail)
6229
6230# The visible subcommands in items.
6231def sub_entries(items: List<&2, Item>) -> List<&2, Entry>:
6232 match items:
6233 case Nil{}: Nil{}
6234 case Sub{+name, about, +children} <> tail:
6235 +rest = sub_entries(tail)
6236 Bool.pick(List<&2, Entry>, has_hidden(children), rest,
6237 Entry{name, name <> find_aliases(children), one_line(about)} <> rest)
6238 case head <> tail: sub_entries(tail)
6239
6240def opt_names(opts: List<&2, Opt>) -> List<&2, String>:
6241 match opts:
6242 case Nil{}: Nil{}
6243 case Opt{names, kind, ph, comp, help} <> tail:
6244 List.append(&2, String, names, opt_names(tail))
6245
6246def entry_names(entries: List<&2, Entry>) -> List<&2, String>:
6247 match entries:
6248 case Nil{}: Nil{}
6249 case Entry{name, names, about} <> tail:
6250 List.append(&2, String, names, entry_names(tail))
6251
6252# What a script needs at a place: its number, its words, its options in
6253# scope, own first, the built-ins, its visible subcommands, and its slots.
6254type Stop is Data:
6255 Stop{k: Nat, words: List<&2, String>, opts: List<&2, Opt>,
6256 builtins: List<&2, Opt>, subs: List<&2, Entry>, slots: List<&2, Slot>}
6257
6258def stop_of(place: Place, k: Nat, +lists: List<&2, List<&2, String>>) ->
6259 Stop:
6260 match place:
6261 case Place{+words, +items, +globals}:
6262 Stop{k, words, List.append(&2, Opt, arg_opts(items, lists),
6263 arg_opts(globals, lists)),
6264 builtin_opts(items, globals, List.is_empty(&2, String, words)),
6265 sub_entries(items), arg_slots(items, 1n, lists)}
6266
6267def stops(places: List<&2, Place>, +k: Nat,
6268 +lists: List<&2, List<&2, String>>) -> List<&2, Stop>:
6269 match places:
6270 case Nil{}: Nil{}
6271 case place <> tail:
6272 stop_of(place, k, lists) <> stops(tail, (k + 1n : Nat), lists)
6273
6274# The number of the stop at words, from found on.
6275def stop_number(stops: List<&2, Stop>, +words: List<&2, String>,
6276 found: Maybe<&2, Nat>) -> Nat:
6277 match stops found:
6278 case _ Some{k}: k
6279 case Nil{} None{}: 0n
6280 case Stop{+k, here, opts, builtins, subs, slots} <> tail None{}:
6281 stop_number(tail, words, Bool.pick(Maybe<&2, Nat>,
6282 same_words(here, words), Some{k}, None{}))
6283
6284# ---- bash
6285
6286# Words quoted for sh, apart by sep.
6287def sh_list(words: List<&2, String>, +sep: String) -> String:
6288 match words:
6289 case Nil{}: ""
6290 case word <> Nil{}: Sh.quote(word)
6291 case word <> tail: String.concat([Sh.quote(word), sep, sh_list(tail, sep)])
6292
6293def dests(names: List<&2, String>, +dest: String) -> List<&2, String>:
6294 match names:
6295 case Nil{}: Nil{}
6296 case name <> tail: dest <> dests(tail, dest)
6297
6298# Each subcommand name's path, once per name.
6299def entry_dests(entries: List<&2, Entry>, +words: List<&2, String>) ->
6300 List<&2, String>:
6301 match entries:
6302 case Nil{}: Nil{}
6303 case Entry{name, names, about} <> tail:
6304 List.append(&2, String,
6305 dests(names, String.join(List.append(&2, String, words, [name]), " ")),
6306 entry_dests(tail, words))
6307
6308def bash_subs(+entries: List<&2, Entry>, words: List<&2, String>) ->
6309 List<&2, String>:
6310 Bool.pick(List<&2, String>, List.is_empty(&2, Entry, entries), [], [
6311 String.concat([" subs=(", sh_list(entry_names(entries), " "), ")"]),
6312 String.concat([" dests=(", sh_list(entry_dests(entries, words), " "),
6313 ")"]),
6314 ])
6315
6316def bash_shape(found: Maybe<&2, Slot>) -> List<&2, String>:
6317 match found:
6318 case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
6319 [String.concat([" many=", Nat.show(k), " rest=",
6320 Bool.pick(String, rest, "1", "0")])]
6321 case None{}: []
6322
6323def bash_scope_arm(stop: Stop) -> List<&2, String>:
6324 match stop:
6325 case Stop{k, +words, opts, builtins, subs, slots}:
6326 String.concat([" ", Sh.quote(String.join(words, " ")), ")"]) <>
6327 last_with(List.concat(&2, String, [
6328 [String.concat([" opts=(", sh_list(List.append(&2, String,
6329 opt_names(opts), opt_names(builtins)), " "), ")"])],
6330 bash_subs(subs, words),
6331 bash_shape(variadic(slots)),
6332 ]), " ;;")
6333
6334def bash_letter(comp: Comp) -> String:
6335 match comp:
6336 case Words{list, values}: "w"
6337 case Files{}: "f"
6338 case Dirs{}: "d"
6339 case Free{}: "s"
6340
6341def bash_values(comp: Comp) -> String:
6342 match comp:
6343 case Words{list, values}: String.concat([" values=(", sh_list(values, " "), ")"])
6344 case _: ""
6345
6346def bash_kind(+comp: Comp, bare: Bool) -> String:
6347 String.concat(["kind=", bash_letter(comp),
6348 Bool.pick(String, bare, " opt=1", ""), bash_values(comp)])
6349
6350def bash_opt_case(opt: Opt) -> List<&2, String>:
6351 match opt:
6352 case Opt{names, OptionKind{many}, ph, comp, help}:
6353 [String.concat([" ", sh_list(names, "|"), ") ",
6354 bash_kind(comp, False{}), " ;;"])]
6355 case Opt{names, BareOptionKind{}, ph, comp, help}:
6356 [String.concat([" ", sh_list(names, "|"), ") ",
6357 bash_kind(comp, True{}), " ;;"])]
6358 case _: []
6359
6360def bash_opt_cases(opts: List<&2, Opt>) -> List<&2, String>:
6361 match opts:
6362 case Nil{}: Nil{}
6363 case opt <> tail: List.append(&2, String, bash_opt_case(opt), bash_opt_cases(tail))
6364
6365def bash_slot_cases(slots: List<&2, Slot>) -> List<&2, String>:
6366 match slots:
6367 case Nil{}: Nil{}
6368 case Slot{k, False{}, required, many, rest, ph, Words{list, values}} <> tail:
6369 String.concat([" ", Nat.show(k), ") ",
6370 bash_kind(Words{list, values}, False{}), " ;;"]) <> bash_slot_cases(tail)
6371 case Slot{k, False{}, required, many, rest, ph, Files{}} <> tail:
6372 String.concat([" ", Nat.show(k), ") kind=f ;;"]) <> bash_slot_cases(tail)
6373 case Slot{k, False{}, required, many, rest, ph, Dirs{}} <> tail:
6374 String.concat([" ", Nat.show(k), ") kind=d ;;"]) <> bash_slot_cases(tail)
6375 case head <> tail: bash_slot_cases(tail)
6376
6377def bash_cases(words: List<&2, String>, cases: List<&2, String>) ->
6378 List<&2, String>:
6379 match cases:
6380 case Nil{}: []
6381 case head <> tail:
6382 List.concat(&2, String, [
6383 [String.concat([" ", Sh.quote(String.join(words, " ")), ")"]),
6384 " case $1 in"],
6385 head <> tail,
6386 [" esac ;;"],
6387 ])
6388
6389def bash_arg_arm(stop: Stop) -> List<&2, String>:
6390 match stop:
6391 case Stop{k, words, opts, builtins, subs, slots}:
6392 bash_cases(words, List.append(&2, String, bash_opt_cases(opts),
6393 bash_slot_cases(completing(slots))))
6394
6395def bash_arms(~f: Stop -> List<&2, String>, stops: List<&2, Stop>) ->
6396 List<&2, String>:
6397 match stops:
6398 case Nil{}: Nil{}
6399 case stop <> tail: List.append(&2, String, f(stop), bash_arms(~f, tail))
6400# The rest of every bash script, for function prefix id and the quoted
6401# command name: the parser of COMP_WORDS, then the binding.
6402def bash_engine(+id: String, name: String) -> List<&2, String>:
6403 [
6404 "",
6405 "# Offers each value of kind that starts with $1, written after $2 and cut to",
6406 "# the part readline completes: the text after the last \"=\" or \":\". Readline",
6407 "# quotes files and directories; words are quoted here.",
6408 String.concat([id, "_offer() {"]),
6409 " local word full i",
6410 " case $kind in",
6411 " f) compopt -o filenames; mapfile -t values < <(compgen -f -- \"$1\") ;;",
6412 " d) compopt -o filenames; mapfile -t values < <(compgen -d -- \"$1\") ;;",
6413 " w)",
6414 " for ((i = 0; i < ${#values[@]}; i++)); do",
6415 " printf -v 'values[i]' %q \"${values[i]}\"",
6416 " done ;;",
6417 " *) return ;;",
6418 " esac",
6419 " for word in \"${values[@]}\"; do",
6420 " full=$2$word",
6421 " [[ $word == \"$1\"* ]] && COMPREPLY+=(\"${full:${#2}+${#1}-${#tail}}\")",
6422 " done",
6423 "}",
6424 "",
6425 String.concat([id, "() {"]),
6426 " local -a words=() opts=() subs=() dests=() values=()",
6427 " local cur tail word line gap path= want= kind opt many rest i j n=0 end=0 help=0",
6428 " COMPREPLY=()",
6429 " # Rejoin the \"=\" and \":\" that bash splits words at. COMP_LINE has no blank",
6430 " # at such a split, so a \"=\" typed as its own word stays one.",
6431 " line=${COMP_LINE#*\"${COMP_WORDS[0]}\"}",
6432 " for ((i = 1; i <= COMP_CWORD; i++)); do",
6433 " word=${COMP_WORDS[i]}",
6434 " tail=${COMP_WORDS[i-1]}",
6435 " gap=${line%%[![:blank:]]*}",
6436 " line=${line:${#gap}+${#word}}",
6437 " if ((i > 1)) && [[ -z $gap ]] &&",
6438 " [[ -n $word && -z ${word//[=:]} || -n $tail && -z ${tail//[=:]} ]]; then",
6439 " words[${#words[@]}-1]+=$word",
6440 " else",
6441 " words+=(\"$word\")",
6442 " fi",
6443 " done",
6444 " tail=${COMP_WORDS[COMP_CWORD]}",
6445 " [[ -z ${tail//[=:]} ]] && tail=",
6446 " cur=${words[${#words[@]}-1]}",
6447 " unset 'words[${#words[@]}-1]'",
6448 String.concat([" ", id, "_scope"]),
6449 " for word in \"${words[@]}\"; do",
6450 " if [[ -n $want ]]; then",
6451 " want=",
6452 " elif ((!end)) && [[ $word == -- ]]; then",
6453 " end=1",
6454 " elif ((!end)) && [[ $word == --* ]]; then",
6455 String.concat([" ", id, "_arg \"${word%%=*}\""]),
6456 " [[ -n $kind && $opt == 0 && $word != *=* ]] && want=$word",
6457 " elif ((!end)) && [[ $word == -?* ]]; then",
6458 " for ((j = 1; j < ${#word}; j++)); do",
6459 String.concat([" ", id, "_arg \"-${word:j:1}\""]),
6460 " [[ -n $kind ]] || continue",
6461 " ((j + 1 == ${#word})) && want=-${word:j:1}",
6462 " break",
6463 " done",
6464 " elif ((${#subs[@]})); then",
6465 " ((end)) && return",
6466 " if [[ $word == help ]] && ((!help)); then",
6467 " help=1",
6468 " continue",
6469 " fi",
6470 " for ((j = 0; j < ${#subs[@]}; j++)); do",
6471 " [[ $word == \"${subs[j]}\" ]] && break",
6472 " done",
6473 " ((j < ${#subs[@]})) || return",
6474 " path=${dests[j]}",
6475 String.concat([" ", id, "_scope"]),
6476 " else",
6477 " # n counts the slots filled. The variadic slot stays open, but a rest()",
6478 " # value takes every later word, so nothing more completes.",
6479 " n=$((n + 1))",
6480 " if ((n == many && rest)); then",
6481 " end=1",
6482 " elif ((n == many)); then",
6483 " n=$((n - 1))",
6484 " fi",
6485 " fi",
6486 " done",
6487 " if [[ -n $want ]]; then",
6488 String.concat([" ", id, "_arg \"$want\""]),
6489 String.concat([" ", id, "_offer \"$cur\" ''"]),
6490 " elif ((!end)) && [[ $cur == --*=* ]]; then",
6491 String.concat([" ", id, "_arg \"${cur%%=*}\""]),
6492 String.concat([" ", id, "_offer \"${cur#*=}\" \"${cur%%=*}=\""]),
6493 " elif ((!end)) && [[ $cur == -* ]]; then",
6494 " values=(\"${opts[@]}\") kind=w",
6495 " ((help)) && values=('-h' '--help')",
6496 String.concat([" ", id, "_offer \"$cur\" ''"]),
6497 " elif ((!end && ${#subs[@]})); then",
6498 " values=(\"${subs[@]}\") kind=w",
6499 " ((help)) || values+=('help')",
6500 String.concat([" ", id, "_offer \"$cur\" ''"]),
6501 " elif ((!help)); then",
6502 String.concat([" ", id, "_arg $((n + 1))"]),
6503 String.concat([" ", id, "_offer \"$cur\" ''"]),
6504 " fi",
6505 "}",
6506 "",
6507 String.concat(["complete -F ", id, " ", name]),
6508 ]
6509
6510def bash_script(+name: String, +items: List<&2, Item>) -> String:
6511 +all = all_places(items)
6512 +found = stops(all, 0n, place_lists(all, []))
6513 +id = String.append("_", ident(name))
6514 text_lines(List.concat(&2, String, [
6515 ["# bash completion; needs bash 4 or later.",
6516 "",
6517 "# Sets the tables of command path $path: opts, the options to offer; subs and",
6518 "# dests, each subcommand name and the path it enters; many, the slot of the",
6519 "# variadic positional, or 0; rest, 1 when that slot is rest().",
6520 String.append(id, "_scope() {"),
6521 " opts=() subs=() dests=() many=0 rest=0",
6522 " case $path in"],
6523 bash_arms(~bash_scope_arm, found),
6524 [" esac",
6525 "}",
6526 "",
6527 "# Sets kind to what the option or positional $1 (a name, or a 1-based slot)",
6528 "# takes at $path: w, a word from values; f, a file; d, a directory; s, other",
6529 "# text. kind is empty for an option that takes no value and for a positional",
6530 "# that completes nothing; opt is 1 when the value can only follow \"=\".",
6531 String.append(id, "_arg() {"),
6532 " kind= opt=0 values=()",
6533 " case $path in"],
6534 bash_arms(~bash_arg_arm, found),
6535 [" esac", "}"],
6536 bash_engine(id, Sh.quote(name)),
6537 ]))
6538
6539# ---- zsh
6540
6541def zsh_action(comp: Comp) -> String:
6542 match comp:
6543 case Words{list, values}: String.concat(["($v", Nat.show(list), ")"])
6544 case Files{}: "_files"
6545 case Dirs{}: "_files -/"
6546 case Free{}: " "
6547
6548# The prefix of an option's specs: * when it repeats, else its names as a
6549# group that excludes each other.
6550def zsh_pre(kind: UsageKind, +names: List<&2, String>) -> String:
6551 match kind:
6552 case CountKind{}: "*"
6553 case OptionKind{True{}}: "*"
6554 case _:
6555 Bool.pick(String, Nat.is_lt(1n, List.length(&2, String, names)),
6556 String.concat(["(", String.join(names, " "), ")"]), "")
6557
6558def zsh_spec(+name: String, kind: UsageKind, pre: String, d: String,
6559 ph: String, act: String) -> String:
6560 match kind:
6561 case OptionKind{many}:
6562 String.concat([pre, name, Bool.pick(String, long_name(name), "=", "+"),
6563 "[", d, "]:", ph, ":", act])
6564 case BareOptionKind{}:
6565 String.concat([pre, name, "=-[", d, "]::", ph, ":", act])
6566 case _: String.concat([pre, name, "[", d, "]"])
6567
6568def zsh_names(names: List<&2, String>, +kind: UsageKind, +pre: String,
6569 +d: String, +ph: String, +act: String) -> List<&2, String>:
6570 match names:
6571 case Nil{}: Nil{}
6572 case name <> tail:
6573 zsh_spec(name, kind, pre, d, ph, act) <>
6574 zsh_names(tail, kind, pre, d, ph, act)
6575
6576def zsh_opts(opts: List<&2, Opt>) -> List<&2, String>:
6577 match opts:
6578 case Nil{}: Nil{}
6579 case Opt{+names, +kind, ph, comp, help} <> tail:
6580 List.append(&2, String,
6581 zsh_names(names, kind, zsh_pre(kind, names), Zsh.esc(help),
6582 Zsh.esc(ph), zsh_action(comp)),
6583 zsh_opts(tail))
6584
6585def zsh_slots(slots: List<&2, Slot>) -> List<&2, String>:
6586 match slots:
6587 case Nil{}: Nil{}
6588 case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
6589 zsh_slots(tail)
6590 case Slot{k, False{}, required, True{}, False{}, ph, comp} <> tail:
6591 String.concat(["*:", Zsh.esc(ph), ":", zsh_action(comp)]) <>
6592 zsh_slots(tail)
6593 case Slot{k, False{}, required, +many, rest, ph, comp} <> tail:
6594 String.concat([Bool.pick(String, many, "(-)", ""), Nat.show(k),
6595 Bool.pick(String, required, ":", "::"), Zsh.esc(ph), ":",
6596 zsh_action(comp)]) <> zsh_slots(tail)
6597
6598# Specs quoted, one per line, all but the last continued.
6599def zsh_spec_lines(specs: List<&2, String>, +ind: String) ->
6600 List<&2, String>:
6601 match specs:
6602 case Nil{}: Nil{}
6603 case spec <> Nil{}: [String.concat([ind, " ", Sh.quote(spec)])]
6604 case spec <> tail:
6605 String.concat([ind, " ", Sh.quote(spec), " \\"]) <>
6606 zsh_spec_lines(tail, ind)
6607
6608def zsh_args(+ind: String, specs: List<&2, String>) -> List<&2, String>:
6609 String.append(ind, "_arguments -C -s -S \\") <> zsh_spec_lines(specs, ind)
6610
6611def zsh_entry(names: List<&2, String>, +ind: String, +about: String) ->
6612 List<&2, String>:
6613 match names:
6614 case Nil{}: Nil{}
6615 case name <> tail:
6616 String.concat([ind, " ", Sh.quote(String.concat([Zsh.esc(name), ":",
6617 about]))]) <> zsh_entry(tail, ind, about)
6618
6619def zsh_entries(entries: List<&2, Entry>, +ind: String) -> List<&2, String>:
6620 match entries:
6621 case Nil{}: Nil{}
6622 case Entry{name, names, +about} <> tail:
6623 List.append(&2, String, zsh_entry(names, ind, Zsh.esc(about)),
6624 zsh_entries(tail, ind))
6625
6626def zsh_commands(entries: List<&2, Entry>, with_help: Bool, label: String,
6627 +ind: String) -> List<&2, String>:
6628 List.concat(&2, String, [
6629 [String.append(ind, "local -a commands=(")],
6630 zsh_entries(entries, ind),
6631 Bool.pick(List<&2, String>, with_help, [String.append(ind,
6632 " 'help:Print this message or the help of the given subcommand(s)'")],
6633 []),
6634 [String.append(ind, ")"),
6635 String.concat([ind, "_describe -t commands ", Sh.quote(label),
6636 " commands"])],
6637 ])
6638
6639def zsh_label(+name: String, words: List<&2, String>) -> String:
6640 String.append(String.join(name <> words, " "), " command")
6641
6642def zsh_deeper(arms: List<&2, String>, +depth: Nat, +ind: String) ->
6643 List<&2, String>:
6644 match arms:
6645 case Nil{}: []
6646 case head <> tail:
6647 List.concat(&2, String, [
6648 [String.append(ind, "else"),
6649 String.concat([ind, " case $words[", Nat.show(depth), "] in"])],
6650 head <> tail,
6651 [String.append(ind, " esac")],
6652 ])
6653
6654# After help at depth, the subcommand names, then deeper by arms.
6655def zsh_help_tree(entries: List<&2, Entry>, label: String,
6656 arms: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6657 List.concat(&2, String, [
6658 [String.concat([ind, "if (( CURRENT == ", Nat.show(depth), " )); then"])],
6659 zsh_commands(entries, False{}, label, String.append(ind, " ")),
6660 zsh_deeper(arms, depth, ind),
6661 [String.append(ind, "fi")],
6662 ])
6663
6664def bar_names(names: List<&2, String>) -> String:
6665 match names:
6666 case Nil{}: ""
6667 case name <> Nil{}: Sh.quote(name)
6668 case name <> tail: String.concat([Sh.quote(name), "|", bar_names(tail)])
6669
6670# The help tree's arms at ind for the subcommands in items that have
6671# subcommands of their own.
6672def zsh_help_arms(items: List<&2, Item>, +name: String,
6673 +words: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6674 match items:
6675 case Nil{}: Nil{}
6676 case Sub{+sub, about, +children} <> tail:
6677 +here = List.append(&2, String, words, [sub])
6678 +entries = sub_entries(children)
6679 List.append(&2, String,
6680 Bool.pick(List<&2, String>,
6681 Bool.or(has_hidden(children), List.is_empty(&2, Entry, entries)), [],
6682 String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6683 last_with(zsh_help_tree(entries, zsh_label(name, here),
6684 zsh_help_arms(children, name, here, (depth + 1n : Nat),
6685 String.append(ind, " ")),
6686 (depth + 1n : Nat), String.append(ind, " ")), " ;;")),
6687 zsh_help_arms(tail, name, words, depth, ind))
6688 case head <> tail: zsh_help_arms(tail, name, words, depth, ind)
6689
6690# Ends a state that _arguments entered past a "--" it dropped, after which
6691# no subcommand follows.
6692def zsh_past_end(ind: String) -> String:
6693 String.append(ind, "(( ${#${(M)words[1,CURRENT-1]:#--}} < ends )) && return 1")
6694
6695# At the top command (no words), the count of "--" before the cursor that
6696# zsh_past_end compares with.
6697def zsh_ends(words: List<&2, String>, +ind: String) -> List<&2, String>:
6698 match words:
6699 case Nil{}:
6700 [String.append(ind,
6701 "# _arguments -S drops a -- typed before a command's first word, so a"),
6702 String.append(ind,
6703 "# state whose words hold fewer -- than the line has is past one."),
6704 String.append(ind, "local -i ends=${#${(M)words[2,CURRENT-1]:#--}}")]
6705 case _: []
6706
6707# The body of a command with subcommands: its specs end in a state that
6708# offers them, completes help's tree, or enters the arm of the one given.
6709def zsh_branch(+entries: List<&2, Entry>, specs: List<&2, String>,
6710 +items: List<&2, Item>, +name: String, +words: List<&2, String>,
6711 arms: List<&2, String>, +ind: String) -> List<&2, String>:
6712 +state = String.join(name <> words, "-")
6713 +i2 = String.append(ind, " ")
6714 +hstate = String.append(state, "-help")
6715 +label = zsh_label(name, words)
6716 List.concat(&2, String, [
6717 zsh_ends(words, ind),
6718 last_with(zsh_args(ind, List.append(&2, String, specs,
6719 [String.append("*:: :->", state)])), " && return"),
6720 [String.append(ind, "case $state in"),
6721 String.concat([ind, " ", Sh.quote(state), ")"]),
6722 zsh_past_end(i2),
6723 String.append(i2, "if (( CURRENT == 1 )); then")],
6724 zsh_commands(entries, True{}, label, String.append(i2, " ")),
6725 [String.append(i2, "else"), String.append(i2, " case $words[1] in")],
6726 arms,
6727 [String.append(i2, " 'help')")],
6728 last_with(zsh_args(String.append(i2, " "), ["(-h --help)-h[Print help]",
6729 "(-h --help)--help[Print help]", String.append("*:: :->", hstate)]),
6730 " && return"),
6731 [String.append(i2, " case $state in"),
6732 String.concat([i2, " ", Sh.quote(hstate), ")"]),
6733 zsh_past_end(String.append(i2, " "))],
6734 last_with(zsh_help_tree(entries, label,
6735 zsh_help_arms(items, name, words, 1n,
6736 String.append(i2, " ")),
6737 1n, String.append(i2, " ")), " ;;"),
6738 [String.append(i2, " esac ;;"), String.append(i2, " esac"),
6739 String.append(i2, "fi ;;"), String.append(ind, "esac")],
6740 ])
6741
6742def zsh_body(entries: List<&2, Entry>, specs: List<&2, String>,
6743 +items: List<&2, Item>, +lists: List<&2, List<&2, String>>,
6744 name: String, words: List<&2, String>, arms: List<&2, String>,
6745 ind: String) -> List<&2, String>:
6746 match entries:
6747 case Nil{}:
6748 zsh_args(ind, List.append(&2, String, specs,
6749 zsh_slots(completing(arg_slots(items, 1n, lists)))))
6750 case head <> tail:
6751 zsh_branch(head <> tail, specs, items, name, words, arms, ind)
6752
6753# The body at ind of the command with items and globals at words, given
6754# the arms of its subcommands.
6755def zsh_cmd(+items: List<&2, Item>, +globals: List<&2, Item>,
6756 +lists: List<&2, List<&2, String>>, name: String,
6757 +words: List<&2, String>, arms: List<&2, String>, ind: String) ->
6758 List<&2, String>:
6759 zsh_body(sub_entries(items),
6760 List.append(&2, String,
6761 zsh_opts(List.append(&2, Opt, arg_opts(items, lists),
6762 arg_opts(globals, lists))),
6763 zsh_opts(builtin_opts(items, globals,
6764 List.is_empty(&2, String, words)))),
6765 items, lists, name, words, arms, ind)
6766
6767# The arms at ind for the visible subcommands in items, which inherit
6768# globals; their bodies are 2 deeper.
6769def zsh_arms(items: List<&2, Item>, +globals: List<&2, Item>,
6770 +lists: List<&2, List<&2, String>>, +name: String,
6771 +words: List<&2, String>, +ind: String) -> List<&2, String>:
6772 match items:
6773 case Nil{}: Nil{}
6774 case Sub{+sub, about, +children} <> tail:
6775 +here = List.append(&2, String, words, [sub])
6776 List.append(&2, String,
6777 Bool.pick(List<&2, String>, has_hidden(children), [],
6778 String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6779 last_with(zsh_cmd(children, globals, lists, name, here,
6780 zsh_arms(children, List.append(&2, Item, globals,
6781 global_items(children)), lists, name, here,
6782 String.append(ind, " ")),
6783 String.append(ind, " ")), " ;;")),
6784 zsh_arms(tail, globals, lists, name, words, ind))
6785 case head <> tail: zsh_arms(tail, globals, lists, name, words, ind)
6786
6787def zsh_lists(lists: List<&2, List<&2, String>>, +k: Nat) ->
6788 List<&2, String>:
6789 match lists:
6790 case Nil{}: Nil{}
6791 case values <> tail:
6792 String.concat(["v", Nat.show(k), "=(", sh_list(values, " "), ")"]) <>
6793 zsh_lists(tail, (k + 1n : Nat))
6794
6795def zsh_arrays(lists: List<&2, List<&2, String>>) -> List<&2, String>:
6796 match lists:
6797 case Nil{}: []
6798 case head <> tail:
6799 [String.append(" local -a ", String.join(zsh_lists(head <> tail, 1n),
6800 " "))]
6801
6802def zsh_script(+name: String, +items: List<&2, Item>) -> String:
6803 +lists = place_lists(all_places(items), [])
6804 +id = String.append("_", ident(name))
6805 text_lines(List.concat(&2, String, [
6806 [String.append("#compdef ", name), "", String.append(id, "() {"),
6807 " local curcontext=$curcontext state line"],
6808 zsh_arrays(lists),
6809 zsh_cmd(items, [], lists, name, [],
6810 zsh_arms(items, global_items(items), lists, name, [], " "),
6811 " "),
6812 ["}",
6813 "",
6814 String.concat(["if [[ $funcstack[1] == ", Sh.quote(String.append("_",
6815 name)), " ]]; then"]),
6816 String.concat([" ", id, " \"$@\""]),
6817 "else",
6818 String.concat([" compdef ", id, " ", Sh.quote(name)]),
6819 "fi"],
6820 ]))
6821
6822# ---- fish
6823
6824# Words quoted for fish, apart by spaces.
6825def fish_list(words: List<&2, String>) -> String:
6826 match words:
6827 case Nil{}: ""
6828 case word <> Nil{}: Fish.quote(word)
6829 case word <> tail: String.concat([Fish.quote(word), " ", fish_list(tail)])
6830
6831def fish_pats(names: List<&2, String>, +pre: String) -> List<&2, String>:
6832 match names:
6833 case Nil{}: Nil{}
6834 case name <> tail: Fish.quote(String.append(pre, name)) <> fish_pats(tail, pre)
6835
6836# The arms of _dest for subcommands entries of the path at words, numbered
6837# pre.
6838def fish_dest_arms(entries: List<&2, Entry>, +pre: String,
6839 +words: List<&2, String>, +found: List<&2, Stop>) -> List<&2, String>:
6840 match entries:
6841 case Nil{}: Nil{}
6842 case Entry{name, names, about} <> tail:
6843 String.append(" case ", String.join(fish_pats(names, pre), " ")) <>
6844 String.append(" echo ", Nat.show(stop_number(found,
6845 List.append(&2, String, words, [name]), None{}))) <>
6846 fish_dest_arms(tail, pre, words, found)
6847
6848def fish_dests(stops: List<&2, Stop>, +found: List<&2, Stop>) ->
6849 List<&2, String>:
6850 match stops:
6851 case Nil{}: Nil{}
6852 case Stop{k, words, opts, builtins, subs, slots} <> tail:
6853 List.append(&2, String,
6854 fish_dest_arms(subs, String.append(Nat.show(k), " "), words, found),
6855 fish_dests(tail, found))
6856
6857def valued_names(opts: List<&2, Opt>) -> List<&2, String>:
6858 match opts:
6859 case Nil{}: Nil{}
6860 case Opt{names, OptionKind{many}, ph, comp, help} <> tail:
6861 List.append(&2, String, names, valued_names(tail))
6862 case head <> tail: valued_names(tail)
6863
6864def fish_valued_arm(k: Nat, names: List<&2, String>) -> List<&2, String>:
6865 match names:
6866 case Nil{}: []
6867 case head <> tail:
6868 [String.append(" case ", Nat.show(k)),
6869 String.append(" contains -- $argv[2] ",
6870 fish_list(head <> tail))]
6871
6872def fish_valued(stop: Stop) -> List<&2, String>:
6873 match stop:
6874 case Stop{k, words, opts, builtins, subs, slots}:
6875 fish_valued_arm(k, valued_names(opts))
6876
6877def slot_shape(found: Maybe<&2, Slot>) -> String:
6878 match found:
6879 case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
6880 String.concat([Nat.show(k), " ", Bool.pick(String, rest, "1", "0")])
6881 case None{}: "0 0"
6882
6883def fish_shape_arm(shown: Bool, k: Nat, has: Bool, found: Maybe<&2, Slot>) ->
6884 List<&2, String>:
6885 match shown:
6886 case True{}:
6887 [String.append(" case ", Nat.show(k)),
6888 String.concat([" echo ", Bool.pick(String, has, "1", "0"),
6889 " ", slot_shape(found)])]
6890 case False{}: []
6891
6892def fish_shape(stop: Stop) -> List<&2, String>:
6893 match stop:
6894 case Stop{k, words, opts, builtins, subs, slots}:
6895 +has = Bool.not(List.is_empty(&2, Entry, subs))
6896 +found = variadic(slots)
6897 fish_shape_arm(Bool.or(has, Maybe.is_some(&2, Slot, found)), k, has,
6898 found)
6899
6900def fish_values(lists: List<&2, List<&2, String>>, +k: Nat) ->
6901 List<&2, String>:
6902 match lists:
6903 case Nil{}: Nil{}
6904 case values <> tail:
6905 String.append(" case ", Nat.show(k)) <>
6906 String.append(" printf '%s\\n' ", fish_list(values)) <>
6907 fish_values(tail, (k + 1n : Nat))
6908
6909def fish_values_fn(+id: String, lists: List<&2, List<&2, String>>) ->
6910 List<&2, String>:
6911 match lists:
6912 case Nil{}: []
6913 case head <> tail:
6914 List.concat(&2, String, [
6915 ["", "# Prints the values of the one_of list numbered $argv[1].",
6916 String.concat(["function ", id, "_values"]), " switch $argv[1]"],
6917 fish_values(head <> tail, 1n),
6918 [" end", "end"],
6919 ])
6920
6921def fish_flag(long: Bool, name: String) -> String:
6922 match long:
6923 case True{}: String.append("-l ", Fish.quote(String.drop(name, 2n)))
6924 case False{}: String.append("-s ", Fish.quote(String.drop(name, 1n)))
6925
6926def fish_flags(names: List<&2, String>) -> List<&2, String>:
6927 match names:
6928 case Nil{}: Nil{}
6929 case +name <> tail: fish_flag(long_name(name), name) <> fish_flags(tail)
6930
6931# What completes a value comp offers: words, files, directories or none.
6932def fish_words(+id: String, comp: Comp) -> String:
6933 match comp:
6934 case Words{list, values}:
6935 String.concat([" -a '(", id, "_values ", Nat.show(list), ")'"])
6936 case Files{}: " -F"
6937 case Dirs{}: " -a '(__fish_complete_directories)'"
6938 case Free{}: ""
6939
6940def fish_needs(comp: Comp) -> String:
6941 match comp:
6942 case Files{}: " -r"
6943 case _: " -x"
6944
6945# An option of kind needs its value (-r), and only a file name may be any
6946# word (-x elsewhere); a [=<V>] value is optional.
6947def fish_kind(kind: UsageKind, +id: String, +comp: Comp) -> String:
6948 match kind:
6949 case OptionKind{many}:
6950 String.append(fish_needs(comp), fish_words(id, comp))
6951 case BareOptionKind{}: fish_words(id, comp)
6952 case _: ""
6953
6954def fish_opt_lines(opts: List<&2, Opt>, +at: String, +id: String) ->
6955 List<&2, String>:
6956 match opts:
6957 case Nil{}: Nil{}
6958 case Opt{names, kind, ph, comp, help} <> tail:
6959 String.concat([at, " n v' ", String.join(fish_flags(names), " "),
6960 fish_kind(kind, id, comp), " -d ", Fish.quote(help)]) <>
6961 fish_opt_lines(tail, at, id)
6962
6963# The built-ins, help first, which also holds after help (h).
6964def fish_builtin_lines(opts: List<&2, Opt>, +at: String, tags: String) ->
6965 List<&2, String>:
6966 match opts:
6967 case Nil{}: Nil{}
6968 case Opt{names, kind, ph, comp, help} <> tail:
6969 String.concat([at, tags, "' ", String.join(fish_flags(names), " "),
6970 " -d ", Fish.quote(help)]) <> fish_builtin_lines(tail, at, " n")
6971
6972def fish_name_lines(names: List<&2, String>, +at: String, +about: String) ->
6973 List<&2, String>:
6974 match names:
6975 case Nil{}: Nil{}
6976 case name <> tail:
6977 String.concat([at, " n h' -a ", Fish.quote(name), " -d ", about]) <>
6978 fish_name_lines(tail, at, about)
6979
6980def fish_sub_lines(entries: List<&2, Entry>, +at: String) -> List<&2, String>:
6981 match entries:
6982 case Nil{}: Nil{}
6983 case Entry{name, names, about} <> tail:
6984 List.append(&2, String, fish_name_lines(names, at, Fish.quote(about)),
6985 fish_sub_lines(tail, at))
6986
6987def fish_slot_lines(slots: List<&2, Slot>, +at: String, +id: String) ->
6988 List<&2, String>:
6989 match slots:
6990 case Nil{}: Nil{}
6991 case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
6992 fish_slot_lines(tail, at, id)
6993 case Slot{k, False{}, required, many, rest, ph, Free{}} <> tail:
6994 fish_slot_lines(tail, at, id)
6995 case Slot{k, False{}, required, many, rest, ph, comp} <> tail:
6996 String.concat([at, " p", Nat.show(k), "'", fish_words(id, comp)]) <>
6997 fish_slot_lines(tail, at, id)
6998
6999def fish_ends(subs: List<&2, Entry>, +at: String, id: String,
7000 slots: List<&2, Slot>) -> List<&2, String>:
7001 match subs:
7002 case Nil{}: fish_slot_lines(completing(slots), at, id)
7003 case head <> tail:
7004 List.append(&2, String, fish_sub_lines(head <> tail, at),
7005 [String.append(at, " n' -a 'help' -d 'Print this message or the help of the given subcommand(s)'")])
7006
7007def fish_lines(stops: List<&2, Stop>, +pre: String, +id: String) ->
7008 List<&2, String>:
7009 match stops:
7010 case Nil{}: Nil{}
7011 case Stop{k, words, opts, builtins, subs, slots} <> tail:
7012 +at = String.append(pre, Nat.show(k))
7013 List.concat(&2, String, [
7014 fish_opt_lines(opts, at, id),
7015 fish_builtin_lines(builtins, at, " n h"),
7016 fish_ends(subs, at, id, slots),
7017 fish_lines(tail, pre, id),
7018 ])
7019
7020# The function every fish script shares, for function prefix id: it walks
7021# the words before the cursor as the bash engine does.
7022def fish_engine(+id: String) -> List<&2, String>:
7023 [
7024 "",
7025 "# Succeeds when the words before the cursor reach path $argv[1] and the next",
7026 "# word can be one of $argv[2..-1]: n, an option or subcommand; h, a",
7027 "# subcommand after help; v, an option's value; p<k>, positional slot k.",
7028 String.concat(["function ", id, "_is"]),
7029 " # fish 4 deprecates -o, which fish 3 needs.",
7030 " set -l words (commandline -xpc 2>/dev/null; or commandline -opc)",
7031 " set -e words[1]",
7032 " set -l path 0",
7033 String.concat([" set -l shape (", id, "_shape 0 | string split ' ')"]),
7034 " set -l want 0",
7035 " set -l end 0",
7036 " set -l help 0",
7037 " set -l n 0",
7038 " for word in $words",
7039 " if test $want = 1",
7040 " set want 0",
7041 " else if test $end = 0; and test \"$word\" = --",
7042 " set end 1",
7043 " else if test $end = 0; and string match -q -r -- '^--.' $word",
7044 String.concat([" if not string match -q -- '*=*' $word; and ", id, "_valued $path $word"]),
7045 " set want 1",
7046 " end",
7047 " else if test $end = 0; and string match -q -r -- '^-.' $word",
7048 " set -l chars (string split '' -- (string sub -s 2 -- $word))",
7049 " for i in (seq (count $chars))",
7050 String.concat([" if ", id, "_valued $path -$chars[$i]"]),
7051 " test $i = (count $chars); and set want 1",
7052 " break",
7053 " end",
7054 " end",
7055 " else if test $shape[1] = 1",
7056 " if test $end = 1",
7057 " return 1",
7058 " else if test $help = 0; and test \"$word\" = help",
7059 " set help 1",
7060 " else",
7061 String.concat([" set path (", id, "_dest $path $word); or return 1"]),
7062 String.concat([" set shape (", id, "_shape $path | string split ' ')"]),
7063 " end",
7064 " else",
7065 " # n counts the slots filled. The variadic slot stays open, but a",
7066 " # rest() value takes every later word, so nothing more completes.",
7067 " set n (math $n + 1)",
7068 " if test $n = $shape[2]; and test $shape[3] = 1",
7069 " set end 1",
7070 " else if test $n = $shape[2]",
7071 " set n (math $n - 1)",
7072 " end",
7073 " end",
7074 " end",
7075 " test $path = $argv[1]; or return 1",
7076 " set -l tags",
7077 " if test $want = 1",
7078 " set tags v",
7079 " else",
7080 " test $end = 0; and test $help = 0; and set -a tags n",
7081 " test $end = 0; and test $help = 1; and set -a tags h",
7082 " test $shape[1] = 0; and test $help = 0; and set -a tags p(math $n + 1)",
7083 " end",
7084 " for tag in $argv[2..-1]",
7085 " contains -- $tag $tags; and return 0",
7086 " end",
7087 " return 1",
7088 "end",
7089 ]
7090
7091def fish_script(+name: String, +items: List<&2, Item>) -> String:
7092 +all = all_places(items)
7093 +lists = place_lists(all, [])
7094 +found = stops(all, 0n, lists)
7095 +id = String.append("__", ident(name))
7096 +cq = Fish.quote(name)
7097 text_lines(List.concat(&2, String, [
7098 ["# fish completion; needs fish 3.3 or later.",
7099 "",
7100 "# Prints the number of the path that subcommand $argv[2] of path $argv[1]",
7101 "# enters; paths are numbered in pre-order from 0, the root.",
7102 String.concat(["function ", id, "_dest"]),
7103 " switch \"$argv[1] $argv[2]\""],
7104 fish_dests(found, found),
7105 [" case '*'", " return 1", " end", "end", "",
7106 "# Succeeds when option $argv[2] takes the next word as its value at path",
7107 "# $argv[1].",
7108 String.concat(["function ", id, "_valued"]),
7109 " switch $argv[1]"],
7110 bash_arms(~fish_valued, found),
7111 [" case '*'", " return 1", " end", "end", "",
7112 "# Prints how path $argv[1] takes words: 1 if it has subcommands, else 0; the",
7113 "# slot of its variadic positional, or 0; 1 if that slot is rest(), else 0.",
7114 String.concat(["function ", id, "_shape"]),
7115 " switch $argv[1]"],
7116 bash_arms(~fish_shape, found),
7117 [" case '*'", " echo 0 0 0", " end", "end"],
7118 fish_values_fn(id, lists),
7119 fish_engine(id),
7120 ["",
7121 "# Option lines also hold at v, so that fish can read grouped short options",
7122 "# before a value, as in -vC DIR.",
7123 String.concat(["complete -c ", cq, " -f"])],
7124 fish_lines(found, String.concat(["complete -c ", cq, " -n '", id, "_is "]),
7125 id),
7126 ]))
7127
7128# ---- man pages
7129
7130def roff_bold(s: String) -> String:
7131 String.concat(["\\fB", roff_text(s), "\\fR"])
7132
7133def roff_bolds(names: List<&2, String>) -> List<&2, String>:
7134 match names:
7135 case Nil{}: Nil{}
7136 case name <> tail: roff_bold(String.trim_start(name)) <> roff_bolds(tail)
7137
7138def roff_texts(texts: List<&2, String>) -> List<&2, String>:
7139 match texts:
7140 case Nil{}: Nil{}
7141 case text <> tail: roff_text(text) <> roff_texts(tail)
7142
7143def spaces_only(s: String) -> Bool:
7144 match s:
7145 case SNil{}: True{}
7146 case SCon{Chr{32}, tail}: spaces_only(tail)
7147 case SCon{head, tail}: False{}
7148
7149def line_mark(blank: Bool, line: String) -> List<&2, Maybe<&2, String>>:
7150 match blank:
7151 case True{}: []
7152 case False{}: [Some{line}]
7153
7154# Each line, None for an empty one, which ends a paragraph; a line of only
7155# spaces is left out.
7156def para_marks(lines: List<&2, String>) -> List<&2, Maybe<&2, String>>:
7157 match lines:
7158 case Nil{}: Nil{}
7159 case SNil{} <> tail: None{} <> para_marks(tail)
7160 case +line <> tail:
7161 List.append(&2, Maybe<&2, String>, line_mark(spaces_only(line), line),
7162 para_marks(tail))
7163
7164# Marked lines escaped, after gap: nothing before the first, .br within a
7165# paragraph, sep between paragraphs.
7166def roff_marks(marks: List<&2, Maybe<&2, String>>, +sep: String,
7167 gap: Maybe<&2, String>) -> List<&2, String>:
7168 match marks gap:
7169 case Nil{} _: Nil{}
7170 case None{} <> tail None{}: roff_marks(tail, sep, None{})
7171 case None{} <> tail Some{g}: roff_marks(tail, sep, Some{sep})
7172 case Some{line} <> tail None{}:
7173 Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7174 case Some{line} <> tail Some{g}:
7175 g <> Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7176
7177def roff_paras(text: String, sep: String) -> List<&2, String>:
7178 roff_marks(para_marks(String.lines(text)), sep, None{})
7179
7180def ph_end(s: String) -> Nat:
7181 match s:
7182 case SNil{}: 0n
7183 case SCon{Chr{62}, tail}: 0n
7184 case SCon{head, tail}: 1n+ph_end(tail)
7185
7186# A placeholder's name in italics, from after its <.
7187def roff_ph(+s: String) -> String:
7188 +end = ph_end(s)
7189 String.concat([roff_text(String.take(s, end)), ">\\fR",
7190 roff_text(String.drop(s, (end + 1n : Nat)))])
7191
7192def roff_value(rest: String) -> String:
7193 match rest:
7194 case SCon{Chr{32}, SCon{Chr{60}, tail}}: String.append(" \\fI<", roff_ph(tail))
7195 case SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, tail}}}:
7196 String.append("[=\\fI<", roff_ph(tail))
7197 case SNil{}: ""
7198 case SCon{head, tail}: roff_text(SCon{head, tail})
7199
7200# Option names bold, the placeholder in italics.
7201def roff_option(+usage: String) -> String:
7202 +cut = value_cut(usage)
7203 String.append(String.join(roff_bolds(String.split(String.take(usage, cut),
7204 ',')), ", "), roff_value(String.drop(usage, cut)))
7205
7206# A positional in italics, apart from its repeat.
7207def roff_positional(+usage: String) -> String:
7208 +cut = value_cut(usage)
7209 String.concat(["\\fI", roff_text(String.take(usage, cut)), "\\fR",
7210 roff_text(String.drop(usage, cut))])
7211
7212def roff_entry(left: String, help: String, attrs: List<&2, Item>) ->
7213 List<&2, String>:
7214 ".TP" <> left <> roff_paras(right_text(True{}, help, attrs), ".IP")
7215
7216def roff_positional_entry(shown: Bool, usage: String, help: String,
7217 attrs: List<&2, Item>) -> List<&2, String>:
7218 match shown:
7219 case True{}: roff_entry(roff_positional(usage), help, attrs)
7220 case False{}: []
7221
7222def roff_positionals(items: List<&2, Item>) -> List<&2, String>:
7223 match items:
7224 case Nil{}: Nil{}
7225 case Arg{+usage, help, +attrs} <> tail:
7226 List.append(&2, String,
7227 roff_positional_entry(Bool.and(arg_pos(usage),
7228 Bool.not(has_hidden(attrs))), usage, help, attrs),
7229 roff_positionals(tail))
7230 case head <> tail: roff_positionals(tail)
7231
7232def roff_option_entry(shown: Bool, usage: String, help: String,
7233 attrs: List<&2, Item>) -> List<&2, String>:
7234 match shown:
7235 case True{}: roff_entry(roff_option(usage), help, attrs)
7236 case False{}: []
7237
7238# The visible options under a heading, "" for OPTIONS.
7239def roff_options(items: List<&2, Item>, +heading: String) -> List<&2, String>:
7240 match items:
7241 case Nil{}: Nil{}
7242 case Arg{+usage, help, +attrs} <> tail:
7243 List.append(&2, String,
7244 roff_option_entry(Bool.and(Bool.not(arg_pos(usage)),
7245 Bool.and(Bool.not(has_hidden(attrs)),
7246 String.eq(find_heading(attrs), heading))), usage, help, attrs),
7247 roff_options(tail, heading))
7248 case head <> tail: roff_options(tail, heading)
7249
7250def roff_builtins(opts: List<&2, Opt>) -> List<&2, String>:
7251 match opts:
7252 case Nil{}: Nil{}
7253 case Opt{names, kind, ph, comp, help} <> tail:
7254 ".TP" <> String.join(roff_bolds(names), ", ") <> help <>
7255 roff_builtins(tail)
7256
7257def roff_headings(titles: List<&2, String>, +args: List<&2, Item>) ->
7258 List<&2, String>:
7259 match titles:
7260 case Nil{}: Nil{}
7261 case +title <> tail:
7262 String.concat([".SS \"", roff_text(title), "\""]) <>
7263 List.append(&2, String, roff_options(args, title),
7264 roff_headings(tail, args))
7265
7266def roff_command(hidden: Bool, +page: String, name: String, about: String,
7267 children: List<&2, Item>) -> List<&2, String>:
7268 match hidden:
7269 case True{}: []
7270 case False{}:
7271 [".TP", String.append(roff_bold(String.concat([page, "-", name])), "(1)"),
7272 Roff.esc(join_filled([one_line(about),
7273 tag("aliases", find_aliases(children))]))]
7274
7275def roff_commands(items: List<&2, Item>, +page: String) -> List<&2, String>:
7276 match items:
7277 case Nil{}: Nil{}
7278 case Sub{name, about, +children} <> tail:
7279 List.append(&2, String,
7280 roff_command(has_hidden(children), page, name, about, children),
7281 roff_commands(tail, page))
7282 case head <> tail: roff_commands(tail, page)
7283
7284def roff_arg(pos: Bool, +usage: String, attrs: List<&2, Item>) -> String:
7285 match pos:
7286 case True{}: roff_positional(usage)
7287 case False{}: roff_option(arg_display(arg_info(usage, attrs)))
7288
7289def roff_env_names(names: List<&2, String>) -> List<&2, String>:
7290 match names:
7291 case Nil{}: Nil{}
7292 case name <> tail: roff_bold(name) <> roff_env_names(tail)
7293
7294# How a variable after the first is read, if there is one.
7295def roff_env_order(rest: List<&2, String>) -> String:
7296 match rest:
7297 case Nil{}: ""
7298 case head <> tail: "; the first that is set wins"
7299
7300def roff_env(hidden: Bool, names: List<&2, String>, +usage: String,
7301 attrs: List<&2, Item>) -> List<&2, String>:
7302 match hidden names:
7303 case False{} name <> +tail:
7304 [".TP", String.join(roff_env_names(name <> tail), ", "),
7305 String.concat(["Used for ", roff_arg(arg_pos(usage), usage, attrs),
7306 " when it is not given", roff_env_order(tail)])]
7307 case _ _: []
7308
7309def roff_envs(items: List<&2, Item>) -> List<&2, String>:
7310 match items:
7311 case Nil{}: Nil{}
7312 case Arg{usage, help, +attrs} <> tail:
7313 List.append(&2, String,
7314 roff_env(has_hidden(attrs), find_envs(attrs), usage, attrs),
7315 roff_envs(tail))
7316 case head <> tail: roff_envs(tail)
7317
7318def roff_examples(items: List<&2, Item>, before: List<&2, String>) ->
7319 List<&2, String>:
7320 match items:
7321 case Nil{}: Nil{}
7322 case Example{line, what} <> tail:
7323 List.concat(&2, String, [
7324 before,
7325 [Roff.esc(String.append(what, ":")), ".IP", ".nf",
7326 Roff.esc(String.append("$ ", line)), ".fi"],
7327 roff_examples(tail, [".PP"]),
7328 ])
7329 case head <> tail: roff_examples(tail, before)
7330
7331def sub_pages(items: List<&2, Item>, +page: String) -> List<&2, String>:
7332 match items:
7333 case Nil{}: Nil{}
7334 case Sub{name, about, +children} <> tail:
7335 +rest = sub_pages(tail, page)
7336 Bool.pick(List<&2, String>, has_hidden(children), rest,
7337 String.concat([page, "-", name]) <> rest)
7338 case head <> tail: sub_pages(tail, page)
7339
7340def init_words(words: List<&2, String>) -> List<&2, String>:
7341 match words:
7342 case Nil{}: Nil{}
7343 case word <> Nil{}: Nil{}
7344 case word <> tail: word <> init_words(tail)
7345
7346# The parent's page, none at the root.
7347def parent_pages(words: List<&2, String>) -> List<&2, String>:
7348 match words:
7349 case Nil{}: []
7350 case word <> Nil{}: []
7351 case word <> tail: [String.join(word <> init_words(tail), "-")]
7352
7353def roff_refs(pages: List<&2, String>) -> List<&2, String>:
7354 match pages:
7355 case Nil{}: Nil{}
7356 case page <> tail: String.append(roff_bold(page), "(1)") <> roff_refs(tail)
7357
7358def roff_see(pages: List<&2, String>) -> List<&2, String>:
7359 match pages:
7360 case Nil{}: []
7361 case head <> tail: [String.join(roff_refs(head <> tail), ", ")]
7362
7363def roff_synopsis(texts: List<&2, String>) -> String:
7364 match texts:
7365 case Nil{}: ""
7366 case path <> tail: String.join(roff_bold(path) <> roff_texts(tail), " ")
7367
7368def roff_section(title: String, lines: List<&2, String>) -> List<&2, String>:
7369 match lines:
7370 case Nil{}: []
7371 case head <> tail: String.append(".SH ", title) <> head <> tail
7372
7373def roff_view(view: View, title: String) -> String:
7374 match view:
7375 case View{+path, +about, +items, +globals, root, stuck}:
7376 +words = String.split(path, ' ')
7377 +page = String.join(words, "-")
7378 +args = List.append(&2, Item, items, globals)
7379 text_lines(List.concat(&2, String, [
7380 [String.concat([".TH \"", roff_text(String.to_upper(page)),
7381 "\" \"1\" \"\" \"", roff_text(title), "\" \"User Commands\""]),
7382 ".SH NAME",
7383 Roff.esc(String.concat([page, " - ", one_line(about)])),
7384 ".SH SYNOPSIS",
7385 roff_synopsis(segment_texts(usage_elems(path, items, globals)))],
7386 roff_section("DESCRIPTION", roff_paras(about, ".PP")),
7387 roff_section("ARGUMENTS", roff_positionals(items)),
7388 [".SH OPTIONS"],
7389 roff_options(args, ""),
7390 roff_builtins(builtin_opts(items, globals, root)),
7391 roff_headings(heading_titles(args, []), args),
7392 roff_section("COMMANDS", roff_commands(items, page)),
7393 roff_section("ENVIRONMENT", roff_envs(args)),
7394 [".SH EXIT STATUS", ".TP", "\\fB0\\fR", "Success", ".TP", "\\fB1\\fR",
7395 "Failure", ".TP", "\\fB2\\fR", "Usage error", ".TP", "\\fB70\\fR",
7396 "Internal software error"],
7397 roff_section("EXAMPLES", roff_examples(items, [])),
7398 roff_section("NOTES",
7399 roff_paras(String.join(after_texts(items), "\n\n"), ".PP")),
7400 roff_section("SEE ALSO", roff_see(List.append(&2, String,
7401 parent_pages(words), sub_pages(items, page)))),
7402 ]))
7403
7404def find_version(items: List<&2, Item>) -> String:
7405 match items:
7406 case Nil{}: ""
7407 case Version{value} <> tail: value
7408 case head <> tail: find_version(tail)
7409
7410# The man page of the command at the longest valid prefix of path (§11).
7411def roff(c: Cmd, path: List<&2, String>) -> String:
7412 match c:
7413 case Cmd{+name, about, +items}:
7414 roff_view(view_walk(path, View{name, about, items, [], True{}, False{}}),
7415 join_filled([name, find_version(items)]))
7416
7417# The completion script for shell s (§11).
7418def script(c: Cmd, s: Shell) -> String:
7419 match c s:
7420 case Cmd{name, about, items} Bash{}: bash_script(name, items)
7421 case Cmd{name, about, items} Zsh{}: zsh_script(name, items)
7422 case Cmd{name, about, items} Fish{}: fish_script(name, items)
7423
7424# ---- Outcome (§8.7, V1-V6) ----
7425
7426type Outcome is Data:
7427 Shown{code: U32, out: String, err: String}
7428 Matched{m: Matches}
7429
7430# The subcommand at path, from found on.
7431def sub_at(path: List<&2, String>, items: List<&2, Item>,
7432 found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
7433 match path:
7434 case Nil{}: found
7435 case name <> tail:
7436 +next = find_sub(items, name)
7437 sub_at(tail, sub_items(next), next)
7438
7439def builtin_name(found: Maybe<&2, SubFound>) -> Maybe<&2, String>:
7440 match found:
7441 case Some{SubFound{name, items}}:
7442 Bool.pick(Maybe<&2, String>, has_builtin(items), Some{name}, None{})
7443 case None{}: None{}
7444
7445def field_values(f: Field) -> List<&2, String>:
7446 match f:
7447 case Field{id, values, source}: values
7448
7449# The shell completions was given, which one_of limits to these three.
7450def shell_named(+name: String) -> Shell:
7451 Bool.pick(Shell, String.eq(name, "zsh"), Zsh{},
7452 Bool.pick(Shell, String.eq(name, "fish"), Fish{}, Bash{}))
7453
7454def builtin_page(man: Bool, c: Cmd, fields: List<&2, Field>) -> String:
7455 match man:
7456 case True{}: roff(c, field_values(field_find(fields, "command")))
7457 case False{}:
7458 script(c, shell_named(head_part(field_values(field_find(fields,
7459 "shell")))))
7460
7461# V6: the completions built-in prints its script and man its page, plain;
7462# any other path is left to decode.
7463def outcome_accepted(builtin: Maybe<&2, String>, c: Cmd,
7464 path: List<&2, String>, fields: List<&2, Field>,
7465 external: Maybe<&2, ExternalArgs>) -> Outcome:
7466 match builtin:
7467 case Some{name}: Shown{0, builtin_page(String.eq(name, "man"), c, fields), ""}
7468 case None{}: Matched{Matches{c, path, fields, external}}
7469
7470def outcome_parse(result: ParseResult, +c: Cmd, h: Host,
7471 +argv: List<&2, String>) -> Outcome:
7472 match result:
7473 case Accepted{+path, fields, external}:
7474 outcome_accepted(builtin_name(sub_at(path, cmd_items(c), None{})), c,
7475 path, fields, external)
7476 case HelpRequested{path, long}:
7477 Shown{0, help_out(term(c, h, argv), view_of(c, path), long), ""}
7478 case VersionRequested{}:
7479 Shown{0, String.concat([cmd_name(c), " ", find_version(cmd_items(c)),
7480 "\n"]), ""}
7481 case SummaryRequired{}:
7482 Shown{2, "", summary_err(term(c, h, argv), view_of(c, []))}
7483 case Rejected{path, fault}:
7484 Shown{2, "", fault_err(term(c, h, argv), fault, c, path, argv)}
7485
7486def outcome_lint(found: List<&2, Problem>, +c: Cmd, +h: Host,
7487 +argv: List<&2, String>) -> Outcome:
7488 match found:
7489 case Nil{}: outcome_parse(parse(c, h, argv), c, h, argv)
7490 case p <> tail:
7491 Shown{70, "", String.append("bug: invalid command-line definition\n",
7492 problem_lines(p <> tail))}
7493
7494# eval through V6: a lint failure, help, version, the V3 summary, an error
7495# or a built-in's output is Shown; Matched is a parse left to decode.
7496def outcome(+c: Cmd, h: Host, argv: List<&2, String>) -> Outcome:
7497 outcome_lint(problems(c), c, h, argv)
7498
7499# A Cmd that passed lint, from linted: eval_linted skips the lint eval runs.
7500type Linted is Data:
7501 Linted{cmd: Cmd}
7502
7503def linted_found(found: List<&2, Problem>, c: Cmd) ->
7504 Result<&2, &2, List<&2, Problem>, Linted>:
7505 match found:
7506 case Nil{}: Done{Linted{c}}
7507 case p <> tail: Fail{p <> tail}
7508
7509# Lint c once: a Linted, or lint's problems in the order problems lists.
7510def linted(+c: Cmd) -> Result<&2, &2, List<&2, Problem>, Linted>:
7511 linted_found(problems(c), c)
7512
7513# outcome on a Linted's spec, without the lint.
7514def outcome_linted(l: Linted, +h: Host, +argv: List<&2, String>) -> Outcome:
7515 match l:
7516 case Linted{+c}: outcome_parse(parse(c, h, argv), c, h, argv)
7517
7518# ---- Eval (§6.6, §7.2, §7.3, V7) ----
7519
7520type Step<-A: Type> is Type:
7521 Go{value: A, term: Term}
7522 Quit{code: U32, out: String, err: String}
7523
7524# E12: a decoder's Usage message as an error at the matched scope, printed
7525# as given, in stderr's colors.
7526def usage_err(t: Term, msg: String, view: View) -> String:
7527 match t view:
7528 case Term{out_color, err_color, width}
7529 View{here, about, items, globals, root, stuck}:
7530 emit(err_color, error_page([plain(msg)], [],
7531 usage_block(usage_elems(here, items, globals), page_width(width))))
7532
7533def faulted(-A: Type, f: Fault, +cmd: Cmd, path: List<&2, String>, h: Host,
7534 argv: List<&2, String>) -> Step<A>:
7535 match f:
7536 case Bug{msg}: Quit{70, "", String.concat(["bug: ", msg, "\n"])}
7537 case Usage{msg}:
7538 Quit{2, "", usage_err(term(cmd, h, argv), msg, view_of(cmd, path))}
7539
7540# V7: Done is Go with term's decisions; a fault exits 70 or 2.
7541def decoded(-A: Type, d: Dec(A), cmd: Cmd, path: List<&2, String>, h: Host,
7542 argv: List<&2, String>) -> Step<A>:
7543 match d:
7544 case Done{a}: Go{a, term(cmd, h, argv)}
7545 case Fail{f}: faulted(A, f, cmd, path, h, argv)
7546
7547def eval_step(-A: Type, decode: @+m: Matches -> Dec(A), o: Outcome, h: Host,
7548 argv: List<&2, String>) -> Step<A>:
7549 match o:
7550 case Shown{code, out, err}: Quit{code, out, err}
7551 case Matched{Matches{+cmd, +path, fields, external}}:
7552 decoded(A, decode(Matches{cmd, path, fields, external}), cmd, path, h,
7553 argv)
7554
7555# eval with the decoder as a value, so a law can quantify over decoders.
7556def eval_with(-A: Type, decode: @+m: Matches -> Dec(A), c: Cmd, +h: Host,
7557 +argv: List<&2, String>) -> Step<A>:
7558 eval_step(A, decode, outcome(c, h, argv), h, argv)
7559
7560# The whole CLI as a function. It must stay a direct forwarding definition:
7561# P10 and P11 are proved of eval_with.
7562def eval(~A: Type, ~decode: @+m: Matches -> Dec(A), c: Cmd, h: Host,
7563 argv: List<&2, String>) -> Step<A>:
7564 eval_with(A, decode, c, h, argv)
7565
7566# eval_linted with the decoder as a value.
7567def eval_linted_with(-A: Type, decode: @+m: Matches -> Dec(A), l: Linted,
7568 +h: Host, +argv: List<&2, String>) -> Step<A>:
7569 eval_step(A, decode, outcome_linted(l, h, argv), h, argv)
7570
7571# eval on a Linted, without linting its spec again. It must stay a direct
7572# forwarding definition: P16, p10_linted and p11_linted are proved of
7573# eval_linted_with.
7574def eval_linted(~A: Type, ~decode: @+m: Matches -> Dec(A), l: Linted,
7575 h: Host, argv: List<&2, String>) -> Step<A>:
7576 eval_linted_with(A, decode, l, h, argv)
7577
7578# Each arg's env name in pre-order: an arg's own, then its subcommands'.
7579def env_names(items: List<&2, Item>) -> List<&2, String>:
7580 match items:
7581 case Nil{}: Nil{}
7582 case Arg{usage, help, attrs} <> tail:
7583 List.append(&2, String, find_envs(attrs), env_names(tail))
7584 case Sub{name, about, children} <> tail:
7585 List.append(&2, String, env_names(children), env_names(tail))
7586 case head <> tail: env_names(tail)
7587
7588# kept, then each name not yet in it.
7589def distinct(names: List<&2, String>, kept: List<&2, String>) ->
7590 List<&2, String>:
7591 match names:
7592 case Nil{}: kept
7593 case name <> tail: distinct(tail, append_unique(kept, name))
7594
7595def vars(c: Cmd) -> List<&2, String>:
7596 match c:
7597 case Cmd{name, about, items}:
7598 distinct(List.append(&2, String, env_names(items),
7599 ["NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE", "TERM", "COLUMNS"]), [])
7600
7601# COLUMNS sets the width when as_nat reads it and it is at least 1. digits
7602# is bound because a temporary argument makes String.is_empty take its
7603# string, adding a take to each caller that reuses one (valid_nat).
7604def columns_ok(+value: String) -> Bool:
7605 digits = Nat.read.trim(value)
7606 Bool.and(valid_nat(value), Bool.not(String.is_empty(digits)))
7607
7608def columns_valid(ok: Bool, value: String) -> Maybe<&2, Nat>:
7609 match ok:
7610 case True{}: Some{nat_of(value)}
7611 case False{}: None{}
7612
7613def columns_width(found: Maybe<&2, String>) -> Maybe<&2, Nat>:
7614 match found:
7615 case None{}: None{}
7616 case Some{+value}: columns_valid(columns_ok(value), value)
7617
7618# stdout's columns if it is a TTY, else stderr's if it is one, else 0.
7619def tty_columns(out_tty: Bool, err_tty: Bool, out_cols: U32,
7620 err_cols: U32) -> Nat:
7621 match out_tty err_tty:
7622 case True{} _: U32.to_nat(out_cols)
7623 case False{} True{}: U32.to_nat(err_cols)
7624 case False{} False{}: 0n
7625
7626# first, unless it is 0.
7627def nonzero(first: Nat, then: Nat) -> Nat:
7628 match first:
7629 case 0n: then
7630 case 1n+p: 1n+p
7631
7632def width_or(found: Maybe<&2, Nat>, then: Nat) -> Nat:
7633 match found:
7634 case None{}: then
7635 case Some{width}: width
7636
7637# §7.3: the host from Run.host's readings; the width is COLUMNS, else the
7638# TTY columns, else 80.
7639def probe(+vars: List<&2, Var>, +out_tty: Bool, +err_tty: Bool,
7640 out_cols: U32, err_cols: U32) -> Host:
7641 Host{width_or(columns_width(var_value(vars, "COLUMNS")),
7642 nonzero(tty_columns(out_tty, err_tty, out_cols, err_cols), 80n)),
7643 vars, out_tty, err_tty}
7644
7645def act_code(fits: Bool, n: U32) -> (U32 & String):
7646 match fits:
7647 case True{}: (n, "")
7648 case False{}:
7649 (70, String.concat(["bug: exit code ", U32.show(n),
7650 " is out of range 0..255\n"]))
7651
7652# §7.2: act's code, or 70 with a stderr line when it exceeds 255.
7653def act_exit(+n: U32) -> (U32 & String):
7654 act_code(U32.is_le(n, 255), n)