cli.bend
cli@0.2.0.0 · 8,031 lines · 988 definitions
1
2
3
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
126
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
252
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
601
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
685
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
731
732
733
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
852
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
868
869type CodeClass is Data:
870 PlainCode{}
871 KeycapCode{}
872 PictoCode{}
873 ToneCode{}
874 JoinerCode{}
875 Vs16Code{}
876
877
878
879
880
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
895
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
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
934
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
956
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
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
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
1001
1002
1003
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
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
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
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
1277
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
1284
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
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
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
1702
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
1776
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
1920def valid_u32(s: String) -> Bool:
1921 Maybe.is_some(&2, U32, U32.read(s))
1922
1923
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
1931
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
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
2097def value_bad(+attrs: List<&2, Item>, value: String) -> Maybe<&2, Bad>:
2098 split_bad(find_delim(attrs), attrs, value)
2099
2100
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
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
2141
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
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
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
2257
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
2283
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
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
2417
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
2459type SubKey is Data:
2460 SubKey{name: String, aliases: List<&2, String>, children: List<&2, Item>}
2461
2462
2463type OptKey is Data:
2464 OptKey{name: String, found: OptFound}
2465
2466
2467type SpecItem is Data:
2468 SpecArg{info: ArgInfo}
2469 SpecOther{item: Item}
2470
2471
2472
2473
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
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
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
2555
2556
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
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
2628
2629def head_named(keys: List<&2, OptKey>, +query: String) -> Bool:
2630 match keys:
2631 case Nil{}: False{}
2632 case OptKey{name, found} <> tail: same_text(query, name)
2633
2634
2635
2636
2637def key_rank(keys: List<&2, OptKey>, +query: String, at: Nat, hit: Bool) ->
2638 Maybe<&2, Nat>:
2639 match keys hit:
2640 case Nil{} _: None{}
2641 case key <> tail True{}: Some{at}
2642 case key <> +tail False{}:
2643 next = head_named(tail, query)
2644 key_rank(tail, query, 1n+at, next)
2645
2646def key_found_at(key: Maybe<&2, OptKey>) -> Maybe<&2, OptFound>:
2647 match key:
2648 case None{}: None{}
2649 case Some{OptKey{name, found}}: Some{found}
2650
2651
2652
2653def key_at(rank: Maybe<&2, Nat>, keys: List<&2, OptKey>) ->
2654 Maybe<&2, OptFound>:
2655 match rank:
2656 case None{}: None{}
2657 case Some{at}: key_found_at(List.get(&2, OptKey, keys, at))
2658
2659
2660def key_lookup(+keys: List<&2, OptKey>, +query: String) ->
2661 Maybe<&2, OptFound>:
2662 hit = head_named(keys, query)
2663 rank = key_rank(keys, query, 0n, hit)
2664 key_at(rank, keys)
2665
2666
2667def key_known(+keys: List<&2, OptKey>, +query: String) -> Bool:
2668 hit = head_named(keys, query)
2669 Maybe.is_some(&2, Nat, key_rank(keys, query, 0n, hit))
2670
2671
2672def find_option_scope(scope: Scope, query: String) ->
2673 Maybe<&2, OptFound>:
2674 match scope:
2675 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2676 external, needs_sub, version, digit_short, inherit}}:
2677 key_lookup(keys, query)
2678
2679
2680def has_alias(items: List<&2, Item>, +query: String, found: Bool) -> Bool:
2681 match items found:
2682 case _ True{}: True{}
2683 case Nil{} False{}: False{}
2684 case Alias{name} <> tail False{}:
2685 has_alias(tail, query, same_text(name, query))
2686 case head <> tail False{}: has_alias(tail, query, False{})
2687
2688def alias_hit(aliased: Bool, name: String, children: List<&2, Item>) ->
2689 Maybe<&2, SubFound>:
2690 match aliased:
2691 case True{}: Some{SubFound{name, children}}
2692 case False{}: None{}
2693
2694def sub_hit(same: Bool, name: String, +children: List<&2, Item>,
2695 query: String) -> Maybe<&2, SubFound>:
2696 match same:
2697 case True{}: Some{SubFound{name, children}}
2698 case False{}:
2699 alias_hit(has_alias(children, query, False{}), name, children)
2700
2701def sub_found(item: Item, +query: String) -> Maybe<&2, SubFound>:
2702 match item:
2703 case Sub{+name, about, children}:
2704 sub_hit(same_text(query, name), name, children, query)
2705 case _: None{}
2706
2707
2708def find_sub_go(items: List<&2, Item>, +query: String,
2709 found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
2710 match items found:
2711 case _ Some{hit}: Some{hit}
2712 case Nil{} None{}: None{}
2713 case item <> tail None{}: find_sub_go(tail, query, sub_found(item, query))
2714
2715def find_sub(items: List<&2, Item>, +query: String) ->
2716 Maybe<&2, SubFound>:
2717 find_sub_go(items, query, None{})
2718
2719def positional_items(items: List<&2, Item>) -> List<&2, Item>:
2720 match items:
2721 case Nil{}: Nil{}
2722 case Arg{+usage, +help, +attrs} <> tail:
2723 +rest = positional_items(tail)
2724 Bool.pick(List<&2, Item>,
2725 usage_kind(~kind_pos, parse_usage(usage)),
2726 Arg{usage, help, attrs} <> rest, rest)
2727 case head <> tail: positional_items(tail)
2728
2729def has_variadic_pos(items: List<&2, Item>) -> Bool:
2730 match items:
2731 case Nil{}: False{}
2732 case Arg{usage, help, attrs} <> tail:
2733 Bool.or(usage_kind(~kind_variadic_pos, parse_usage(usage)),
2734 has_variadic_pos(tail))
2735 case head <> tail: has_variadic_pos(tail)
2736
2737def positional_can_take(items: List<&2, Item>, count: Nat) -> Bool:
2738 +slots = positional_items(items)
2739 Bool.or(Nat.is_lt(count, List.length(&2, Item, slots)),
2740 has_variadic_pos(slots))
2741
2742
2743
2744def opt_key(+name: String, info: ArgInfo) -> OptKey:
2745 match info:
2746 case ArgInfo{id, usage, short, +long, +kind, attrs}:
2747 OptKey{name, OptFound{ArgInfo{id, usage, short, long, kind, attrs},
2748 Bool.not(Bool.and(kind_negatable(kind),
2749 Bool.and(Bool.not(String.is_empty(long)),
2750 String.eq(name, negated_long(long)))))}}
2751
2752def name_keys(names: List<&2, String>, +info: ArgInfo) ->
2753 List<&2, OptKey>:
2754 match names:
2755 case Nil{}: Nil{}
2756 case name <> tail: opt_key(name, info) <> name_keys(tail, info)
2757
2758def nonempty_name(+name: String) -> List<&2, String>:
2759 Bool.pick(List<&2, String>, String.is_empty(name), [], [name])
2760
2761
2762def info_names(info: ArgInfo) -> List<&2, String>:
2763 match info:
2764 case ArgInfo{id, usage, short, +long, kind, attrs}:
2765 List.append(&2, String, nonempty_name(short),
2766 List.append(&2, String, nonempty_name(long),
2767 List.append(&2, String, find_aliases(attrs),
2768 Bool.pick(List<&2, String>, Bool.and(kind_negatable(kind),
2769 Bool.not(String.is_empty(long))),
2770 [negated_long(long)], []))))
2771
2772def spec_keys(items: List<&2, SpecItem>) -> List<&2, OptKey>:
2773 match items:
2774 case Nil{}: Nil{}
2775 case SpecArg{+info} <> tail:
2776 List.append(&2, OptKey, name_keys(info_names(info), info),
2777 spec_keys(tail))
2778 case head <> tail: spec_keys(tail)
2779
2780
2781def spec_items(items: List<&2, Item>) -> List<&2, SpecItem>:
2782 match items:
2783 case Nil{}: Nil{}
2784 case Arg{usage, help, attrs} <> tail:
2785 SpecArg{arg_info(usage, attrs)} <> spec_items(tail)
2786 case Sub{name, about, children} <> tail: spec_items(tail)
2787 case head <> tail: SpecOther{head} <> spec_items(tail)
2788
2789
2790def global_spec(items: List<&2, SpecItem>, +all: List<&2, Item>) ->
2791 List<&2, SpecItem>:
2792 match items:
2793 case Nil{}: Nil{}
2794 case SpecArg{+info} <> tail:
2795 +rest = global_spec(tail, all)
2796 attrs = info_attrs(info)
2797 Bool.pick(List<&2, SpecItem>, has_global(attrs),
2798 SpecArg{info} <> rest, rest)
2799 case SpecOther{Exclusive{+name}} <> tail:
2800 +rest = global_spec(tail, all)
2801 Bool.pick(List<&2, SpecItem>, group_global(all, name),
2802 SpecOther{Exclusive{name}} <> rest, rest)
2803 case SpecOther{NeedOne{+name}} <> tail:
2804 +rest = global_spec(tail, all)
2805 Bool.pick(List<&2, SpecItem>, group_global(all, name),
2806 SpecOther{NeedOne{name}} <> rest, rest)
2807 case head <> tail: global_spec(tail, all)
2808
2809def spec_slots(items: List<&2, SpecItem>) -> List<&2, ArgInfo>:
2810 match items:
2811 case Nil{}: Nil{}
2812 case SpecArg{+info} <> tail:
2813 +rest = spec_slots(tail)
2814 Bool.pick(List<&2, ArgInfo>, kind_pos(info_kind(info)),
2815 info <> rest, rest)
2816 case head <> tail: spec_slots(tail)
2817
2818def slots_variadic(slots: List<&2, ArgInfo>) -> Bool:
2819 match slots:
2820 case Nil{}: False{}
2821 case info <> tail:
2822 Bool.or(kind_variadic_pos(info_kind(info)), slots_variadic(tail))
2823
2824def info_digit_short(info: ArgInfo) -> Bool:
2825 match info:
2826 case ArgInfo{id, usage, short, long, kind, attrs}: short_digit(short)
2827
2828def spec_digit_short(items: List<&2, SpecItem>) -> Bool:
2829 match items:
2830 case Nil{}: False{}
2831 case SpecArg{info} <> tail:
2832 Bool.or(info_digit_short(info), spec_digit_short(tail))
2833 case head <> tail: spec_digit_short(tail)
2834
2835def raw_subs(items: List<&2, Item>) -> List<&2, SubKey>:
2836 match items:
2837 case Nil{}: Nil{}
2838 case Sub{name, about, +children} <> tail:
2839 SubKey{name, find_aliases(children), children} <> raw_subs(tail)
2840 case head <> tail: raw_subs(tail)
2841
2842
2843
2844def spec_node(+items: List<&2, Item>, +globals: List<&2, SpecItem>) ->
2845 Spec:
2846 +own = spec_items(items)
2847 +args = List.append(&2, SpecItem, globals, own)
2848 +slots = spec_slots(own)
2849 Spec{List.append(&2, OptKey, spec_keys(own), spec_keys(globals)),
2850 args, slots, slots_variadic(slots), raw_subs(items),
2851 has_sub_item(items), has_external(items), needs_sub(items),
2852 has_version_item(items), spec_digit_short(args),
2853 List.append(&2, SpecItem, global_spec(own, items), globals)}
2854
2855def scan_start(c: Cmd) -> ScanState:
2856 match c:
2857 case Cmd{name, about, items}:
2858 ScanState{Scope{[], spec_node(items, [])}, [], False{}, 0n, None{},
2859 None{}}
2860
2861def scan_event(state: ScanState, event: ScanEvent) -> ScanState:
2862 match state:
2863 case ScanState{scope, events, resting, positional_count,
2864 external, issue}:
2865 ScanState{scope, event <> events, resting,
2866 positional_count, external, issue}
2867
2868def first_issue(issue: Maybe<&2, ScanFault>, fault: ScanFault) ->
2869 Maybe<&2, ScanFault>:
2870 match issue:
2871 case None{}: Some{fault}
2872 case Some{old}: Some{old}
2873
2874def scan_error(state: ScanState, fault: ScanFault) -> ScanState:
2875 match state:
2876 case ScanState{scope, events, resting, positional_count,
2877 external, issue}:
2878 ScanState{scope, events, resting, positional_count,
2879 external, first_issue(issue, fault)}
2880
2881def has_hyphen(items: List<&2, Item>) -> Bool:
2882 match items:
2883 case Nil{}: False{}
2884 case Hyphen{} <> tail: True{}
2885 case head <> tail: has_hyphen(tail)
2886
2887def has_as_f32(items: List<&2, Item>) -> Bool:
2888 match items:
2889 case Nil{}: False{}
2890 case As{TF32{}} <> tail: True{}
2891 case head <> tail: has_as_f32(tail)
2892
2893
2894
2895type SlotTraits is Data:
2896 SlotTraits{rest: Bool, hyphen: Bool, f32: Bool}
2897
2898
2899
2900def slot_here(slots: List<&2, ArgInfo>, count: Nat) -> Bool:
2901 match slots:
2902 case Nil{}: False{}
2903 case ArgInfo{id, usage, short, long, kind, attrs} <> tail:
2904 Bool.or(Nat.is_eq(count, 0n), kind_variadic_pos(kind))
2905
2906
2907
2908
2909def slot_traits_go(slots: List<&2, ArgInfo>, count: Nat, here: Bool) ->
2910 SlotTraits:
2911 match slots here:
2912 case Nil{} _: SlotTraits{False{}, False{}, False{}}
2913 case ArgInfo{id, usage, short, long, kind, +attrs} <> tail True{}:
2914 SlotTraits{has_rest(attrs), has_hyphen(attrs), has_as_f32(attrs)}
2915 case info <> +tail False{}:
2916 +rest = Nat.sub(count, 1n)
2917 next = slot_here(tail, rest)
2918 slot_traits_go(tail, rest, next)
2919
2920def scope_slot_traits(scope: Scope, +count: Nat) -> SlotTraits:
2921 match scope:
2922 case Scope{path, Spec{keys, args, +slots, variadic, subs, has_sub,
2923 external, needs_sub, version, digit_short, inherit}}:
2924 slot_traits_go(slots, count, slot_here(slots, count))
2925
2926def traits_rest(traits: SlotTraits) -> Bool:
2927 match traits:
2928 case SlotTraits{rest, hyphen, f32}: rest
2929
2930def is_negative_number(+s: String) -> Bool:
2931 Bool.and(String.starts_with(s, "-"), valid_f32(s))
2932
2933def before_equal(s: String) -> String:
2934 match s:
2935 case SNil{}: SNil{}
2936 case SCon{Chr{61}, tail}: SNil{}
2937 case SCon{head, tail}: SCon{head, before_equal(tail)}
2938
2939def after_equal(s: String) -> Maybe<&2, String>:
2940 match s:
2941 case SNil{}: None{}
2942 case SCon{Chr{61}, tail}: Some{tail}
2943 case SCon{head, tail}: after_equal(tail)
2944
2945def first_option_name(+token: String) -> String:
2946 Bool.pick(String, String.starts_with(token, "--"),
2947 before_equal(token), String.take(token, 2n))
2948
2949def scope_option_known(scope: Scope, token: String) -> Bool:
2950 match scope:
2951 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2952 external, needs_sub, version, digit_short, inherit}}:
2953 key_known(keys, first_option_name(token))
2954
2955def scope_builtin_known(scope: Scope, +token: String) -> Bool:
2956 match scope:
2957 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2958 external, needs_sub, version, digit_short, inherit}}:
2959 +name = first_option_name(token)
2960 Bool.or(String.eq(name, "-h"),
2961 Bool.or(String.eq(name, "--help"),
2962 Bool.and(List.is_empty(&2, String, path),
2963 Bool.and(version,
2964 Bool.or(String.eq(name, "-V"),
2965 String.eq(name, "--version"))))))
2966
2967def scope_any_option_known(+scope: Scope, +token: String) -> Bool:
2968 Bool.or(scope_option_known(scope, token),
2969 scope_builtin_known(scope, token))
2970
2971def scan_operand_ok(state: ScanState, token: String) -> ScanState:
2972 match state:
2973 case ScanState{+scope, events, resting, +positional_count,
2974 external, issue}:
2975 ScanState{scope, Operand{token} <> events,
2976 Bool.or(resting,
2977 traits_rest(scope_slot_traits(scope, positional_count))),
2978 Nat.add(positional_count, 1n), external, issue}
2979
2980def scan_operand_case(allowed: Bool, state: ScanState,
2981 token: String) -> ScanState:
2982 match allowed:
2983 case True{}: scan_operand_ok(state, token)
2984 case False{}: scan_error(state, E09{token})
2985
2986def scope_can_take(scope: Scope, count: Nat) -> Bool:
2987 match scope:
2988 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
2989 external, needs_sub, version, digit_short, inherit}}:
2990 Bool.or(Nat.is_lt(count, List.length(&2, ArgInfo, slots)), variadic)
2991
2992def scan_operand(state: ScanState, token: String) -> ScanState:
2993 match state:
2994 case ScanState{+scope, events, resting, +positional_count,
2995 external, issue}:
2996 scan_operand_case(scope_can_take(scope, positional_count),
2997 ScanState{scope, events, resting, positional_count,
2998 external, issue}, token)
2999
3000def spec_inherit(spec: Spec) -> List<&2, SpecItem>:
3001 match spec:
3002 case Spec{keys, args, slots, variadic, subs, has_sub, external,
3003 needs_sub, version, digit_short, inherit}: inherit
3004
3005def event_id(event: ScanEvent) -> Maybe<&2, String>:
3006 match event:
3007 case FlagHit{id, value}: Some{id}
3008 case CountHit{id}: Some{id}
3009 case ValueHit{id, value}: Some{id}
3010 case _: None{}
3011
3012def spec_info_found(item: SpecItem, id: String) -> Maybe<&2, ArgInfo>:
3013 match item:
3014 case SpecArg{+info}:
3015 own = info_id(info)
3016 Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
3017 case SpecOther{other}: None{}
3018
3019
3020def find_spec_info(items: List<&2, SpecItem>, +id: String,
3021 found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
3022 match items found:
3023 case Nil{} hit: hit
3024 case item <> tail Some{hit}: Some{hit}
3025 case item <> tail None{}:
3026 find_spec_info(tail, id, spec_info_found(item, id))
3027
3028
3029
3030
3031def event_inherited(found: Maybe<&2, String>,
3032 +globals: List<&2, SpecItem>) -> Bool:
3033 match found:
3034 case None{}: True{}
3035 case Some{id}:
3036 Maybe.is_some(&2, ArgInfo, find_spec_info(globals, id, None{}))
3037
3038def events_kept(keep: Bool, event: ScanEvent,
3039 rest: List<&2, ScanEvent>) -> List<&2, ScanEvent>:
3040 match keep:
3041 case True{}: event <> rest
3042 case False{}: rest
3043
3044def events_inherited(events: List<&2, ScanEvent>,
3045 +globals: List<&2, SpecItem>) -> List<&2, ScanEvent>:
3046 match events:
3047 case Nil{}: Nil{}
3048 case +event <> tail:
3049 events_kept(event_inherited(event_id(event), globals), event,
3050 events_inherited(tail, globals))
3051
3052def scan_descend(state: ScanState, sub: SubKey) -> ScanState:
3053 match state:
3054 case ScanState{scope, events, resting, positional_count,
3055 external, issue}:
3056 match scope:
3057 case Scope{path, spec}:
3058 match sub:
3059 case SubKey{+name, aliases, children}:
3060 +globals = spec_inherit(spec)
3061 ScanState{Scope{
3062 List.append(&2, String, path, [name]),
3063 spec_node(children, globals)},
3064 Enter{name} <> events_inherited(events, globals), resting, 0n,
3065 external, issue}
3066
3067def scan_external(state: ScanState, name: String,
3068 args: List<&2, String>) -> ScanState:
3069 match state:
3070 case ScanState{scope, events, resting, positional_count,
3071 external, issue}:
3072 ScanState{scope, events, resting, positional_count,
3073 Some{ExternalArgs{name, args}}, issue}
3074
3075
3076def scan_value_result(found: Maybe<&2, Bad>, state: ScanState,
3077 info: ArgInfo, value: String) -> ScanState:
3078 match found:
3079 case None{}:
3080 match info:
3081 case ArgInfo{id, usage, short, long, kind, attrs}:
3082 scan_event(state, ValueHit{id, value})
3083 case Some{Bad{bad, why}}:
3084 scan_error(state, E04{arg_display(info), bad, why})
3085
3086def scan_value(state: ScanState, +info: ArgInfo,
3087 +value: String) -> ScanState:
3088 scan_value_result(value_bad(info_attrs(info), value), state, info, value)
3089
3090def scan_simple_hit(state: ScanState, info: ArgInfo,
3091 polarity: Bool) -> ScanState:
3092 match info:
3093 case ArgInfo{id, usage, short, long, kind, attrs}:
3094 match kind:
3095 case CountKind{}: scan_event(state, CountHit{id})
3096 case _: scan_event(state, FlagHit{id, polarity})
3097
3098def scan_settle_case(resting: Bool, issue: Maybe<&2, ScanFault>,
3099 scope: Scope, events: List<&2, ScanEvent>, positional_count: Nat,
3100 external: Maybe<&2, ExternalArgs>) -> Mode:
3101 match resting issue:
3102 case True{} None{}:
3103 Operands{ScanState{scope, events, True{}, positional_count, external,
3104 None{}}, False{}}
3105 case True{} Some{fault}:
3106 Operands{ScanState{scope, events, True{}, positional_count, external,
3107 Some{fault}}, False{}}
3108 case False{} None{}:
3109 Open{ScanState{scope, events, False{}, positional_count, external,
3110 None{}}}
3111 case False{} Some{fault}:
3112 Closed{Faulted{ScanState{scope, events, False{}, positional_count,
3113 external, Some{fault}}}}
3114
3115def scan_settle(state: ScanState) -> Mode:
3116 match state:
3117 case ScanState{scope, events, resting, positional_count, external,
3118 issue}:
3119 scan_settle_case(resting, issue, scope, events, positional_count,
3120 external)
3121
3122def help_flag_case(long: Bool, short: Bool) -> Maybe<&2, Bool>:
3123 match long short:
3124 case True{} _: Some{True{}}
3125 case False{} True{}: Some{False{}}
3126 case False{} False{}: None{}
3127
3128
3129def help_flag(+token: String) -> Maybe<&2, Bool>:
3130 help_flag_case(String.eq(token, "--help"), String.eq(token, "-h"))
3131
3132def scan_faulted_case(help: Maybe<&2, Bool>, state: ScanState) -> Tail:
3133 match help:
3134 case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3135 case None{}: Faulted{state}
3136
3137def scan_faulted(state: ScanState, token: String) -> Tail:
3138 scan_faulted_case(help_flag(token), state)
3139
3140def scope_digit_short(scope: Scope) -> Bool:
3141 match scope:
3142 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3143 external, needs_sub, version, digit_short, inherit}}: digit_short
3144
3145def arg_has_hyphen(info: ArgInfo) -> Bool:
3146 match info:
3147 case ArgInfo{id, usage, short, long, kind, attrs}:
3148 has_hyphen(attrs)
3149
3150def arg_is_f32(info: ArgInfo) -> Bool:
3151 match info:
3152 case ArgInfo{id, usage, short, long, kind, attrs}:
3153 has_as_f32(attrs)
3154
3155
3156def fetch_dash_allowed(scope: Scope, +info: ArgInfo,
3157 +token: String) -> Bool:
3158 Bool.and(Bool.not(String.eq(token, "--")),
3159 Bool.or(String.eq(token, "-"),
3160 Bool.or(arg_has_hyphen(info),
3161 Bool.and(arg_is_f32(info),
3162 Bool.and(is_negative_number(token),
3163 Bool.not(scope_digit_short(scope)))))))
3164
3165def fetch_allowed_case(dash: Bool, scope: Scope, info: ArgInfo,
3166 token: String) -> Bool:
3167 match dash:
3168 case True{}: fetch_dash_allowed(scope, info, token)
3169 case False{}: True{}
3170
3171def fetch_allowed(scope: Scope, info: ArgInfo, +token: String) -> Bool:
3172 fetch_allowed_case(String.starts_with(token, "-"), scope, info, token)
3173
3174def scan_fetch_result(allowed: Bool, state: ScanState,
3175 info: ArgInfo, token: String) -> Mode:
3176 match allowed:
3177 case True{}: scan_settle(scan_value(state, info, token))
3178 case False{}:
3179 Closed{scan_faulted(scan_error(state, E03{arg_display(info)}), token)}
3180
3181def scan_state_scope(state: ScanState) -> Scope:
3182 match state:
3183 case ScanState{scope, events, resting, positional_count,
3184 external, issue}: scope
3185
3186def scan_fetch(+state: ScanState, +info: ArgInfo,
3187 +token: String) -> Mode:
3188 scan_fetch_result(fetch_allowed(scan_state_scope(state), info, token),
3189 state, info, token)
3190
3191def bare_value(attrs: List<&2, Item>) -> String:
3192 match attrs:
3193 case Nil{}: ""
3194 case Bare{value} <> tail: value
3195 case head <> tail: bare_value(tail)
3196
3197def scan_option(attached: Maybe<&2, String>, state: ScanState,
3198 info: ArgInfo) -> Mode:
3199 match attached:
3200 case Some{value}: scan_settle(scan_value(state, info, value))
3201 case None{}: Fetch{state, info}
3202
3203def info_bare_value(info: ArgInfo) -> String:
3204 match info:
3205 case ArgInfo{id, usage, short, long, kind, attrs}:
3206 bare_value(attrs)
3207
3208def scan_bare_option(attached: Maybe<&2, String>, state: ScanState,
3209 +info: ArgInfo) -> Mode:
3210 match attached:
3211 case Some{value}: scan_settle(scan_value(state, info, value))
3212 case None{}:
3213 scan_settle(scan_value(state, info, info_bare_value(info)))
3214
3215def scan_simple_option(attached: Maybe<&2, String>, state: ScanState,
3216 info: ArgInfo, polarity: Bool) -> Mode:
3217 match attached:
3218 case Some{value}:
3219 scan_settle(scan_error(state, E05{arg_display(info), value}))
3220 case None{}: scan_settle(scan_simple_hit(state, info, polarity))
3221
3222def scan_found_kind(kind: UsageKind, attached: Maybe<&2, String>,
3223 state: ScanState, info: ArgInfo, polarity: Bool) -> Mode:
3224 match kind:
3225 case FlagKind{}:
3226 scan_simple_option(attached, state, info, polarity)
3227 case CountKind{}:
3228 scan_simple_option(attached, state, info, polarity)
3229 case NegatableKind{}:
3230 scan_simple_option(attached, state, info, polarity)
3231 case OptionKind{many}:
3232 scan_option(attached, state, info)
3233 case BareOptionKind{}:
3234 scan_bare_option(attached, state, info)
3235 case PosKind{required, many}:
3236 scan_settle(state)
3237
3238def scan_found(found: OptFound, attached: Maybe<&2, String>,
3239 state: ScanState) -> Mode:
3240 match found:
3241 case OptFound{+arg, polarity}:
3242 scan_found_kind(info_kind(arg), attached, state, arg, polarity)
3243
3244def scope_version(scope: Scope) -> Bool:
3245 match scope:
3246 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3247 external, needs_sub, version, digit_short, inherit}}:
3248 Bool.and(List.is_empty(&2, String, path), version)
3249
3250def scan_builtin_hit(attached: Maybe<&2, String>, state: ScanState,
3251 event: ScanEvent, name: String) -> Mode:
3252 match attached:
3253 case Some{value}: scan_settle(scan_error(state, E05{name, value}))
3254 case None{}: Closed{Stopped{scan_event(state, event)}}
3255
3256def traits_hyphen(traits: SlotTraits) -> Bool:
3257 match traits:
3258 case SlotTraits{rest, hyphen, f32}: Bool.or(rest, hyphen)
3259
3260
3261
3262def scan_state_hyphen(state: ScanState) -> Bool:
3263 match state:
3264 case ScanState{scope, events, resting, positional_count,
3265 external, issue}:
3266 traits_hyphen(scope_slot_traits(scope, positional_count))
3267
3268
3269
3270def long_token(attached: Maybe<&2, String>, name: String) -> String:
3271 match attached:
3272 case Some{value}: String.append(name, SCon{Chr{61}, value})
3273 case None{}: name
3274
3275def scan_unknown_long_case(hyphen: Bool, name: String,
3276 attached: Maybe<&2, String>, state: ScanState) -> Mode:
3277 match hyphen:
3278 case True{}: scan_settle(scan_operand(state, long_token(attached, name)))
3279 case False{}: scan_settle(scan_error(state, E01{name}))
3280
3281
3282
3283
3284
3285def scan_unknown_long(name: String, attached: Maybe<&2, String>,
3286 +state: ScanState) -> Mode:
3287 scan_unknown_long_case(scan_state_hyphen(state), name, attached, state)
3288
3289def scan_builtin_other(version: Bool, name: String,
3290 attached: Maybe<&2, String>, state: ScanState) -> Mode:
3291 match version:
3292 case True{}:
3293 scan_builtin_hit(attached, state, VersionHit{}, "--version")
3294 case False{}: scan_unknown_long(name, attached, state)
3295
3296def scan_builtin_long_case(help: Bool, +name: String,
3297 attached: Maybe<&2, String>, +state: ScanState) -> Mode:
3298 match help:
3299 case True{}:
3300 scan_builtin_hit(attached, state, HelpHit{True{}}, "--help")
3301 case False{}:
3302 scan_builtin_other(Bool.and(String.eq(name, "--version"),
3303 scope_version(scan_state_scope(state))), name, attached, state)
3304
3305def scan_builtin_long(+name: String, attached: Maybe<&2, String>,
3306 state: ScanState) -> Mode:
3307 scan_builtin_long_case(String.eq(name, "--help"), name, attached, state)
3308
3309def scan_long_found(found: Maybe<&2, OptFound>, name: String,
3310 attached: Maybe<&2, String>, state: ScanState) -> Mode:
3311 match found:
3312 case Some{option}: scan_found(option, attached, state)
3313 case None{}: scan_builtin_long(name, attached, state)
3314
3315def scan_long(+token: String, +state: ScanState) -> Mode:
3316 +name = before_equal(token)
3317 scan_long_found(find_option_scope(scan_state_scope(state), name),
3318 name, after_equal(token), state)
3319
3320
3321def scan_state_keys(state: ScanState) -> List<&2, OptKey>:
3322 match state:
3323 case ScanState{Scope{path, Spec{keys, args, slots, variadic, subs,
3324 has_sub, external, needs_sub, version, digit_short, inherit}},
3325 events, resting, positional_count, ext, issue}: keys
3326
3327def find_short_for_head(chars: String, keys: List<&2, OptKey>) ->
3328 Maybe<&2, OptFound>:
3329 match chars:
3330 case SNil{}: None{}
3331 case SCon{head, tail}:
3332 key_lookup(keys, String.append("-", Char.show(head)))
3333
3334def short_attached(chars: String) -> String:
3335 match chars:
3336 case SCon{Chr{61}, tail}: tail
3337 case _: chars
3338
3339def scan_short_value_attached(value: String, state: ScanState,
3340 info: ArgInfo) -> Mode:
3341 match value:
3342 case SNil{}: Fetch{state, info}
3343 case _: scan_settle(scan_value(state, info, value))
3344
3345def scan_short_value(chars: String, state: ScanState,
3346 info: ArgInfo) -> Mode:
3347 scan_short_value_attached(short_attached(chars), state, info)
3348
3349def scan_short_bare_value(chars: String, state: ScanState,
3350 +info: ArgInfo) -> Mode:
3351 match chars:
3352 case SNil{}:
3353 scan_settle(scan_value(state, info, info_bare_value(info)))
3354 case _:
3355 scan_settle(scan_value(state, info, short_attached(chars)))
3356
3357def scan_short_version(enabled: Bool, state: ScanState) -> Mode:
3358 match enabled:
3359 case True{}: Closed{Stopped{scan_event(state, VersionHit{})}}
3360 case False{}: scan_settle(scan_error(state, E01{"-V"}))
3361
3362def scan_short_go(chars: String, +state: ScanState,
3363 found: Maybe<&2, OptFound>) -> Mode:
3364 match chars found:
3365 case SNil{} _:
3366 scan_settle(state)
3367 case SCon{Chr{104}, +tail} None{}:
3368 Closed{Stopped{scan_event(state, HelpHit{False{}})}}
3369 case SCon{Chr{86}, tail} None{}:
3370 scan_short_version(scope_version(scan_state_scope(state)), state)
3371 case SCon{head, tail} None{}:
3372 scan_settle(scan_error(state,
3373 E01{String.append("-", Char.show(head))}))
3374 case SCon{head, tail} Some{OptFound{arg, polarity}}:
3375 match arg:
3376 case ArgInfo{id, usage, short, long, kind, attrs}:
3377 match kind:
3378 case FlagKind{}:
3379 +next = scan_event(state, FlagHit{id, polarity})
3380 scan_short_go(tail, next,
3381 find_short_for_head(tail, scan_state_keys(next)))
3382 case CountKind{}:
3383 +next = scan_event(state, CountHit{id})
3384 scan_short_go(tail, next,
3385 find_short_for_head(tail, scan_state_keys(next)))
3386 case NegatableKind{}:
3387 +next = scan_event(state, FlagHit{id, polarity})
3388 scan_short_go(tail, next,
3389 find_short_for_head(tail, scan_state_keys(next)))
3390 case OptionKind{many}:
3391 scan_short_value(tail, state,
3392 ArgInfo{id, usage, short, long, OptionKind{many}, attrs})
3393 case BareOptionKind{}:
3394 scan_short_bare_value(tail, state,
3395 ArgInfo{id, usage, short, long, BareOptionKind{}, attrs})
3396 case PosKind{required, many}:
3397 scan_settle(state)
3398
3399
3400
3401def scan_short(+state: ScanState, token: String) -> Mode:
3402 match state:
3403 case ScanState{Scope{path, Spec{keys, args, slots, variadic, subs,
3404 has_sub, external, needs_sub, version, digit_short, inherit}},
3405 events, resting, positional_count, ext, issue}:
3406 +chars = String.drop(token, 1n)
3407 found = find_short_for_head(chars, keys)
3408 scan_short_go(chars, state, found)
3409
3410def scope_external(scope: Scope) -> Bool:
3411 match scope:
3412 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3413 external, needs_sub, version, digit_short, inherit}}: external
3414
3415def scope_has_sub(scope: Scope) -> Bool:
3416 match scope:
3417 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3418 external, needs_sub, version, digit_short, inherit}}: has_sub
3419
3420def sub_key_aliased(aliased: Bool, name: String, aliases: List<&2, String>,
3421 children: List<&2, Item>) -> Maybe<&2, SubKey>:
3422 match aliased:
3423 case True{}: Some{SubKey{name, aliases, children}}
3424 case False{}: None{}
3425
3426def sub_key_hit(same: Bool, name: String, +aliases: List<&2, String>,
3427 children: List<&2, Item>, query: String) -> Maybe<&2, SubKey>:
3428 match same:
3429 case True{}: Some{SubKey{name, aliases, children}}
3430 case False{}:
3431 sub_key_aliased(List.contains(~String, ~String.eq, aliases, query),
3432 name, aliases, children)
3433
3434def sub_key_found(key: SubKey, +query: String) -> Maybe<&2, SubKey>:
3435 match key:
3436 case SubKey{+name, aliases, children}:
3437 sub_key_hit(same_text(query, name), name, aliases, children, query)
3438
3439
3440def find_sub_key(subs: List<&2, SubKey>, +query: String,
3441 found: Maybe<&2, SubKey>) -> Maybe<&2, SubKey>:
3442 match subs found:
3443 case Nil{} hit: hit
3444 case key <> tail Some{hit}: Some{hit}
3445 case key <> tail None{}:
3446 find_sub_key(tail, query, sub_key_found(key, query))
3447
3448def scope_find_sub(scope: Scope, query: String) -> Maybe<&2, SubKey>:
3449 match scope:
3450 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3451 external, needs_sub, version, digit_short, inherit}}:
3452 find_sub_key(subs, query, None{})
3453
3454def scan_sub_unknown(allow_external: Bool, state: ScanState,
3455 token: String) -> Mode:
3456 match allow_external:
3457 case True{}: Capture{state, token}
3458 case False{}: Closed{Stopped{scan_error(state, E02{token})}}
3459
3460def scan_sub_found(found: Maybe<&2, SubKey>, +state: ScanState,
3461 token: String) -> Mode:
3462 match found:
3463 case Some{sub}: scan_settle(scan_descend(state, sub))
3464 case None{}:
3465 scan_sub_unknown(scope_external(scan_state_scope(state)),
3466 state, token)
3467
3468def unknown_fault(dash: Bool, token: String) -> ScanFault:
3469 match dash:
3470 case True{}: E01{token}
3471 case False{}: E02{token}
3472
3473def help_missing_case(help: Maybe<&2, Bool>, +token: String,
3474 state: ScanState) -> Tail:
3475 match help:
3476 case Some{long}: Stopped{scan_event(state, HelpHit{long})}
3477 case None{}:
3478 Stopped{scan_error(state,
3479 unknown_fault(String.starts_with(token, "-"), token))}
3480
3481def help_missing(+token: String, state: ScanState) -> Tail:
3482 help_missing_case(help_flag(token), token, state)
3483
3484def help_walk(found: Maybe<&2, SubKey>, state: ScanState,
3485 token: String) -> Tail:
3486 match found:
3487 case Some{sub}: Walk{scan_descend(state, sub)}
3488 case None{}: help_missing(token, state)
3489
3490def scan_non_option_sub_case(help: Bool, +token: String,
3491 +state: ScanState) -> Mode:
3492 match help:
3493 case True{}: Closed{Walk{state}}
3494 case False{}:
3495 scan_sub_found(scope_find_sub(scan_state_scope(state), token),
3496 state, token)
3497
3498def scan_non_option_sub(+token: String, state: ScanState) -> Mode:
3499 scan_non_option_sub_case(String.eq(token, "help"), token, state)
3500
3501def scan_non_option_case(has_sub: Bool, token: String,
3502 state: ScanState) -> Mode:
3503 match has_sub:
3504 case True{}: scan_non_option_sub(token, state)
3505 case False{}: scan_settle(scan_operand(state, token))
3506
3507def scan_non_option(token: String, +state: ScanState) -> Mode:
3508 scan_non_option_case(scope_has_sub(scan_state_scope(state)), token, state)
3509
3510
3511
3512
3513def hyphen_operand(hyphen: Bool, scope: Scope, token: String) -> Bool:
3514 match hyphen:
3515 case True{}: Bool.not(scope_any_option_known(scope, token))
3516 case False{}: False{}
3517
3518
3519def f32_operand(f32: Bool, scope: Scope, token: String) -> Bool:
3520 match f32:
3521 case True{}:
3522 Bool.and(is_negative_number(token), Bool.not(scope_digit_short(scope)))
3523 case False{}: False{}
3524
3525def slot_candidate(traits: SlotTraits, +scope: Scope, +token: String) ->
3526 Bool:
3527 match traits:
3528 case SlotTraits{rest, hyphen, f32}:
3529 Bool.or(hyphen_operand(Bool.or(rest, hyphen), scope, token),
3530 f32_operand(f32, scope, token))
3531
3532def scan_hyphen_candidate(state: ScanState, +token: String) -> Bool:
3533 match state:
3534 case ScanState{+scope, events, resting, positional_count,
3535 external, issue}:
3536 slot_candidate(scope_slot_traits(scope, positional_count), scope, token)
3537
3538def scan_dash_case(operand: Bool, token: String, state: ScanState) -> Mode:
3539 match operand:
3540 case True{}: scan_settle(scan_operand(state, token))
3541 case False{}: scan_short(state, token)
3542
3543def scan_dash_token(+token: String, +state: ScanState) -> Mode:
3544 scan_dash_case(scan_hyphen_candidate(state, token), token, state)
3545
3546
3547
3548
3549type TokenClass is Data:
3550 LongToken{}
3551 DashToken{}
3552 WordToken{}
3553
3554def token_class(token: String) -> TokenClass:
3555 match token:
3556 case SCon{Chr{45}, SCon{Chr{45}, rest}}: LongToken{}
3557 case SCon{Chr{45}, SNil{}}: WordToken{}
3558 case SCon{Chr{45}, rest}: DashToken{}
3559 case _: WordToken{}
3560
3561def scan_token_case(kind: TokenClass, token: String,
3562 state: ScanState) -> Mode:
3563 match kind:
3564 case LongToken{}: scan_long(token, state)
3565 case DashToken{}: scan_dash_token(token, state)
3566 case WordToken{}: scan_non_option(token, state)
3567
3568def scan_token(+token: String, state: ScanState) -> Mode:
3569 scan_token_case(token_class(token), token, state)
3570
3571def scan_tail(tail: Tail, +token: String) -> Tail:
3572 match tail:
3573 case Faulted{state}: scan_faulted(state, token)
3574 case Walk{+state}:
3575 help_walk(scope_find_sub(scan_state_scope(state), token),
3576 state, token)
3577 case Stopped{state}: Stopped{state}
3578
3579
3580def scan_open(dash: Bool, state: ScanState, token: String) -> Mode:
3581 match dash:
3582 case True{}: Operands{state, True{}}
3583 case False{}: scan_token(token, state)
3584
3585
3586def scan_go(tokens: List<&2, String>, mode: Mode) -> Mode:
3587 match tokens mode:
3588 case argv Capture{state, name}:
3589 Closed{Stopped{scan_external(state, name, argv)}}
3590 case Nil{} _: mode
3591 case +token <> rest Open{state}:
3592 scan_go(rest, scan_open(String.eq(token, "--"), state, token))
3593 case token <> rest Fetch{state, info}:
3594 scan_go(rest, scan_fetch(state, info, token))
3595 case token <> rest Operands{state, ended}:
3596 scan_go(rest, Operands{scan_operand(state, token), ended})
3597 case token <> rest Closed{tail}:
3598 scan_go(rest, Closed{scan_tail(tail, token)})
3599
3600def scan_tail_close(tail: Tail) -> ScanState:
3601 match tail:
3602 case Faulted{state}: state
3603 case Walk{state}: scan_event(state, HelpHit{True{}})
3604 case Stopped{state}: state
3605
3606def scan_close(mode: Mode) -> ScanState:
3607 match mode:
3608 case Open{state}: state
3609 case Fetch{state, info}: scan_error(state, E03{arg_display(info)})
3610 case Operands{state, ended}: state
3611 case Capture{state, name}: scan_external(state, name, [])
3612 case Closed{tail}: scan_tail_close(tail)
3613
3614def scan_trace_of(state: ScanState, ended: Bool) -> ScanTrace:
3615 match state:
3616 case ScanState{scope, events, resting, positional_count,
3617 external, issue}:
3618 match scope:
3619 case Scope{path, spec}:
3620 ScanTrace{path, List.reverse(&2, ScanEvent, events),
3621 ended, external, issue}
3622
3623def scan_trace(mode: Mode) -> ScanTrace:
3624 match mode:
3625 case Operands{state, ended}: scan_trace_of(state, ended)
3626 case other: scan_trace_of(scan_close(other), False{})
3627
3628def scan(c: Cmd, argv: List<&2, String>) -> ScanTrace:
3629 scan_trace(scan_go(argv, Open{scan_start(c)}))
3630
3631def find_early(events: List<&2, ScanEvent>) -> Maybe<&2, Early>:
3632 match events:
3633 case Nil{}: None{}
3634 case HelpHit{long} <> tail: Some{EarlyHelp{long}}
3635 case VersionHit{} <> tail: Some{EarlyVersion{}}
3636 case event <> tail: find_early(tail)
3637
3638def scope_needs_sub(scope: Scope) -> Bool:
3639 match scope:
3640 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3641 external, needs_sub, version, digit_short, inherit}}: needs_sub
3642
3643def bool_text(value: Bool) -> String:
3644 Bool.pick(String, value, "true", "false")
3645
3646def gather_value(gather: Gather, +value: String,
3647 many: Bool) -> Gather:
3648 match gather:
3649 case Gather{seen, values, +count}:
3650 Bool.pick(Gather, many,
3651 Gather{True{}, value <> values, count},
3652 Gather{True{}, [value], count})
3653
3654def gather_flag(gather: Gather, value: Bool) -> Gather:
3655 match gather:
3656 case Gather{seen, values, count}:
3657 Gather{True{}, [bool_text(value)], count}
3658
3659def gather_count(gather: Gather) -> Gather:
3660 match gather:
3661 case Gather{seen, values, count}:
3662 +next = Nat.add(count, 1n)
3663 Gather{True{}, [Nat.show(next)], next}
3664
3665def kind_many(kind: UsageKind) -> Bool:
3666 match kind:
3667 case OptionKind{many}: many
3668 case PosKind{required, many}: many
3669 case _: False{}
3670
3671
3672
3673def gather_value_if(hit: Bool, gather: Gather, value: String,
3674 kind: UsageKind) -> Gather:
3675 match hit:
3676 case True{}: gather_value(gather, value, kind_many(kind))
3677 case False{}: gather
3678
3679def gather_flag_if(hit: Bool, gather: Gather, value: Bool) -> Gather:
3680 match hit:
3681 case True{}: gather_flag(gather, value)
3682 case False{}: gather
3683
3684def gather_count_if(hit: Bool, gather: Gather) -> Gather:
3685 match hit:
3686 case True{}: gather_count(gather)
3687 case False{}: gather
3688
3689def gather_event(event: ScanEvent, id: String,
3690 kind: UsageKind, gather: Gather) -> Gather:
3691 match event:
3692 case ValueHit{key, value}:
3693 gather_value_if(same_text(key, id), gather, value, kind)
3694 case FlagHit{key, value}:
3695 gather_flag_if(same_text(key, id), gather, value)
3696 case CountHit{key}: gather_count_if(same_text(key, id), gather)
3697 case _: gather
3698
3699def gather_events(events: List<&2, ScanEvent>, +id: String,
3700 +kind: UsageKind, gather: Gather) -> Gather:
3701 match events:
3702 case Nil{}: gather
3703 case event <> tail:
3704 gather_events(tail, id, kind,
3705 gather_event(event, id, kind, gather))
3706
3707def gather_arg(events: List<&2, ScanEvent>, info: ArgInfo) -> Gather:
3708 match info:
3709 case ArgInfo{id, usage, short, long, kind, attrs}:
3710 gather_events(events, id, kind, Gather{False{}, [], 0n})
3711
3712
3713def find_envs(attrs: List<&2, Item>) -> List<&2, String>:
3714 match attrs:
3715 case Nil{}: Nil{}
3716 case AttrEnv{name} <> tail: name <> find_envs(tail)
3717 case head <> tail: find_envs(tail)
3718
3719def env_present(name: String, found: Maybe<&2, String>) ->
3720 Maybe<&2, EnvValue>:
3721 match found:
3722 case None{}: None{}
3723 case Some{+value}:
3724 Bool.pick(Maybe<&2, EnvValue>, String.is_empty(value),
3725 None{}, Some{EnvValue{name, value}})
3726
3727
3728def env_next(found: Maybe<&2, EnvValue>, +name: String,
3729 vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3730 match found:
3731 case Some{env}: Some{env}
3732 case None{}: env_present(name, var_value(vars, name))
3733
3734
3735def env_first(names: List<&2, String>, found: Maybe<&2, EnvValue>,
3736 +vars: List<&2, Var>) -> Maybe<&2, EnvValue>:
3737 match names:
3738 case Nil{}: found
3739 case name <> tail: env_first(tail, env_next(found, name, vars), vars)
3740
3741def host_env(h: Host, names: List<&2, String>) -> Maybe<&2, EnvValue>:
3742 match h:
3743 case Host{width, vars, out_tty, err_tty}:
3744 env_first(names, None{}, vars)
3745
3746def env_bool_true(value: String) -> Bool:
3747 List.contains(~String, ~String.eq,
3748 ["true", "yes", "on", "1"], String.to_lower(value))
3749
3750def env_values(kind: UsageKind, attrs: List<&2, Item>,
3751 value: String) -> List<&2, String>:
3752 match kind:
3753 case FlagKind{}: [bool_text(env_bool_true(value))]
3754 case NegatableKind{}: [bool_text(env_bool_true(value))]
3755 case CountKind{}: [value]
3756 case _: apply_delim(find_delim(attrs), [value])
3757
3758def resolve_env_valid(found: Maybe<&2, Bad>, info: ArgInfo,
3759 name: String, value: String) -> FieldBuild:
3760 match found:
3761 case Some{Bad{bad, why}}:
3762 BuildFault{E11{arg_display(info), bad, name, why}}
3763 case None{}:
3764 match info:
3765 case ArgInfo{id, usage, short, long, kind, attrs}:
3766 Built{[Field{id, env_values(kind, attrs, value), Env{}}]}
3767
3768def resolve_env(+info: ArgInfo, env: EnvValue) -> FieldBuild:
3769 match env:
3770 case EnvValue{name, +value}:
3771 resolve_env_valid(value_issue(info_kind(info), info_attrs(info), value),
3772 info, name, value)
3773
3774def resolve_default(found: Maybe<&2, String>,
3775 info: ArgInfo) -> FieldBuild:
3776 match found:
3777 case None{}: Built{[]}
3778 case Some{value}:
3779 match info:
3780 case ArgInfo{id, usage, short, long, kind, attrs}:
3781 Built{[Field{id, env_values(kind, attrs, value), Default{}}]}
3782
3783def info_default(info: ArgInfo) -> Maybe<&2, String>:
3784 match info:
3785 case ArgInfo{id, usage, short, long, kind, attrs}:
3786 find_default(attrs)
3787
3788def resolve_env_default(found: Maybe<&2, EnvValue>,
3789 +info: ArgInfo) -> FieldBuild:
3790 match found:
3791 case Some{env}: resolve_env(info, env)
3792 case None{}: resolve_default(info_default(info), info)
3793
3794def info_envs(info: ArgInfo) -> List<&2, String>:
3795 match info:
3796 case ArgInfo{id, usage, short, long, kind, attrs}:
3797 find_envs(attrs)
3798
3799def resolve_argv_gather(gather: Gather, +info: ArgInfo,
3800 h: Host) -> FieldBuild:
3801 match gather:
3802 case Gather{True{}, values, count}:
3803 match info:
3804 case ArgInfo{id, usage, short, long, kind, attrs}:
3805 Built{[Field{id, apply_delim(find_delim(attrs),
3806 List.reverse(&2, String, values)), Argv{}}]}
3807 case Gather{False{}, values, count}:
3808 resolve_env_default(host_env(h, info_envs(info)), info)
3809
3810def resolve_option(+info: ArgInfo,
3811 events: List<&2, ScanEvent>, h: Host) -> FieldBuild:
3812 resolve_argv_gather(gather_arg(events, info), info, h)
3813
3814def scope_arg_items(scope: Scope) -> List<&2, SpecItem>:
3815 match scope:
3816 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3817 external, needs_sub, version, digit_short, inherit}}: args
3818
3819def combine_built(fields: List<&2, Field>,
3820 next: FieldBuild) -> FieldBuild:
3821 match next:
3822 case Built{more}:
3823 Built{List.append(&2, Field, fields, more)}
3824 case BuildFault{fault}: BuildFault{fault}
3825
3826def combine_build(first: FieldBuild,
3827 second: FieldBuild) -> FieldBuild:
3828 match first:
3829 case Built{fields}: combine_built(fields, second)
3830 case BuildFault{fault}: BuildFault{fault}
3831
3832def resolve_arg(pos: Bool, +info: ArgInfo, events: List<&2, ScanEvent>,
3833 h: Host, later: FieldBuild) -> FieldBuild:
3834 match pos:
3835 case True{}: later
3836 case False{}: combine_build(resolve_option(info, events, h), later)
3837
3838def resolve_options(items: List<&2, SpecItem>,
3839 +events: List<&2, ScanEvent>, +h: Host) -> FieldBuild:
3840 match items:
3841 case Nil{}: Built{[]}
3842 case SpecArg{+info} <> tail:
3843 resolve_arg(kind_pos(info_kind(info)), info, events, h,
3844 resolve_options(tail, events, h))
3845 case head <> tail: resolve_options(tail, events, h)
3846
3847def scan_operands(events: List<&2, ScanEvent>) ->
3848 List<&2, String>:
3849 match events:
3850 case Nil{}: Nil{}
3851 case Operand{token} <> tail:
3852 token <> scan_operands(tail)
3853 case event <> tail: scan_operands(tail)
3854
3855def position_take_count(kind: UsageKind,
3856 after: List<&2, ArgInfo>, +tokens: List<&2, String>) -> Nat:
3857 match kind:
3858 case PosKind{required, True{}}:
3859 Nat.sub(List.length(&2, String, tokens),
3860 List.length(&2, ArgInfo, after))
3861 case _:
3862 Bool.pick(Nat, List.is_empty(&2, String, tokens), 0n, 1n)
3863
3864def pos_valid_result(found: Maybe<&2, Bad>,
3865 info: ArgInfo, values: List<&2, String>) -> FieldBuild:
3866 match found:
3867 case Some{Bad{bad, why}}:
3868 BuildFault{E04{arg_display(info), bad, why}}
3869 case None{}:
3870 match info:
3871 case ArgInfo{id, usage, short, long, kind, attrs}:
3872 Built{[Field{id, values, Argv{}}]}
3873
3874def resolve_pos(+info: ArgInfo,
3875 +values: List<&2, String>, h: Host) -> FieldBuild:
3876 match values:
3877 case Nil{}:
3878 resolve_env_default(host_env(h, info_envs(info)), info)
3879 case head <> tail:
3880 +attrs = info_attrs(info)
3881 +split = apply_delim(find_delim(attrs), values)
3882 pos_valid_result(first_bad(split, attrs), info, split)
3883
3884def resolve_positions(slots: List<&2, ArgInfo>,
3885 +tokens: List<&2, String>, +h: Host) -> FieldBuild:
3886 match slots:
3887 case Nil{}: Built{[]}
3888 case +info <> +tail:
3889 +amount = position_take_count(info_kind(info), tail, tokens)
3890 combine_build(
3891 resolve_pos(info, List.take(&2, String, tokens, amount), h),
3892 resolve_positions(tail, List.drop(&2, String, tokens, amount), h))
3893
3894def scope_positions(scope: Scope) -> List<&2, ArgInfo>:
3895 match scope:
3896 case Scope{path, Spec{keys, args, slots, variadic, subs, has_sub,
3897 external, needs_sub, version, digit_short, inherit}}: slots
3898
3899def resolve_scope(+scope: Scope, +events: List<&2, ScanEvent>,
3900 +h: Host) -> FieldBuild:
3901 combine_build(
3902 resolve_options(scope_arg_items(scope), events, h),
3903 resolve_positions(scope_positions(scope),
3904 scan_operands(events), h))
3905
3906def source_given(source: Src) -> Bool:
3907 match source:
3908 case Argv{}: True{}
3909 case Env{}: True{}
3910 case Default{}: False{}
3911 case Absent{}: False{}
3912
3913def field_source(fields: List<&2, Field>, +id: String) -> Src:
3914 match fields:
3915 case Nil{}: Absent{}
3916 case Field{key, values, source} <> tail:
3917 Bool.pick(Src, String.eq(key, id), source,
3918 field_source(tail, id))
3919
3920def field_given(fields: List<&2, Field>, id: String) -> Bool:
3921 source_given(field_source(fields, id))
3922
3923def source_present(source: Src) -> Bool:
3924 match source:
3925 case Absent{}: False{}
3926 case _: True{}
3927
3928def field_present(fields: List<&2, Field>, id: String) -> Bool:
3929 source_present(field_source(fields, id))
3930
3931def group_usages(items: List<&2, Item>, +name: String) ->
3932 List<&2, String>:
3933 match items:
3934 case Nil{}: Nil{}
3935 case Arg{+usage, help, +attrs} <> tail:
3936 +later = group_usages(tail, name)
3937 Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3938 arg_display(arg_info(usage, attrs)) <> later, later)
3939 case head <> tail: group_usages(tail, name)
3940
3941def spec_group_usages(items: List<&2, SpecItem>, +name: String) ->
3942 List<&2, String>:
3943 match items:
3944 case Nil{}: Nil{}
3945 case SpecArg{+info} <> tail:
3946 +later = spec_group_usages(tail, name)
3947 attrs = info_attrs(info)
3948 Bool.pick(List<&2, String>, arg_in_group(attrs, name),
3949 arg_display(info) <> later, later)
3950 case head <> tail: spec_group_usages(tail, name)
3951
3952def spec_group_display(items: List<&2, SpecItem>, name: String) -> String:
3953 String.concat(["<", String.join(spec_group_usages(items, name), "|"), ">"])
3954
3955def group_given(items: List<&2, SpecItem>,
3956 +fields: List<&2, Field>, +name: String) -> Bool:
3957 match items:
3958 case Nil{}: False{}
3959 case SpecArg{ArgInfo{id, usage, short, long, kind, attrs}} <> tail:
3960 Bool.or(Bool.and(arg_in_group(attrs, name), field_given(fields, id)),
3961 group_given(tail, fields, name))
3962 case head <> tail: group_given(tail, fields, name)
3963
3964def arg_required_missing(info: ArgInfo,
3965 +fields: List<&2, Field>) -> Bool:
3966 match info:
3967 case ArgInfo{+id, usage, short, long, kind, attrs}:
3968 Bool.or(Bool.and(has_required(attrs),
3969 Bool.not(field_given(fields, id))),
3970 Bool.and(kind_required_pos(kind),
3971 Bool.not(field_present(fields, id))))
3972
3973def group_display(items: List<&2, Item>, name: String) -> String:
3974 String.concat(["<", String.join(group_usages(items, name), "|"), ">"])
3975
3976def missing_arg(+info: ArgInfo,
3977 fields: List<&2, Field>) -> List<&2, String>:
3978 Bool.pick(List<&2, String>,
3979 arg_required_missing(info, fields),
3980 [arg_display(info)], [])
3981
3982def missing_group(+items: List<&2, SpecItem>,
3983 fields: List<&2, Field>, +name: String) -> List<&2, String>:
3984 Bool.pick(List<&2, String>,
3985 group_given(items, fields, name), [],
3986 [spec_group_display(items, name)])
3987
3988
3989def missing_once(listed: Bool, items: List<&2, SpecItem>,
3990 fields: List<&2, Field>, name: String) -> List<&2, String>:
3991 match listed:
3992 case True{}: []
3993 case False{}: missing_group(items, fields, name)
3994
3995
3996
3997def missing_items(items: List<&2, SpecItem>,
3998 +all_items: List<&2, SpecItem>,
3999 +fields: List<&2, Field>, +done: List<&2, String>) ->
4000 List<&2, String>:
4001 match items:
4002 case Nil{}: Nil{}
4003 case SpecArg{info} <> tail:
4004 List.append(&2, String,
4005 missing_arg(info, fields),
4006 missing_items(tail, all_items, fields, done))
4007 case SpecOther{NeedOne{+name}} <> tail:
4008 List.append(&2, String,
4009 missing_once(List.contains(~String, ~String.eq, done, name),
4010 all_items, fields, name),
4011 missing_items(tail, all_items, fields, name <> done))
4012 case head <> tail:
4013 missing_items(tail, all_items, fields, done)
4014
4015def scope_missing(scope: Scope,
4016 fields: List<&2, Field>) -> List<&2, String>:
4017 +items = scope_arg_items(scope)
4018 missing_items(items, items, fields, [])
4019
4020def append_new(seen: Bool, ids: List<&2, String>, id: String) ->
4021 List<&2, String>:
4022 match seen:
4023 case True{}: ids
4024 case False{}: List.append(&2, String, ids, [id])
4025
4026
4027
4028def has_text(names: List<&2, String>, +query: String, found: Bool) -> Bool:
4029 match names found:
4030 case _ True{}: True{}
4031 case Nil{} False{}: False{}
4032 case name <> tail False{}: has_text(tail, query, same_text(name, query))
4033
4034def append_unique(+ids: List<&2, String>, +id: String) ->
4035 List<&2, String>:
4036 append_new(has_text(ids, id, False{}), ids, id)
4037
4038def append_event_id(found: Maybe<&2, String>,
4039 ids: List<&2, String>) -> List<&2, String>:
4040 match found:
4041 case None{}: ids
4042 case Some{id}: append_unique(ids, id)
4043
4044def argv_order(events: List<&2, ScanEvent>,
4045 ids: List<&2, String>) -> List<&2, String>:
4046 match events:
4047 case Nil{}: ids
4048 case event <> tail:
4049 argv_order(tail, append_event_id(event_id(event), ids))
4050
4051def source_env(source: Src) -> Bool:
4052 match source:
4053 case Env{}: True{}
4054 case _: False{}
4055
4056def env_order(fields: List<&2, Field>,
4057 +ids: List<&2, String>) -> List<&2, String>:
4058 match fields:
4059 case Nil{}: ids
4060 case Field{id, values, source} <> tail:
4061 env_order(tail, Bool.pick(List<&2, String>,
4062 source_env(source), append_unique(ids, id), ids))
4063
4064def present_order(events: List<&2, ScanEvent>,
4065 fields: List<&2, Field>) -> List<&2, String>:
4066 env_order(fields, argv_order(events, []))
4067
4068def arg_info_found(item: Item, id: String) -> Maybe<&2, ArgInfo>:
4069 match item:
4070 case Arg{usage, help, attrs}:
4071 +info = arg_info(usage, attrs)
4072 own = info_id(info)
4073 Bool.pick(Maybe<&2, ArgInfo>, same_text(own, id), Some{info}, None{})
4074 case _: None{}
4075
4076
4077def find_arg_go(items: List<&2, Item>, +id: String,
4078 found: Maybe<&2, ArgInfo>) -> Maybe<&2, ArgInfo>:
4079 match items found:
4080 case _ Some{hit}: Some{hit}
4081 case Nil{} None{}: None{}
4082 case item <> tail None{}: find_arg_go(tail, id, arg_info_found(item, id))
4083
4084def find_arg_info(items: List<&2, Item>, +id: String) ->
4085 Maybe<&2, ArgInfo>:
4086 find_arg_go(items, id, None{})
4087
4088def exclusive_groups(items: List<&2, SpecItem>) -> List<&2, String>:
4089 match items:
4090 case Nil{}: Nil{}
4091 case SpecOther{Exclusive{name}} <> tail:
4092 name <> exclusive_groups(tail)
4093 case head <> tail: exclusive_groups(tail)
4094
4095def common_exclusive(groups: List<&2, String>,
4096 +left: List<&2, String>, +right: List<&2, String>) -> Bool:
4097 match groups:
4098 case Nil{}: False{}
4099 case +name <> tail:
4100 Bool.or(Bool.and(
4101 List.contains(~String, ~String.eq, left, name),
4102 List.contains(~String, ~String.eq, right, name)),
4103 common_exclusive(tail, left, right))
4104
4105def pair_conflicts(+later: ArgInfo, +earlier: ArgInfo,
4106 groups: List<&2, String>) -> Bool:
4107 +later_attrs = info_attrs(later)
4108 +earlier_attrs = info_attrs(earlier)
4109 Bool.or(List.contains(~String, ~String.eq,
4110 find_conflicts(later_attrs), info_id(earlier)),
4111 Bool.or(List.contains(~String, ~String.eq,
4112 find_conflicts(earlier_attrs), info_id(later)),
4113 common_exclusive(groups,
4114 find_group_attrs(later_attrs),
4115 find_group_attrs(earlier_attrs))))
4116
4117def pair_fault(+later: ArgInfo, earlier: Maybe<&2, ArgInfo>,
4118 groups: List<&2, String>) -> Maybe<&2, ScanFault>:
4119 match earlier:
4120 case None{}: None{}
4121 case Some{+prior}:
4122 Bool.pick(Maybe<&2, ScanFault>,
4123 pair_conflicts(later, prior, groups),
4124 Some{E07{arg_display(later), arg_display(prior)}}, None{})
4125
4126def earlier_conflict(earlier_ids: List<&2, String>,
4127 +later: ArgInfo, +items: List<&2, SpecItem>,
4128 +groups: List<&2, String>,
4129 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4130 match earlier_ids found:
4131 case Nil{} Some{fault}: Some{fault}
4132 case Nil{} None{}: None{}
4133 case id <> tail Some{fault}: Some{fault}
4134 case id <> tail None{}:
4135 earlier_conflict(tail, later, items, groups,
4136 pair_fault(later, find_spec_info(items, id, None{}), groups))
4137
4138def conflict_for_id(info: Maybe<&2, ArgInfo>,
4139 seen: List<&2, String>, items: List<&2, SpecItem>,
4140 groups: List<&2, String>) -> Maybe<&2, ScanFault>:
4141 match info:
4142 case None{}: None{}
4143 case Some{later}:
4144 earlier_conflict(seen, later, items, groups, None{})
4145
4146def conflict_go(ids: List<&2, String>,
4147 +seen: List<&2, String>, +items: List<&2, SpecItem>,
4148 +groups: List<&2, String>,
4149 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4150 match ids found:
4151 case Nil{} Some{fault}: Some{fault}
4152 case Nil{} None{}: None{}
4153 case id <> tail Some{fault}: Some{fault}
4154 case +id <> tail None{}:
4155 conflict_go(tail, List.append(&2, String, seen, [id]),
4156 items, groups,
4157 conflict_for_id(find_spec_info(items, id, None{}),
4158 seen, items, groups))
4159
4160def require_target_fault(target: Maybe<&2, ArgInfo>,
4161 +requester: ArgInfo, fields: List<&2, Field>) ->
4162 Maybe<&2, ScanFault>:
4163 match target:
4164 case None{}: None{}
4165 case Some{+needed}:
4166 Bool.pick(Maybe<&2, ScanFault>,
4167 Bool.not(field_given(fields, info_id(needed))),
4168 Some{E08{arg_display(requester), arg_display(needed)}},
4169 None{})
4170
4171def require_targets(targets: List<&2, String>,
4172 +requester: ArgInfo, +items: List<&2, SpecItem>,
4173 +fields: List<&2, Field>,
4174 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4175 match targets found:
4176 case Nil{} Some{fault}: Some{fault}
4177 case Nil{} None{}: None{}
4178 case name <> tail Some{fault}: Some{fault}
4179 case name <> tail None{}:
4180 require_targets(tail, requester, items, fields,
4181 require_target_fault(find_spec_info(items, name, None{}),
4182 requester, fields))
4183
4184def require_for_id(info: Maybe<&2, ArgInfo>,
4185 items: List<&2, SpecItem>,
4186 fields: List<&2, Field>) -> Maybe<&2, ScanFault>:
4187 match info:
4188 case None{}: None{}
4189 case Some{+requester}:
4190 require_targets(find_requires(info_attrs(requester)),
4191 requester, items, fields, None{})
4192
4193def require_go(ids: List<&2, String>,
4194 +items: List<&2, SpecItem>, +fields: List<&2, Field>,
4195 found: Maybe<&2, ScanFault>) -> Maybe<&2, ScanFault>:
4196 match ids found:
4197 case Nil{} Some{fault}: Some{fault}
4198 case Nil{} None{}: None{}
4199 case id <> tail Some{fault}: Some{fault}
4200 case id <> tail None{}:
4201 require_go(tail, items, fields,
4202 require_for_id(find_spec_info(items, id, None{}), items, fields))
4203
4204def parse_requires(fault: Maybe<&2, ScanFault>,
4205 path: List<&2, String>, fields: List<&2, Field>,
4206 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4207 match fault:
4208 case Some{problem}: Rejected{path, problem}
4209 case None{}: Accepted{path, fields, external}
4210
4211def parse_conflicts(fault: Maybe<&2, ScanFault>,
4212 +scope: Scope, +fields: List<&2, Field>,
4213 events: List<&2, ScanEvent>,
4214 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4215 match fault:
4216 case Some{problem}: Rejected{scope_path(scope), problem}
4217 case None{}:
4218 parse_requires(require_go(
4219 present_order(events, fields),
4220 scope_arg_items(scope), fields, None{}),
4221 scope_path(scope), fields, external)
4222
4223def parse_missing(missing: List<&2, String>,
4224 +scope: Scope, +fields: List<&2, Field>,
4225 +events: List<&2, ScanEvent>,
4226 empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4227 match missing:
4228 case Nil{}:
4229 +items = scope_arg_items(scope)
4230 parse_conflicts(conflict_go(present_order(events, fields),
4231 [], items, exclusive_groups(items), None{}),
4232 scope, fields, events, external)
4233 case head <> tail:
4234 Bool.pick(ParseResult, empty, SummaryRequired{},
4235 Rejected{scope_path(scope), E06{head <> tail}})
4236
4237def parse_built(build: FieldBuild, +scope: Scope,
4238 events: List<&2, ScanEvent>, empty: Bool,
4239 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4240 match build:
4241 case BuildFault{fault}: Rejected{scope_path(scope), fault}
4242 case Built{+fields}:
4243 parse_missing(scope_missing(scope, fields),
4244 scope, fields, events, empty, external)
4245
4246def parse_no_issue(needs_sub: Bool, +scope: Scope,
4247 +events: List<&2, ScanEvent>, +h: Host,
4248 +empty: Bool, external: Maybe<&2, ExternalArgs>) -> ParseResult:
4249 Bool.pick(ParseResult, needs_sub,
4250 Bool.pick(ParseResult, empty, SummaryRequired{},
4251 Rejected{scope_path(scope), E10{}}),
4252 parse_built(resolve_scope(scope, events, h),
4253 scope, events, empty, external))
4254
4255def parse_issue(issue: Maybe<&2, ScanFault>,
4256 +scope: Scope, events: List<&2, ScanEvent>,
4257 h: Host, empty: Bool,
4258 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4259 match issue:
4260 case Some{fault}: Rejected{scope_path(scope), fault}
4261 case None{}:
4262 parse_no_issue(scope_needs_sub(scope), scope,
4263 events, h, empty, external)
4264
4265def parse_early(found: Maybe<&2, Early>,
4266 +scope: Scope, events: List<&2, ScanEvent>, h: Host,
4267 empty: Bool, issue: Maybe<&2, ScanFault>,
4268 external: Maybe<&2, ExternalArgs>) -> ParseResult:
4269 match found:
4270 case Some{EarlyHelp{long}}:
4271 HelpRequested{scope_path(scope), long}
4272 case Some{EarlyVersion{}}:
4273 VersionRequested{}
4274 case None{}:
4275 parse_issue(issue, scope, events, h, empty, external)
4276
4277def parse_from_state(state: ScanState, h: Host,
4278 empty: Bool) -> ParseResult:
4279 match state:
4280 case ScanState{scope, events, resting, positional_count,
4281 external, issue}:
4282 +ordered: List<&2, ScanEvent> =
4283 List.reverse(&2, ScanEvent, events)
4284 parse_early(find_early(ordered), scope, ordered, h,
4285 empty, issue, external)
4286
4287def parse(c: Cmd, h: Host, +argv: List<&2, String>) -> ParseResult:
4288 parse_from_state(scan_close(scan_go(argv, Open{scan_start(c)})),
4289 h, List.is_empty(&2, String, argv))
4290
4291
4292
4293type Fault is Data:
4294 Bug{msg: String}
4295 Usage{msg: String}
4296
4297def Dec(-A: Type) -> Type:
4298 Result<&2, &1, Fault, A>
4299
4300def Dec.pure(-A: Type, x: A) -> Dec(A):
4301 Done{x}
4302
4303def Dec.bind(-A: Type, -B: Type, r: Dec(A), f: A -> Dec(B)) -> Dec(B):
4304 match r:
4305 case Fail{e}: Fail{e}
4306 case Done{x}: f(x)
4307
4308type Matches is Data:
4309 Matches{cmd: Cmd, path: List<&2, String>, fields: List<&2, Field>,
4310 external: Maybe<&2, ExternalArgs>}
4311
4312def matches(c: Cmd, r: ParseResult) -> Maybe<&2, Matches>:
4313 match r:
4314 case Accepted{path, fields, external}:
4315 Some{Matches{c, path, fields, external}}
4316 case _: None{}
4317
4318def path(m: Matches) -> List<&2, String>:
4319 match m:
4320 case Matches{cmd, path, fields, external}: path
4321
4322def external_args(m: Matches) -> Maybe<(String & List<&2, String>)>:
4323 match m:
4324 case Matches{cmd, path, fields, None{}}: None{}
4325 case Matches{cmd, path, fields, Some{ExternalArgs{name, args}}}:
4326 Some{(name, args)}
4327
4328def fail(-A: Type, msg: String) -> Dec(A):
4329 Fail{Usage{msg}}
4330
4331def bug(-A: Type, msg: String) -> Dec(A):
4332 Fail{Bug{msg}}
4333
4334def keep(+m: Matches) -> Dec(Matches):
4335 Done{m}
4336
4337
4338def path_text(name: String, path: List<&2, String>) -> String:
4339 String.join(name <> path, " ")
4340
4341def sub_items(found: Maybe<&2, SubFound>) -> List<&2, Item>:
4342 match found:
4343 case None{}: Nil{}
4344 case Some{SubFound{name, items}}: items
4345
4346
4347def scope_args(path: List<&2, String>, +items: List<&2, Item>,
4348 globals: List<&2, Item>) -> List<&2, Item>:
4349 match path:
4350 case Nil{}: List.append(&2, Item, globals, items)
4351 case name <> tail:
4352 scope_args(tail, sub_items(find_sub(items, name)),
4353 List.append(&2, Item, global_items(items), globals))
4354
4355type Form is Data:
4356 ReadOne{}
4357 ReadOpt{}
4358 ReadMany{}
4359
4360
4361type Want is Data:
4362 WFlag{}
4363 WCount{}
4364 WValue{stem: String, form: Form}
4365 WAny{}
4366 WChoose{opt: Bool, keys: List<&2, String>}
4367 WProgram{opt: Bool}
4368
4369def kind_flag(kind: UsageKind) -> Bool:
4370 match kind:
4371 case FlagKind{}: True{}
4372 case NegatableKind{}: True{}
4373 case _: False{}
4374
4375def kind_count(kind: UsageKind) -> Bool:
4376 match kind:
4377 case CountKind{}: True{}
4378 case _: False{}
4379
4380
4381def type_stem(found: Maybe<&2, ValueType>) -> String:
4382 match found:
4383 case Some{TNat{}}: "nat"
4384 case Some{TU32{}}: "u32"
4385 case Some{TF32{}}: "f32"
4386 case Some{TBool{}}: "bool"
4387 case _: "str"
4388
4389def stem_noun(+stem: String) -> String:
4390 Bool.pick(String, String.eq(stem, "str"), "string", stem)
4391
4392def stem_article(+stem: String) -> String:
4393 Bool.pick(String, String.eq(stem, "f32"), "an ", "a ")
4394
4395def always_present(kind: UsageKind, +attrs: List<&2, Item>) -> Bool:
4396 Bool.or(has_required(attrs), Bool.or(
4397 Maybe.is_some(&2, String, find_default(attrs)), kind_required_pos(kind)))
4398
4399def may_absent(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4400 Bool.and(Bool.not(kind_many(kind)),
4401 Bool.not(always_present(kind, attrs)))
4402
4403def kind_text(kind: UsageKind, +stem: String) -> String:
4404 match kind:
4405 case FlagKind{}: "a flag"
4406 case NegatableKind{}: "a flag"
4407 case CountKind{}: "a count"
4408 case _:
4409 Bool.pick(String, kind_many(kind),
4410 String.concat(["a list of ", stem_noun(stem), " values"]),
4411 String.concat([stem_article(stem), stem_noun(stem), " value"]))
4412
4413def fit_name(+kind: UsageKind, attrs: List<&2, Item>,
4414 +stem: String) -> String:
4415 match kind:
4416 case FlagKind{}: "flag"
4417 case NegatableKind{}: "flag"
4418 case CountKind{}: "count"
4419 case _:
4420 Bool.pick(String, kind_many(kind), String.append(stem, "s"),
4421 Bool.pick(String, always_present(kind, attrs), stem,
4422 String.append(stem, "_opt")))
4423
4424def kind_verdict(fits: Bool, id: String, +kind: UsageKind,
4425 +attrs: List<&2, Item>) -> Maybe<&2, String>:
4426 match fits:
4427 case True{}: None{}
4428 case False{}:
4429 +stem = type_stem(find_as(attrs))
4430 Some{String.concat(["'", id, "' is ", kind_text(kind, stem),
4431 "; read it with Cli.", fit_name(kind, attrs, stem)])}
4432
4433def absent_verdict(absent: Bool, id: String,
4434 reader: String) -> Maybe<&2, String>:
4435 match absent:
4436 case True{}:
4437 Some{String.concat(["'", id, "' may be absent; read it with Cli.",
4438 reader, "_opt"])}
4439 case False{}: None{}
4440
4441def all_in(xs: List<&2, String>, +ys: List<&2, String>) -> Bool:
4442 match xs:
4443 case Nil{}: True{}
4444 case head <> tail:
4445 Bool.and(List.contains(~String, ~String.eq, ys, head),
4446 all_in(tail, ys))
4447
4448def keys_issue(same: Bool, id: String) -> Maybe<&2, String>:
4449 match same:
4450 case True{}: None{}
4451 case False{}:
4452 Some{String.concat(["choose table for '", id,
4453 "' does not match its values"])}
4454
4455def keys_verdict(found: Maybe<&2, List<&2, String>>,
4456 +keys: List<&2, String>, id: String) -> Maybe<&2, String>:
4457 match found:
4458 case None{}: None{}
4459 case Some{+values}:
4460 keys_issue(Bool.and(Bool.not(List.is_empty(&2, String, keys)),
4461 Bool.and(all_in(keys, values), all_in(values, keys))), id)
4462
4463def form_fits(form: Form, many: Bool) -> Bool:
4464 match form:
4465 case ReadMany{}: many
4466 case _: Bool.not(many)
4467
4468def form_one(form: Form) -> Bool:
4469 match form:
4470 case ReadOne{}: True{}
4471 case _: False{}
4472
4473def value_fits(stem: String, form: Form, +kind: UsageKind,
4474 attrs: List<&2, Item>) -> Bool:
4475 Bool.and(kind_valued(kind), Bool.and(
4476 String.eq(stem, type_stem(find_as(attrs))),
4477 form_fits(form, kind_many(kind))))
4478
4479def choose_fits(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4480 Bool.and(kind_valued(kind), Bool.and(Bool.not(kind_many(kind)),
4481 Maybe.is_some(&2, List<&2, String>, find_one_of(attrs))))
4482
4483
4484def program_fits(+kind: UsageKind, attrs: List<&2, Item>) -> Bool:
4485 Bool.and(kind_pos(kind), value_fits("str", ReadMany{}, kind, attrs))
4486
4487def rest_verdict(rest: Bool, id: String) -> Maybe<&2, String>:
4488 match rest:
4489 case True{}: None{}
4490 case False{}:
4491 Some{String.concat(["'", id,
4492 "' has no Cli.rest(); give it one or read it with Cli.strs"])}
4493
4494
4495
4496def want_verdict(want: Want, +id: String, +kind: UsageKind,
4497 +attrs: List<&2, Item>) -> Maybe<&2, String>:
4498 match want:
4499 case WFlag{}:
4500 kind_verdict(kind_flag(kind), id, kind, attrs)
4501 case WCount{}:
4502 kind_verdict(kind_count(kind), id, kind, attrs)
4503 case WValue{+stem, +form}:
4504 Maybe.or(&2, String,
4505 kind_verdict(value_fits(stem, form, kind, attrs), id, kind, attrs),
4506 absent_verdict(Bool.and(form_one(form), may_absent(kind, attrs)),
4507 id, stem))
4508 case WAny{}:
4509 None{}
4510 case WChoose{opt, keys}:
4511 Maybe.or(&2, String,
4512 kind_verdict(choose_fits(kind, attrs), id, kind, attrs),
4513 Maybe.or(&2, String,
4514 absent_verdict(Bool.and(Bool.not(opt), may_absent(kind, attrs)),
4515 id, "choose"),
4516 keys_verdict(find_one_of(attrs), keys, id)))
4517 case WProgram{opt}:
4518 Maybe.or(&2, String,
4519 kind_verdict(program_fits(kind, attrs), id, kind, attrs),
4520 Maybe.or(&2, String,
4521 absent_verdict(Bool.and(Bool.not(opt),
4522 Bool.not(always_present(kind, attrs))), id, "program"),
4523 rest_verdict(has_rest(attrs), id)))
4524
4525def arg_verdict(found: Maybe<&2, ArgInfo>, id: String, name: String,
4526 path: List<&2, String>, want: Want) -> Maybe<&2, String>:
4527 match found:
4528 case None{}:
4529 Some{String.concat(["'", id, "' is not an argument of '",
4530 path_text(name, path), "'"])}
4531 case Some{ArgInfo{key, usage, short, long, kind, attrs}}:
4532 want_verdict(want, id, kind, attrs)
4533
4534
4535def verdict(cmd: Cmd, +path: List<&2, String>, +id: String,
4536 want: Want) -> Maybe<&2, String>:
4537 match cmd:
4538 case Cmd{name, about, items}:
4539 arg_verdict(find_arg_info(scope_args(path, items, []), id), id, name,
4540 path, want)
4541
4542def field_found(f: Field, id: String) -> Maybe<&2, Field>:
4543 match f:
4544 case Field{+key, values, source}:
4545 Bool.pick(Maybe<&2, Field>, same_text(key, id),
4546 Some{Field{key, values, source}}, None{})
4547
4548
4549def field_seek(fields: List<&2, Field>, +id: String,
4550 found: Maybe<&2, Field>) -> Field:
4551 match fields found:
4552 case _ Some{f}: f
4553 case Nil{} None{}: Field{id, [], Absent{}}
4554 case f <> tail None{}: field_seek(tail, id, field_found(f, id))
4555
4556def field_find(fields: List<&2, Field>, +id: String) -> Field:
4557 field_seek(fields, id, None{})
4558
4559
4560def read_check(~T: Data, ~get: Field -> T, verdict: Maybe<&2, String>,
4561 fields: List<&2, Field>, id: String) -> Dec(T):
4562 match verdict:
4563 case None{}: Done{get(field_find(fields, id))}
4564 case Some{msg}: Fail{Bug{msg}}
4565
4566def read_with(~T: Data, ~get: Field -> T, want: Want, m: Matches,
4567 +id: String) -> Dec(T):
4568 match m:
4569 case Matches{cmd, path, fields, external}:
4570 read_check(~T, ~get, verdict(cmd, path, id, want), fields, id)
4571
4572def get_one(~T: Data, ~conv: String -> T, f: Field) -> T:
4573 match f:
4574 case Field{id, head <> tail, source}: conv(head)
4575 case _: conv("")
4576
4577def get_opt(~T: Data, ~conv: String -> T, f: Field) -> Maybe<&2, T>:
4578 match f:
4579 case Field{id, head <> tail, source}: Some{conv(head)}
4580 case _: None{}
4581
4582def convert_all(~T: Data, ~conv: String -> T, values: List<&2, String>) ->
4583 List<&2, T>:
4584 match values:
4585 case Nil{}: Nil{}
4586 case head <> tail: conv(head) <> convert_all(~T, ~conv, tail)
4587
4588def get_many(~T: Data, ~conv: String -> T, f: Field) -> List<&2, T>:
4589 match f:
4590 case Field{id, values, source}: convert_all(~T, ~conv, values)
4591
4592def true_of(s: String) -> Bool:
4593 String.eq(s, "true")
4594
4595def nat_of(s: String) -> Nat:
4596 Maybe.default(&2, Nat, Nat.read(s), 0n)
4597
4598def u32_of(s: String) -> U32:
4599 Maybe.default(&2, U32, U32.read(s), 0)
4600
4601def f32_of(s: String) -> F32:
4602 Maybe.default(&2, F32, F32.read(s), 0.0)
4603
4604def flag(m: Matches, id: String) -> Dec(Bool):
4605 read_with(~Bool, ~(f => get_one(~Bool, ~true_of, f)), WFlag{}, m, id)
4606
4607def count(m: Matches, id: String) -> Dec(Nat):
4608 read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4609 WCount{}, m, id)
4610
4611def str(m: Matches, id: String) -> Dec(String):
4612 read_with(~String, ~(f => get_one(~String, ~(s => s), f)),
4613 WValue{"str", ReadOne{}}, m, id)
4614
4615def str_opt(m: Matches, id: String) -> Dec(Maybe<&2, String>):
4616 read_with(~Maybe<&2, String>, ~(f => get_opt(~String, ~(s => s), f)),
4617 WValue{"str", ReadOpt{}}, m, id)
4618
4619def strs(m: Matches, id: String) -> Dec(List<&2, String>):
4620 read_with(~List<&2, String>, ~(f => get_many(~String, ~(s => s), f)),
4621 WValue{"str", ReadMany{}}, m, id)
4622
4623def nat(m: Matches, id: String) -> Dec(Nat):
4624 read_with(~Nat, ~(f => get_one(~Nat, ~nat_of, f)),
4625 WValue{"nat", ReadOne{}}, m, id)
4626
4627def nat_opt(m: Matches, id: String) -> Dec(Maybe<&2, Nat>):
4628 read_with(~Maybe<&2, Nat>, ~(f => get_opt(~Nat, ~nat_of, f)),
4629 WValue{"nat", ReadOpt{}}, m, id)
4630
4631def nats(m: Matches, id: String) -> Dec(List<&2, Nat>):
4632 read_with(~List<&2, Nat>, ~(f => get_many(~Nat, ~nat_of, f)),
4633 WValue{"nat", ReadMany{}}, m, id)
4634
4635def u32(m: Matches, id: String) -> Dec(U32):
4636 read_with(~U32, ~(f => get_one(~U32, ~u32_of, f)),
4637 WValue{"u32", ReadOne{}}, m, id)
4638
4639def u32_opt(m: Matches, id: String) -> Dec(Maybe<&2, U32>):
4640 read_with(~Maybe<&2, U32>, ~(f => get_opt(~U32, ~u32_of, f)),
4641 WValue{"u32", ReadOpt{}}, m, id)
4642
4643def u32s(m: Matches, id: String) -> Dec(List<&2, U32>):
4644 read_with(~List<&2, U32>, ~(f => get_many(~U32, ~u32_of, f)),
4645 WValue{"u32", ReadMany{}}, m, id)
4646
4647def f32(m: Matches, id: String) -> Dec(F32):
4648 read_with(~F32, ~(f => get_one(~F32, ~f32_of, f)),
4649 WValue{"f32", ReadOne{}}, m, id)
4650
4651def f32_opt(m: Matches, id: String) -> Dec(Maybe<&2, F32>):
4652 read_with(~Maybe<&2, F32>, ~(f => get_opt(~F32, ~f32_of, f)),
4653 WValue{"f32", ReadOpt{}}, m, id)
4654
4655def f32s(m: Matches, id: String) -> Dec(List<&2, F32>):
4656 read_with(~List<&2, F32>, ~(f => get_many(~F32, ~f32_of, f)),
4657 WValue{"f32", ReadMany{}}, m, id)
4658
4659def bool(m: Matches, id: String) -> Dec(Bool):
4660 read_with(~Bool, ~(f => get_one(~Bool, ~env_bool_true, f)),
4661 WValue{"bool", ReadOne{}}, m, id)
4662
4663def bool_opt(m: Matches, id: String) -> Dec(Maybe<&2, Bool>):
4664 read_with(~Maybe<&2, Bool>, ~(f => get_opt(~Bool, ~env_bool_true, f)),
4665 WValue{"bool", ReadOpt{}}, m, id)
4666
4667def bools(m: Matches, id: String) -> Dec(List<&2, Bool>):
4668 read_with(~List<&2, Bool>, ~(f => get_many(~Bool, ~env_bool_true, f)),
4669 WValue{"bool", ReadMany{}}, m, id)
4670
4671
4672type Program is Data:
4673 Program{name: String, args: List<&2, String>}
4674
4675def program_of(values: List<&2, String>) -> Program:
4676 match values:
4677 case Nil{}: Program{"", []}
4678 case head <> tail: Program{head, tail}
4679
4680def program_maybe(values: List<&2, String>) -> Maybe<&2, Program>:
4681 match values:
4682 case Nil{}: None{}
4683 case head <> tail: Some{Program{head, tail}}
4684
4685def get_program(f: Field) -> Program:
4686 match f:
4687 case Field{id, values, source}: program_of(values)
4688
4689def get_program_opt(f: Field) -> Maybe<&2, Program>:
4690 match f:
4691 case Field{id, values, source}: program_maybe(values)
4692
4693
4694
4695
4696def program(m: Matches, id: String) -> Dec(Program):
4697 read_with(~Program, ~get_program, WProgram{False{}}, m, id)
4698
4699def program_opt(m: Matches, id: String) -> Dec(Maybe<&2, Program>):
4700 read_with(~Maybe<&2, Program>, ~get_program_opt, WProgram{True{}}, m, id)
4701
4702def field_src(f: Field) -> Src:
4703 match f:
4704 case Field{id, values, source}: source
4705
4706def given(m: Matches, id: String) -> Dec(Bool):
4707 read_with(~Bool, ~(f => source_given(field_src(f))), WAny{}, m, id)
4708
4709def source(m: Matches, id: String) -> Dec(Src):
4710 read_with(~Src, ~field_src, WAny{}, m, id)
4711
4712def by_key_hit(a, -A: Kind(a), same: Bool, value: A) -> Maybe<a, A>:
4713 match same:
4714 case True{}: Some{value}
4715 case False{}: None{}
4716
4717
4718def find_by_key_go(a, -A: Kind(a), keys: List<&2, String>,
4719 values: List<a, A>, +key: String, found: Maybe<a, A>) -> Maybe<a, A>:
4720 match keys values found:
4721 case _ _ Some{hit}: Some{hit}
4722 case head <> tail value <> rest None{}:
4723 find_by_key_go(a, A, tail, rest, key,
4724 by_key_hit(a, A, same_text(head, key), value))
4725 case _ _ None{}: None{}
4726
4727def find_by_key(a, -A: Kind(a), keys: List<&2, String>, values: List<a, A>,
4728 +key: String) -> Maybe<a, A>:
4729 find_by_key_go(a, A, keys, values, key, None{})
4730
4731def keyed_cons(a, -A: Kind(a), key: String, value: A,
4732 rest: (List<&2, String> & List<a, A>)) -> (List<&2, String> & List<a, A>):
4733 (keys, values) = rest
4734 (key <> keys, value <> values)
4735
4736
4737def unzip_keyed(a, -A: Kind(a), table: List<(String & A)>) ->
4738 (List<&2, String> & List<a, A>):
4739 match table:
4740 case Nil{}: ([], [])
4741 case entry <> tail:
4742 (key, value) = entry
4743 keyed_cons(a, A, key, value, unzip_keyed(a, A, tail))
4744
4745
4746def choice_dec(-A: Data, found: Maybe<&2, A>) -> Dec(Maybe<&2, A>):
4747 match found:
4748 case Some{value}: Done{Some{value}}
4749 case None{}: Fail{Usage{"the value has no choose entry"}}
4750
4751def choose_read(-A: Data, keys: List<&2, String>, values: List<&2, A>,
4752 f: Field) -> Dec(Maybe<&2, A>):
4753 match f:
4754 case Field{id, head <> tail, source}:
4755 choice_dec(A, find_by_key(&2, A, keys, values, head))
4756 case _: Done{None{}}
4757
4758def choose_check(-A: Data, verdict: Maybe<&2, String>,
4759 keys: List<&2, String>, values: List<&2, A>,
4760 fields: List<&2, Field>, id: String) -> Dec(Maybe<&2, A>):
4761 match verdict:
4762 case None{}: choose_read(A, keys, values, field_find(fields, id))
4763 case Some{msg}: Fail{Bug{msg}}
4764
4765def choose_pick(-A: Data, opt: Bool,
4766 choices: (List<&2, String> & List<&2, A>), m: Matches,
4767 +id: String) -> Dec(Maybe<&2, A>):
4768 (+keys, values) = choices
4769 match m:
4770 case Matches{cmd, path, fields, external}:
4771 choose_check(A, verdict(cmd, path, id, WChoose{opt, keys}),
4772 keys, values, fields, id)
4773
4774def choose_opt(-A: Data, m: Matches, id: String,
4775 table: List<(String & A)>) -> Dec(Maybe<&2, A>):
4776 choose_pick(A, True{}, unzip_keyed(&2, A, table), m, id)
4777
4778
4779def choice_must(-A: Data, d: Dec(Maybe<&2, A>)) -> Dec(A):
4780 match d:
4781 case Fail{e}: Fail{e}
4782 case Done{Some{value}}: Done{value}
4783 case Done{None{}}: Fail{Usage{"the value is absent"}}
4784
4785def choose(-A: Data, m: Matches, id: String,
4786 table: List<(String & A)>) -> Dec(A):
4787 choice_must(A, choose_pick(A, False{}, unzip_keyed(&2, A, table), m, id))
4788
4789def Call(-A: Type) -> Type:
4790 @+m: Matches -> Dec(A)
4791
4792
4793
4794
4795def word_rest(name: String, text: String, same: Bool) ->
4796 Maybe<&2, Maybe<&2, String>>:
4797 match name text same:
4798 case _ _ False{}: None{}
4799 case SNil{} SNil{} True{}: Some{None{}}
4800 case SNil{} SCon{Chr{32}, rest} True{}: Some{Some{rest}}
4801 case SCon{Chr{32}, xs} _ True{}: None{}
4802 case SCon{x, xs} SCon{y, ys} True{}: word_rest(xs, ys, Char.is_eq(x, y))
4803 case _ _ _: None{}
4804
4805
4806
4807
4808type KeyStep is Data:
4809 KeyMiss{}
4810 KeyStop{}
4811 KeyEnd{}
4812 KeyNext{rest: String}
4813
4814def key_user(builtin: Bool, rest: Maybe<&2, String>) -> KeyStep:
4815 match builtin rest:
4816 case True{} _: KeyStop{}
4817 case False{} None{}: KeyEnd{}
4818 case False{} Some{text}: KeyNext{text}
4819
4820def key_word(found: Maybe<&2, Maybe<&2, String>>,
4821 children: List<&2, Item>) -> KeyStep:
4822 match found:
4823 case None{}: KeyMiss{}
4824 case Some{rest}: key_user(has_builtin(children), rest)
4825
4826def key_step(items: List<&2, Item>, text: String) -> KeyStep:
4827 match items:
4828 case Sub{name, about, children} <> tail:
4829 key_word(word_rest(name, text, True{}), children)
4830 case _: KeyMiss{}
4831
4832
4833
4834def key_go(items: List<&2, Item>, +text: String, step: KeyStep) -> Bool:
4835 match items step:
4836 case Nil{} _: False{}
4837 case head <> +tail KeyMiss{}: key_go(tail, text, key_step(tail, text))
4838 case head <> tail KeyStop{}: False{}
4839 case head <> tail KeyEnd{}: True{}
4840 case Sub{name, about, +children} <> tail KeyNext{+rest}:
4841 key_go(children, rest, key_step(children, rest))
4842 case _ _: False{}
4843
4844def route_text(empty: Bool, +key: String, +items: List<&2, Item>) -> Bool:
4845 match empty:
4846 case True{}: True{}
4847 case False{}: key_go(items, key, key_step(items, key))
4848
4849
4850def route_key(+key: String, items: List<&2, Item>) -> Bool:
4851 route_text(String.is_empty(key), key, items)
4852
4853def non_command(keys: List<&2, String>, +items: List<&2, Item>) ->
4854 Maybe<&2, String>:
4855 match keys:
4856 case Nil{}: None{}
4857 case +key <> tail:
4858 Bool.pick(Maybe<&2, String>, route_key(key, items),
4859 non_command(tail, items), Some{key})
4860
4861def route_call(-A: Type, bad: Maybe<&2, String>, call: Call(A),
4862 m: Matches) -> Dec(A):
4863 match bad:
4864 case None{}: call(m)
4865 case Some{key}:
4866 Fail{Bug{String.concat(["route '", key, "' is not a command"])}}
4867
4868
4869def route_found(-A: Type, found: Maybe<Call(A)>, keys: List<&2, String>,
4870 cmd: Cmd, path: List<&2, String>, m: Matches) -> Dec(A):
4871 match found cmd:
4872 case None{} Cmd{name, about, items}:
4873 Fail{Bug{String.concat(["no route for '", path_text(name, path), "'"])}}
4874 case Some{call} Cmd{name, about, items}:
4875 route_call(A, non_command(keys, items), call, m)
4876
4877def route_pick(-A: Type, routes: (List<&2, String> & List<Call(A)>),
4878 m: Matches) -> Dec(A):
4879 (+keys, calls) = routes
4880 match m:
4881 case Matches{+cmd, +path, fields, external}:
4882 route_found(A,
4883 find_by_key(&1, Call(A), keys, calls, String.join(path, " ")),
4884 keys, cmd, path, Matches{cmd, path, fields, external})
4885
4886def route(-A: Type, m: Matches, table: List<(String & Call(A))>) -> Dec(A):
4887 route_pick(A, unzip_keyed(&1, Call(A), table), m)
4888
4889
4890
4891def sample_typed(found: Maybe<&2, ValueType>) -> String:
4892 match found:
4893 case Some{TNat{}}: "0"
4894 case Some{TU32{}}: "0"
4895 case Some{TF32{}}: "0.0"
4896 case Some{TBool{}}: "false"
4897 case _: "x"
4898
4899def sample_choice(found: Maybe<&2, List<&2, String>>,
4900 attrs: List<&2, Item>) -> String:
4901 match found:
4902 case Some{head <> tail}: head
4903 case _: sample_typed(find_as(attrs))
4904
4905def sample_value(kind: UsageKind, +attrs: List<&2, Item>) -> String:
4906 match kind:
4907 case FlagKind{}: "true"
4908 case NegatableKind{}: "true"
4909 case CountKind{}: "1"
4910 case _: sample_choice(find_one_of(attrs), attrs)
4911
4912def sample_field(build: FieldBuild, info: ArgInfo) -> List<&2, Field>:
4913 match build:
4914 case Built{Nil{}}:
4915 match info:
4916 case ArgInfo{id, usage, short, long, kind, attrs}:
4917 [Field{id, [sample_value(kind, attrs)], Argv{}}]
4918 case Built{fields}: fields
4919 case BuildFault{fault}: Nil{}
4920
4921
4922def sample_args(items: List<&2, Item>) -> List<&2, Field>:
4923 match items:
4924 case Nil{}: Nil{}
4925 case Arg{usage, help, attrs} <> tail:
4926 +info = arg_info(usage, attrs)
4927 List.append(&2, Field,
4928 sample_field(resolve_default(info_default(info), info), info),
4929 sample_args(tail))
4930 case head <> tail: sample_args(tail)
4931
4932def sample_fields(c: Cmd, path: List<&2, String>) -> List<&2, Field>:
4933 match c:
4934 case Cmd{name, about, items}: sample_args(scope_args(path, items, []))
4935
4936def sample(+c: Cmd, +path: List<&2, String>) -> Matches:
4937 Matches{c, path, sample_fields(c, path), None{}}
4938
4939
4940def sub_paths(items: List<&2, Item>, +prefix: List<&2, String>) ->
4941 List<&2, List<&2, String>>:
4942 match items:
4943 case Nil{}: Nil{}
4944 case Sub{name, about, +children} <> tail:
4945 +path = List.append(&2, String, prefix, [name])
4946 List.concat(&2, List<&2, String>, [
4947 Bool.pick(List<&2, List<&2, String>>,
4948 Bool.or(has_builtin(children), needs_sub(children)), [], [path]),
4949 sub_paths(children, path),
4950 sub_paths(tail, prefix),
4951 ])
4952 case head <> tail: sub_paths(tail, prefix)
4953
4954def sample_paths(c: Cmd) -> List<&2, List<&2, String>>:
4955 match c:
4956 case Cmd{name, about, +items}:
4957 List.append(&2, List<&2, String>,
4958 Bool.pick(List<&2, List<&2, String>>, needs_sub(items), [], [[]]),
4959 sub_paths(items, []))
4960
4961def decode_bug(-A: Type, d: Dec(A), at: String) -> List<&2, Problem>:
4962 match d:
4963 case Fail{Bug{msg}}: [DecodeBug{at, msg}]
4964 case _: Nil{}
4965
4966def cmd_name(c: Cmd) -> String:
4967 match c:
4968 case Cmd{name, about, items}: name
4969
4970def sample_bugs(~A: Type, ~decode: @+m: Matches -> Dec(A),
4971 paths: List<&2, List<&2, String>>, +c: Cmd) -> List<&2, Problem>:
4972 match paths:
4973 case Nil{}: Nil{}
4974 case +path <> tail:
4975 List.append(&2, Problem,
4976 decode_bug(A, decode(sample(c, path)), path_text(cmd_name(c), path)),
4977 sample_bugs(~A, ~decode, tail, c))
4978
4979def check(~A: Type, ~decode: @+m: Matches -> Dec(A), +c: Cmd) ->
4980 List<&2, Problem>:
4981 List.append(&2, Problem, problems(c),
4982 sample_bugs(~A, ~decode, sample_paths(c), c))
4983
4984
4985type Term is Data:
4986 Term{out_color: Bool, err_color: Bool, width: Nat}
4987
4988def host_tty(h: Host, out: Bool, err: Bool) -> Host:
4989 match h:
4990 case Host{width, vars, out_tty, err_tty}: Host{width, vars, out, err}
4991
4992
4993def color_attrs(attrs: List<&2, Item>) -> Bool:
4994 match attrs:
4995 case Con{OneOf{values}, Con{AttrDefault{value}, Con{Global{}, Nil{}}}}:
4996 Bool.and(String.eq(String.join(values, " "), "auto always never"),
4997 String.eq(value, "auto"))
4998 case _: False{}
4999
5000def is_color(item: Item) -> Bool:
5001 match item:
5002 case Arg{usage, help, attrs}:
5003 Bool.and(String.eq(usage, "--color <WHEN>"),
5004 Bool.and(String.eq(help, "When to use color"), color_attrs(attrs)))
5005 case _: False{}
5006
5007def has_color(items: List<&2, Item>) -> Bool:
5008 match items:
5009 case Nil{}: False{}
5010 case item <> tail: Bool.or(is_color(item), has_color(tail))
5011
5012
5013
5014type Prescan is Data:
5015 ColorSeek{last: Maybe<&2, String>}
5016 ColorWant{}
5017 ColorStop{last: Maybe<&2, String>}
5018
5019def color_seek(dash: Bool, bare: Bool, attached: Bool, token: String,
5020 last: Maybe<&2, String>) -> Prescan:
5021 match dash bare attached:
5022 case True{} _ _: ColorStop{last}
5023 case False{} True{} _: ColorWant{}
5024 case False{} False{} True{}: ColorSeek{Some{String.drop(token, 8n)}}
5025 case False{} False{} False{}: ColorSeek{last}
5026
5027def color_want(hyphen: Bool, +token: String) -> Prescan:
5028 match hyphen:
5029 case False{}: ColorSeek{Some{token}}
5030 case True{}:
5031 color_seek(String.eq(token, "--"), String.eq(token, "--color"),
5032 String.starts_with(token, "--color="), token, Some{""})
5033
5034def color_step(state: Prescan, +token: String) -> Prescan:
5035 match state:
5036 case ColorSeek{last}:
5037 color_seek(String.eq(token, "--"), String.eq(token, "--color"),
5038 String.starts_with(token, "--color="), token, last)
5039 case ColorWant{}: color_want(String.starts_with(token, "-"), token)
5040 case ColorStop{last}: ColorStop{last}
5041
5042def color_scan(argv: List<&2, String>, state: Prescan) -> Prescan:
5043 match argv:
5044 case Nil{}: state
5045 case token <> tail: color_scan(tail, color_step(state, token))
5046
5047def color_word(always: Bool, never: Bool) -> Maybe<&2, Bool>:
5048 match always never:
5049 case True{} _: Some{True{}}
5050 case False{} True{}: Some{False{}}
5051 case False{} False{}: None{}
5052
5053def color_value(found: Maybe<&2, String>) -> Maybe<&2, Bool>:
5054 match found:
5055 case None{}: None{}
5056 case Some{+value}:
5057 color_word(String.eq(value, "always"), String.eq(value, "never"))
5058
5059def color_pick(state: Prescan) -> Maybe<&2, Bool>:
5060 match state:
5061 case ColorSeek{last}: color_value(last)
5062 case ColorWant{}: None{}
5063 case ColorStop{last}: color_value(last)
5064
5065def color_flag(enabled: Bool, argv: List<&2, String>) -> Maybe<&2, Bool>:
5066 match enabled:
5067 case True{}: color_pick(color_scan(argv, ColorSeek{None{}}))
5068 case False{}: None{}
5069
5070
5071def var_set(found: Maybe<&2, String>) -> Bool:
5072 match found:
5073 case None{}: False{}
5074 case Some{value}: Bool.not(String.is_empty(value))
5075
5076def var_is(found: Maybe<&2, String>, text: String) -> Bool:
5077 match found:
5078 case None{}: False{}
5079 case Some{value}: String.eq(value, text)
5080
5081def force_set(found: Maybe<&2, String>) -> Bool:
5082 match found:
5083 case None{}: False{}
5084 case Some{+value}:
5085 Bool.and(Bool.not(String.is_empty(value)),
5086 Bool.not(String.eq(value, "0")))
5087
5088def env_off(off: Bool) -> Maybe<&2, Bool>:
5089 match off:
5090 case True{}: Some{False{}}
5091 case False{}: None{}
5092
5093def env_force(force: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
5094 match force:
5095 case True{}: Some{True{}}
5096 case False{}:
5097 env_off(Bool.or(var_is(var_value(vars, "CLICOLOR"), "0"),
5098 var_is(var_value(vars, "TERM"), "dumb")))
5099
5100def env_rules(no_color: Bool, +vars: List<&2, Var>) -> Maybe<&2, Bool>:
5101 match no_color:
5102 case True{}: Some{False{}}
5103 case False{}:
5104 env_force(force_set(var_value(vars, "CLICOLOR_FORCE")), vars)
5105
5106
5107def env_color(+vars: List<&2, Var>) -> Maybe<&2, Bool>:
5108 env_rules(var_set(var_value(vars, "NO_COLOR")), vars)
5109
5110def term_env(found: Maybe<&2, Bool>, out_tty: Bool, err_tty: Bool,
5111 width: Nat) -> Term:
5112 match found:
5113 case Some{+on}: Term{on, on, width}
5114 case None{}: Term{out_tty, err_tty, width}
5115
5116def term_for(flag: Maybe<&2, Bool>, vars: List<&2, Var>, out_tty: Bool,
5117 err_tty: Bool, width: Nat) -> Term:
5118 match flag:
5119 case Some{+on}: Term{on, on, width}
5120 case None{}: term_env(env_color(vars), out_tty, err_tty, width)
5121
5122def term(c: Cmd, h: Host, argv: List<&2, String>) -> Term:
5123 match c h:
5124 case Cmd{name, about, items} Host{width, vars, out_tty, err_tty}:
5125 term_for(color_flag(has_color(items), argv), vars, out_tty, err_tty,
5126 width)
5127
5128
5129
5130def plain(text: String) -> Segment:
5131 Segment{text, []}
5132
5133def page_width(width: Nat) -> Nat:
5134 Nat.min(100n, Nat.max(40n, width))
5135
5136
5137def summary(s: String) -> String:
5138 match s:
5139 case SNil{}: SNil{}
5140 case SCon{Chr{10}, SCon{Chr{10}, tail}}: SNil{}
5141 case SCon{head, tail}: SCon{head, summary(tail)}
5142
5143def shown_text(long: Bool, text: String) -> String:
5144 match long:
5145 case True{}: text
5146 case False{}: summary(text)
5147
5148def filled(s: String) -> Bool:
5149 Bool.not(String.is_empty(s))
5150
5151def join_filled(parts: List<&2, String>) -> String:
5152 String.join(List.filter(~String, ~filled, parts), " ")
5153
5154
5155
5156type View is Data:
5157 View{path: String, about: String, items: List<&2, Item>,
5158 globals: List<&2, Item>, root: Bool, stuck: Bool}
5159
5160def find_command(items: List<&2, Item>, +query: String,
5161 found: Maybe<&2, Item>) -> Maybe<&2, Item>:
5162 match items found:
5163 case _ Some{item}: Some{item}
5164 case Nil{} None{}: None{}
5165 case Sub{+name, about, +children} <> tail None{}:
5166 find_command(tail, query, Bool.pick(Maybe<&2, Item>,
5167 Bool.or(String.eq(query, name),
5168 List.contains(~String, ~String.eq, find_aliases(children), query)),
5169 Some{Sub{name, about, children}}, None{}))
5170 case head <> tail None{}: find_command(tail, query, None{})
5171
5172def view_enter(found: Maybe<&2, Item>, path: String, about: String,
5173 items: List<&2, Item>, globals: List<&2, Item>, root: Bool) -> View:
5174 match found:
5175 case Some{Sub{name, sub_about, children}}:
5176 View{String.concat([path, " ", name]), sub_about, children,
5177 List.append(&2, Item, globals, global_items(items)), False{}, False{}}
5178 case _: View{path, about, items, globals, root, True{}}
5179
5180def view_step(view: View, step: String) -> View:
5181 match view:
5182 case View{path, about, items, globals, root, True{}}:
5183 View{path, about, items, globals, root, True{}}
5184 case View{path, about, +items, globals, root, False{}}:
5185 view_enter(find_command(items, step, None{}), path, about, items,
5186 globals, root)
5187
5188def view_walk(steps: List<&2, String>, view: View) -> View:
5189 match steps:
5190 case Nil{}: view
5191 case step <> tail: view_walk(tail, view_step(view, step))
5192
5193def view_of(c: Cmd, path: List<&2, String>) -> View:
5194 match c:
5195 case Cmd{name, about, items}:
5196 view_walk(path, View{name, about, items, [], True{}, False{}})
5197
5198def arg_pos(usage: String) -> Bool:
5199 usage_kind(~kind_pos, parse_usage(usage))
5200
5201def has_plain_option(items: List<&2, Item>) -> Bool:
5202 match items:
5203 case Nil{}: False{}
5204 case Arg{usage, help, +attrs} <> tail:
5205 Bool.or(Bool.and(Bool.not(arg_pos(usage)),
5206 Bool.and(Bool.not(has_hidden(attrs)), Bool.not(has_required(attrs)))),
5207 has_plain_option(tail))
5208 case head <> tail: has_plain_option(tail)
5209
5210def required_elem(required: Bool, usage: String, attrs: List<&2, Item>) ->
5211 List<&2, Segment>:
5212 match required:
5213 case True{}: [plain(arg_display(arg_info(usage, attrs)))]
5214 case False{}: []
5215
5216def required_options(items: List<&2, Item>) -> List<&2, Segment>:
5217 match items:
5218 case Nil{}: Nil{}
5219 case Arg{+usage, help, +attrs} <> tail:
5220 List.append(&2, Segment,
5221 required_elem(Bool.and(Bool.not(arg_pos(usage)), has_required(attrs)),
5222 usage, attrs),
5223 required_options(tail))
5224 case head <> tail: required_options(tail)
5225
5226def group_elem(listed: Bool, args: List<&2, Item>, group: String) ->
5227 List<&2, Segment>:
5228 match listed:
5229 case True{}: []
5230 case False{}: [plain(group_display(args, group))]
5231
5232
5233def group_elems(items: List<&2, Item>, +args: List<&2, Item>,
5234 +done: List<&2, String>) -> List<&2, Segment>:
5235 match items:
5236 case Nil{}: Nil{}
5237 case NeedOne{+group} <> tail:
5238 List.append(&2, Segment,
5239 group_elem(List.contains(~String, ~String.eq, done, group), args,
5240 group),
5241 group_elems(tail, args, group <> done))
5242 case head <> tail: group_elems(tail, args, done)
5243
5244def positional_elems(items: List<&2, Item>) -> List<&2, Segment>:
5245 match items:
5246 case Nil{}: Nil{}
5247 case Arg{+usage, help, attrs} <> tail:
5248 +rest = positional_elems(tail)
5249 Bool.pick(List<&2, Segment>, arg_pos(usage), plain(usage) <> rest, rest)
5250 case head <> tail: positional_elems(tail)
5251
5252def command_elems(subs: Bool, need: Bool) -> List<&2, Segment>:
5253 match subs need:
5254 case False{} _: []
5255 case True{} True{}: [plain("<COMMAND>")]
5256 case True{} False{}: [plain("[COMMAND]")]
5257
5258def options_elem(present: Bool) -> List<&2, Segment>:
5259 match present:
5260 case True{}: [plain("[OPTIONS]")]
5261 case False{}: []
5262
5263
5264
5265def usage_elems(path: String, +items: List<&2, Item>,
5266 +globals: List<&2, Item>) -> List<&2, Segment>:
5267 +args = List.append(&2, Item, items, globals)
5268 +scope = List.append(&2, Item, globals, items)
5269 List.concat(&2, Segment, [
5270 [Segment{path, [Bold{}]}],
5271 options_elem(has_plain_option(args)),
5272 required_options(args),
5273 group_elems(scope, scope, []),
5274 positional_elems(items),
5275 command_elems(has_sub_item(items), needs_sub(items)),
5276 ])
5277
5278def segment_texts(segs: List<&2, Segment>) -> List<&2, String>:
5279 match segs:
5280 case Nil{}: Nil{}
5281 case Segment{text, styles} <> tail: text <> segment_texts(tail)
5282
5283def usage_view(view: View) -> String:
5284 match view:
5285 case View{path, about, items, globals, root, stuck}:
5286 String.join(segment_texts(usage_elems(path, items, globals)), " ")
5287
5288def usage(c: Cmd, path: List<&2, String>) -> String:
5289 usage_view(view_of(c, path))
5290
5291
5292def usage_rest(elems: List<&2, Segment>, +w: Nat, +col: Nat) ->
5293 List<&2, Segment>:
5294 match elems:
5295 case Nil{}: Nil{}
5296 case Segment{+text, styles} <> tail:
5297 +ew = width(text)
5298 +fits = Nat.is_le((col + 1n + ew : Nat), w)
5299 plain(Bool.pick(String, fits, " ", "\n ")) <>
5300 Segment{text, styles} <> usage_rest(tail, w,
5301 Bool.pick(Nat, fits, (col + 1n + ew : Nat), (7n + ew : Nat)))
5302
5303def usage_block(elems: List<&2, Segment>, w: Nat) -> List<&2, Segment>:
5304 match elems:
5305 case Nil{}: [Segment{"Usage:", [Bold{}, Under{}]}]
5306 case Segment{+text, styles} <> tail:
5307 Segment{"Usage:", [Bold{}, Under{}]} <> plain(" ") <>
5308 Segment{text, styles} <> usage_rest(tail, w, (7n + width(text) : Nat))
5309
5310
5311
5312type Row is Data:
5313 Row{left: List<&2, Segment>, short: Bool, long_only: Bool, right: String}
5314
5315type Section is Data:
5316 Section{title: String, rows: List<&2, Row>}
5317
5318def tag(name: String, values: List<&2, String>) -> String:
5319 match values:
5320 case Nil{}: ""
5321 case head <> tail:
5322 String.concat(["[", name, ": ", String.join(head <> tail, ", "), "]"])
5323
5324def maybe_list(found: Maybe<&2, String>) -> List<&2, String>:
5325 match found:
5326 case None{}: Nil{}
5327 case Some{value}: [value]
5328
5329def list_or_none(found: Maybe<&2, List<&2, String>>) -> List<&2, String>:
5330 match found:
5331 case None{}: Nil{}
5332 case Some{values}: values
5333
5334def right_text(long: Bool, help: String, +attrs: List<&2, Item>) -> String:
5335 join_filled([
5336 shown_text(long, help),
5337 tag("env", find_envs(attrs)),
5338 tag("default", maybe_list(find_default(attrs))),
5339 tag("possible values", list_or_none(find_one_of(attrs))),
5340 tag("aliases", find_aliases(attrs)),
5341 ])
5342
5343def bold_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5344 match names:
5345 case Nil{}: Nil{}
5346 case name <> tail:
5347 List.append(&2, Segment,
5348 Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5349 Segment{String.trim_start(name), [Bold{}]} <> bold_names(tail, False{}))
5350
5351
5352def value_cut(s: String) -> Nat:
5353 match s:
5354 case SNil{}: 0n
5355 case SCon{Chr{32}, SCon{Chr{60}, tail}}: 0n
5356 case SCon{Chr{91}, SCon{Chr{61}, tail}}: 0n
5357 case SCon{Chr{46}, SCon{Chr{46}, SCon{Chr{46}, SNil{}}}}: 0n
5358 case SCon{head, tail}: 1n+value_cut(tail)
5359
5360
5361def option_left(+usage: String) -> List<&2, Segment>:
5362 +cut = value_cut(usage)
5363 List.append(&2, Segment,
5364 bold_names(String.split(String.take(usage, cut), ','), True{}),
5365 [plain(String.drop(usage, cut))])
5366
5367def arg_row(parsed: Usage, usage: String, help: String,
5368 attrs: List<&2, Item>, long: Bool) -> Row:
5369 match parsed:
5370 case Parsed{id, +short, name, kind}:
5371 Row{option_left(usage), filled(short), String.is_empty(short),
5372 right_text(long, help, attrs)}
5373 case Invalid{col}:
5374 Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}
5375
5376def find_heading(attrs: List<&2, Item>) -> String:
5377 match attrs:
5378 case Nil{}: ""
5379 case Heading{title} <> tail: title
5380 case head <> tail: find_heading(tail)
5381
5382def option_row(shown: Bool, +usage: String, help: String,
5383 attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5384 match shown:
5385 case True{}: [arg_row(parse_usage(usage), usage, help, attrs, long)]
5386 case False{}: []
5387
5388
5389def option_rows(items: List<&2, Item>, +long: Bool, +heading: String) ->
5390 List<&2, Row>:
5391 match items:
5392 case Nil{}: Nil{}
5393 case Arg{+usage, help, +attrs} <> tail:
5394 List.append(&2, Row,
5395 option_row(Bool.and(Bool.not(arg_pos(usage)),
5396 Bool.and(Bool.not(has_hidden(attrs)),
5397 String.eq(find_heading(attrs), heading))),
5398 usage, help, attrs, long),
5399 option_rows(tail, long, heading))
5400 case head <> tail: option_rows(tail, long, heading)
5401
5402def positional_row(shown: Bool, usage: String, help: String,
5403 attrs: List<&2, Item>, long: Bool) -> List<&2, Row>:
5404 match shown:
5405 case True{}:
5406 [Row{[plain(usage)], False{}, False{}, right_text(long, help, attrs)}]
5407 case False{}: []
5408
5409def positional_rows(items: List<&2, Item>, +long: Bool) -> List<&2, Row>:
5410 match items:
5411 case Nil{}: Nil{}
5412 case Arg{+usage, help, +attrs} <> tail:
5413 List.append(&2, Row,
5414 positional_row(Bool.and(arg_pos(usage), Bool.not(has_hidden(attrs))),
5415 usage, help, attrs, long),
5416 positional_rows(tail, long))
5417 case head <> tail: positional_rows(tail, long)
5418
5419def command_row(hidden: Bool, name: String, about: String,
5420 children: List<&2, Item>) -> List<&2, Row>:
5421 match hidden:
5422 case True{}: []
5423 case False{}:
5424 [Row{[Segment{name, [Bold{}]}], False{}, False{},
5425 join_filled([summary(about), tag("aliases", find_aliases(children))])}]
5426
5427def command_rows(items: List<&2, Item>) -> List<&2, Row>:
5428 match items:
5429 case Nil{}: Nil{}
5430 case Sub{name, about, +children} <> tail:
5431 List.append(&2, Row,
5432 command_row(has_hidden(children), name, about, children),
5433 command_rows(tail))
5434 case head <> tail: command_rows(tail)
5435
5436def help_sub_row(present: Bool) -> List<&2, Row>:
5437 match present:
5438 case True{}:
5439 [Row{[Segment{"help", [Bold{}]}], False{}, False{},
5440 "Print this message or the help of the given subcommand(s)"}]
5441 case False{}: []
5442
5443
5444def builtin_row(taken: Bool, short: String, long: String,
5445 text: String) -> Row:
5446 match taken:
5447 case True{}: Row{bold_names([long], True{}), False{}, True{}, text}
5448 case False{}:
5449 Row{bold_names([short, long], True{}), True{}, False{}, text}
5450
5451def help_text(differs: Bool, long: Bool) -> String:
5452 match differs long:
5453 case False{} _: "Print help"
5454 case True{} False{}: "Print help (see more with '--help')"
5455 case True{} True{}: "Print help (see a summary with '-h')"
5456
5457def version_row(present: Bool, args: List<&2, Item>) -> List<&2, Row>:
5458 match present:
5459 case True{}:
5460 [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-V")),
5461 "-V", "--version", "Print version")]
5462 case False{}: []
5463
5464
5465def heading_titles(items: List<&2, Item>, +seen: List<&2, String>) ->
5466 List<&2, String>:
5467 match items:
5468 case Nil{}: seen
5469 case Arg{usage, help, +attrs} <> tail:
5470 +title = find_heading(attrs)
5471 heading_titles(tail, Bool.pick(List<&2, String>,
5472 Bool.or(String.is_empty(title), Bool.or(has_hidden(attrs),
5473 List.contains(~String, ~String.eq, seen, title))),
5474 seen, List.append(&2, String, seen, [title])))
5475 case head <> tail: heading_titles(tail, seen)
5476
5477def heading_sections(titles: List<&2, String>, +args: List<&2, Item>,
5478 +long: Bool) -> List<&2, Section>:
5479 match titles:
5480 case Nil{}: Nil{}
5481 case +title <> tail:
5482 Section{title, option_rows(args, long, title)} <>
5483 heading_sections(tail, args, long)
5484
5485
5486def args_paragraphs(items: List<&2, Item>) -> Bool:
5487 match items:
5488 case Nil{}: False{}
5489 case Arg{usage, help, attrs} <> tail:
5490 Bool.or(Bool.and(Bool.not(has_hidden(attrs)),
5491 String.contains(help, "\n\n")), args_paragraphs(tail))
5492 case head <> tail: args_paragraphs(tail)
5493
5494def indent_cell(indent: Bool, left: List<&2, Segment>) -> List<&2, Segment>:
5495 match indent:
5496 case True{}: plain(" ") <> left
5497 case False{}: left
5498
5499
5500def row_cell(row: Row, shorted: Bool) -> List<&2, Segment>:
5501 match row:
5502 case Row{left, short, long_only, right}:
5503 indent_cell(Bool.and(long_only, shorted), left)
5504
5505def cell_width(cell: List<&2, Segment>) -> Nat:
5506 match cell:
5507 case Nil{}: 0n
5508 case Segment{text, styles} <> tail: Nat.add(width(text), cell_width(tail))
5509
5510def row_short(row: Row) -> Bool:
5511 match row:
5512 case Row{left, short, long_only, right}: short
5513
5514def any_short(rows: List<&2, Row>) -> Bool:
5515 match rows:
5516 case Nil{}: False{}
5517 case row <> tail: Bool.or(row_short(row), any_short(tail))
5518
5519
5520def widest(rows: List<&2, Row>, +shorted: Bool, +k: Nat, +best: Nat) -> Nat:
5521 match rows:
5522 case Nil{}: best
5523 case row <> tail:
5524 +w = cell_width(row_cell(row, shorted))
5525 widest(tail, shorted, k,
5526 Bool.pick(Nat, Nat.is_le((w + 4n : Nat), k), Nat.max(w, best), best))
5527
5528def pad_line(line: String, col: Nat) -> String:
5529 match line:
5530 case SNil{}: "\n"
5531 case SCon{head, tail}:
5532 String.concat(["\n", String.repeat(" ", col), SCon{head, tail}])
5533
5534def pad_lines(lines: List<&2, String>, +col: Nat) -> List<&2, Segment>:
5535 match lines:
5536 case Nil{}: Nil{}
5537 case line <> tail: plain(pad_line(line, col)) <> pad_lines(tail, col)
5538
5539def first_then(lines: List<&2, String>, col: Nat) -> List<&2, Segment>:
5540 match lines:
5541 case Nil{}: Nil{}
5542 case line <> tail: plain(line) <> pad_lines(tail, col)
5543
5544
5545
5546def row_form(fits: Bool, cell: List<&2, Segment>, lw: Nat, right: String,
5547 w: Nat, +c: Nat) -> List<&2, Segment>:
5548 match fits:
5549 case True{}:
5550 plain(" ") <> List.append(&2, Segment, cell,
5551 plain(String.repeat(" ", (c - 2n - lw : Nat))) <>
5552 first_then(String.lines(wrap(right, (w - c : Nat))), c))
5553 case False{}:
5554 +col = Nat.max(c, 10n)
5555 plain(" ") <> List.append(&2, Segment, cell,
5556 pad_lines(String.lines(wrap(right, (w - col : Nat))), col))
5557
5558def row_lines(row: Row, shorted: Bool, w: Nat, k: Nat, c: Nat) ->
5559 List<&2, Segment>:
5560 match row:
5561 case Row{left, short, long_only, right}:
5562 +cell = indent_cell(Bool.and(long_only, shorted), left)
5563 +lw = cell_width(cell)
5564 row_form(Nat.is_le((lw + 4n : Nat), k), cell, lw, right, w, c)
5565
5566def rows_lines(rows: List<&2, Row>, +shorted: Bool, +w: Nat, +k: Nat,
5567 +c: Nat) -> List<&2, Segment>:
5568 match rows:
5569 case Nil{}: Nil{}
5570 case row <> tail:
5571 plain("\n") <> List.append(&2, Segment, row_lines(row, shorted, w, k, c),
5572 rows_lines(tail, shorted, w, k, c))
5573
5574def section_rows(sections: List<&2, Section>) -> List<&2, Row>:
5575 match sections:
5576 case Nil{}: Nil{}
5577 case Section{title, rows} <> tail:
5578 List.append(&2, Row, rows, section_rows(tail))
5579
5580def section_blocks(sections: List<&2, Section>, +shorted: Bool, +w: Nat,
5581 +k: Nat, +c: Nat) -> List<&2, List<&2, Segment>>:
5582 match sections:
5583 case Nil{}: Nil{}
5584 case Section{title, Nil{}} <> tail: section_blocks(tail, shorted, w, k, c)
5585 case Section{title, rows} <> tail:
5586 (Segment{String.append(title, ":"), [Bold{}, Under{}]} <>
5587 rows_lines(rows, shorted, w, k, c)) <>
5588 section_blocks(tail, shorted, w, k, c)
5589
5590
5591def sections_blocks(+sections: List<&2, Section>, +w: Nat) ->
5592 List<&2, List<&2, Segment>>:
5593 +rows = section_rows(sections)
5594 +shorted = any_short(rows)
5595 +k = Nat.div((2n * w : Nat), 5n)
5596 section_blocks(sections, shorted, w, k,
5597 (widest(rows, shorted, k, 0n) + 4n : Nat))
5598
5599def text_block(text: String, w: Nat) -> List<&2, List<&2, Segment>>:
5600 match text:
5601 case SNil{}: Nil{}
5602 case SCon{head, tail}: [[plain(wrap(SCon{head, tail}, w))]]
5603
5604def example_lines(items: List<&2, Item>) -> List<&2, Segment>:
5605 match items:
5606 case Nil{}: Nil{}
5607 case Example{line, what} <> tail:
5608 plain(String.concat(["\n ", what, ":\n $ ", line])) <>
5609 example_lines(tail)
5610 case head <> tail: example_lines(tail)
5611
5612def examples_block(lines: List<&2, Segment>) -> List<&2, List<&2, Segment>>:
5613 match lines:
5614 case Nil{}: Nil{}
5615 case head <> tail: [Segment{"Examples:", [Bold{}, Under{}]} <> head <> tail]
5616
5617def after_texts(items: List<&2, Item>) -> List<&2, String>:
5618 match items:
5619 case Nil{}: Nil{}
5620 case After{text} <> tail: text <> after_texts(tail)
5621 case head <> tail: after_texts(tail)
5622
5623def more_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5624 match blocks:
5625 case Nil{}: [plain("\n")]
5626 case block <> tail:
5627 plain("\n\n") <> List.append(&2, Segment, block, more_blocks(tail))
5628
5629
5630def join_blocks(blocks: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
5631 match blocks:
5632 case Nil{}: Nil{}
5633 case block <> tail: List.append(&2, Segment, block, more_blocks(tail))
5634
5635def help_page(view: View, +long: Bool, +w: Nat) -> List<&2, Segment>:
5636 match view:
5637 case View{path, +about, +items, +globals, root, stuck}:
5638 +args = List.append(&2, Item, items, globals)
5639 join_blocks(List.concat(&2, List<&2, Segment>, [
5640 text_block(shown_text(long, about), w),
5641 [usage_block(usage_elems(path, items, globals), w)],
5642 sections_blocks(List.append(&2, Section, [
5643 Section{"Commands", List.append(&2, Row, command_rows(items),
5644 help_sub_row(has_sub_item(items)))},
5645 Section{"Arguments", positional_rows(items, long)},
5646 Section{"Options", List.concat(&2, Row, [
5647 option_rows(args, long, ""),
5648 [builtin_row(Maybe.is_some(&2, OptFound, find_option(args, "-h")),
5649 "-h", "--help", help_text(Bool.or(String.contains(about, "\n\n"),
5650 args_paragraphs(args)), long))],
5651 version_row(Bool.and(root, has_version_item(items)), args),
5652 ])},
5653 ], heading_sections(heading_titles(args, []), args, long)), w),
5654 examples_block(example_lines(items)),
5655 text_block(String.join(after_texts(items), "\n\n"), w),
5656 ]))
5657
5658def help_out(t: Term, view: View, long: Bool) -> String:
5659 match t:
5660 case Term{out_color, err_color, width}:
5661 emit(out_color, help_page(view, long, page_width(width)))
5662
5663
5664def help(+c: Cmd, path: List<&2, String>, long: Bool, h: Host) -> String:
5665 help_out(term(c, h, []), view_of(c, path), long)
5666
5667
5668
5669type Scored is Data:
5670 Scored{dist: Nat, name: String}
5671
5672def row_zero(b: String, +k: Nat) -> List<&2, Nat>:
5673 match b:
5674 case SNil{}: [k]
5675 case SCon{head, tail}: k <> row_zero(tail, (k + 1n : Nat))
5676
5677def head_nat(xs: List<&2, Nat>) -> Nat:
5678 match xs:
5679 case Nil{}: 0n
5680 case head <> tail: head
5681
5682def tail_nats(xs: List<&2, Nat>) -> List<&2, Nat>:
5683 match xs:
5684 case Nil{}: Nil{}
5685 case head <> tail: tail
5686
5687def same_char(found: Maybe<&2, Char>, c: Char) -> Bool:
5688 match found:
5689 case None{}: False{}
5690 case Some{d}: Char.is_eq(d, c)
5691
5692
5693
5694def osa_row(bs: String, bp: Maybe<&2, Char>, +a: Char,
5695 +ap: Maybe<&2, Char>, +up: List<&2, Nat>, +up2: List<&2, Nat>,
5696 left: Nat) -> List<&2, Nat>:
5697 match bs:
5698 case SNil{}: Nil{}
5699 case SCon{+b, tail}:
5700 +diag = head_nat(up)
5701 +rest = tail_nats(up)
5702 +edit = Nat.min(Nat.min((head_nat(rest) + 1n : Nat), (left + 1n : Nat)),
5703 Bool.pick(Nat, Char.is_eq(a, b), diag, (diag + 1n : Nat)))
5704 +best = Bool.pick(Nat, Bool.and(same_char(bp, a), same_char(ap, b)),
5705 Nat.min(edit, (head_nat(up2) + 1n : Nat)), edit)
5706 best <> osa_row(tail, Some{b}, a, ap, rest, tail_nats(up2), best)
5707
5708def osa_rows(s: String, ap: Maybe<&2, Char>, +i: Nat, +b: String,
5709 +prev: List<&2, Nat>, prev2: List<&2, Nat>) -> List<&2, Nat>:
5710 match s:
5711 case SNil{}: prev
5712 case SCon{+a, tail}:
5713 osa_rows(tail, Some{a}, (i + 1n : Nat), b,
5714 i <> osa_row(b, None{}, a, ap, prev, 0n <> prev2, i), prev)
5715
5716def last_nat(xs: List<&2, Nat>, last: Nat) -> Nat:
5717 match xs:
5718 case Nil{}: last
5719 case head <> tail: last_nat(tail, head)
5720
5721def osa(a: String, +b: String) -> Nat:
5722 +first = row_zero(b, 0n)
5723 last_nat(osa_rows(a, None{}, 1n, b, first, first), 0n)
5724
5725def undashed(s: String) -> String:
5726 match s:
5727 case SCon{Chr{45}, tail}: undashed(tail)
5728 case _: s
5729
5730def scored(names: List<&2, String>, +typed: String, +limit: Nat) ->
5731 List<&2, Scored>:
5732 match names:
5733 case Nil{}: Nil{}
5734 case +name <> tail:
5735 +dist = osa(typed, String.take(name, 64n))
5736 +rest = scored(tail, typed, limit)
5737 Bool.pick(List<&2, Scored>, Nat.is_le(dist, limit),
5738 Scored{dist, name} <> rest, rest)
5739
5740def at_distance(found: List<&2, Scored>, +d: Nat) -> List<&2, String>:
5741 match found:
5742 case Nil{}: Nil{}
5743 case Scored{dist, name} <> tail:
5744 +rest = at_distance(tail, d)
5745 Bool.pick(List<&2, String>, Nat.is_eq(dist, d), name <> rest, rest)
5746
5747def by_distance(levels: Nat, +d: Nat, +found: List<&2, Scored>) ->
5748 List<&2, String>:
5749 match levels:
5750 case 0n: Nil{}
5751 case 1n+more:
5752 List.append(&2, String, at_distance(found, d),
5753 by_distance(more, (d + 1n : Nat), found))
5754
5755
5756
5757
5758
5759def suggest(+typed: String, names: List<&2, String>) -> List<&2, String>:
5760 key = undashed(typed)
5761 +limit = Nat.max(1n, Nat.div(String.length(key), 3n))
5762 List.take(&2, String, by_distance((limit + 1n : Nat), 0n,
5763 scored(names, String.take(typed, 64n), limit)), 3n)
5764
5765
5766
5767def quoted(text: String, styles: List<&2, Style>) -> List<&2, Segment>:
5768 [plain("'"), Segment{text, styles}, plain("'")]
5769
5770def missing_lines(missing: List<&2, String>) -> String:
5771 match missing:
5772 case Nil{}: ""
5773 case head <> tail: String.concat(["\n ", head, missing_lines(tail)])
5774
5775def visible_subs(items: List<&2, Item>) -> List<&2, String>:
5776 match items:
5777 case Nil{}: ["help"]
5778 case Sub{name, about, children} <> tail:
5779 +rest = visible_subs(tail)
5780 Bool.pick(List<&2, String>, has_hidden(children), rest, name <> rest)
5781 case head <> tail: visible_subs(tail)
5782
5783def fault_message(fault: ScanFault, path: String,
5784 items: List<&2, Item>) -> List<&2, Segment>:
5785 match fault:
5786 case E01{token}:
5787 List.concat(&2, Segment, [[plain("unexpected argument ")],
5788 quoted(token, [Yellow{}]), [plain(" found")]])
5789 case E02{name}:
5790 plain("unrecognized subcommand ") <> quoted(name, [Yellow{}])
5791 case E03{arg}:
5792 [plain(String.concat(["a value is required for '", arg,
5793 "' but none was supplied"]))]
5794 case E04{arg, value, reason}:
5795 List.concat(&2, Segment, [[plain("invalid value ")],
5796 quoted(value, [Yellow{}]),
5797 [plain(String.concat([" for '", arg, "': ", reason]))]])
5798 case E05{arg, value}:
5799 List.concat(&2, Segment, [[plain("unexpected value ")],
5800 quoted(value, [Yellow{}]),
5801 [plain(String.concat([" for '", arg,
5802 "' found; no more were expected"]))]])
5803 case E06{missing}:
5804 [plain(String.append(
5805 "the following required arguments were not provided:",
5806 missing_lines(missing)))]
5807 case E07{a, b}:
5808 [plain(String.concat(["the argument '", a, "' cannot be used with '", b,
5809 "'"]))]
5810 case E08{a, b}:
5811 [plain(String.concat(["the argument '", a, "' requires '", b, "'"]))]
5812 case E09{token}:
5813 List.concat(&2, Segment, [[plain("unexpected argument ")],
5814 quoted(token, [Yellow{}]), [plain(" found")]])
5815 case E10{}:
5816 [plain(String.concat(["'", path,
5817 "' requires a subcommand but none was provided\n [subcommands: ",
5818 String.join(visible_subs(items), ", "), "]"]))]
5819 case E11{arg, value, name, reason}:
5820 List.concat(&2, Segment, [[plain("invalid value ")],
5821 quoted(value, [Yellow{}]),
5822 [plain(String.concat([" for '", arg, "' from environment variable ",
5823 name, ": ", reason]))]])
5824
5825def green_names(names: List<&2, String>, first: Bool) -> List<&2, Segment>:
5826 match names:
5827 case Nil{}: Nil{}
5828 case name <> tail:
5829 List.concat(&2, Segment, [
5830 Bool.pick(List<&2, Segment>, first, [], [plain(", ")]),
5831 quoted(name, [Green{}]), green_names(tail, False{})])
5832
5833def tip(text: String) -> List<&2, List<&2, Segment>>:
5834 [[plain(text)]]
5835
5836
5837def similar(kind: String, found: List<&2, String>) ->
5838 List<&2, List<&2, Segment>>:
5839 match found:
5840 case Nil{}: Nil{}
5841 case name <> Nil{}:
5842 [plain(String.concat(["a similar ", kind, " exists: "])) <>
5843 quoted(name, [Green{}])]
5844 case name <> more:
5845 [plain(String.concat(["some similar ", kind, "s exist: "])) <>
5846 green_names(name <> more, True{})]
5847
5848def long_name(s: String) -> Bool:
5849 String.starts_with(s, "--")
5850
5851def option_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
5852 match parsed:
5853 case Parsed{id, short, +long, kind}:
5854 List.concat(&2, String, [
5855 Bool.pick(List<&2, String>, String.is_empty(long), [], [long]),
5856 Bool.pick(List<&2, String>,
5857 Bool.and(kind_negatable(kind), filled(long)),
5858 [negated_long(long)], []),
5859 List.filter(~String, ~long_name, find_aliases(attrs)),
5860 ])
5861 case Invalid{col}: []
5862
5863def visible_names(hidden: Bool, usage: String, attrs: List<&2, Item>) ->
5864 List<&2, String>:
5865 match hidden:
5866 case True{}: []
5867 case False{}: option_names(parse_usage(usage), attrs)
5868
5869
5870def long_candidates(items: List<&2, Item>) -> List<&2, String>:
5871 match items:
5872 case Nil{}: Nil{}
5873 case Arg{usage, help, +attrs} <> tail:
5874 List.append(&2, String, visible_names(has_hidden(attrs), usage, attrs),
5875 long_candidates(tail))
5876 case head <> tail: long_candidates(tail)
5877
5878
5879def sub_candidates(items: List<&2, Item>) -> List<&2, String>:
5880 match items:
5881 case Nil{}: ["help"]
5882 case Sub{name, about, +children} <> tail:
5883 List.append(&2, String,
5884 Bool.pick(List<&2, String>, has_hidden(children), [],
5885 name <> find_aliases(children)),
5886 sub_candidates(tail))
5887 case head <> tail: sub_candidates(tail)
5888
5889
5890def display_choices(items: List<&2, Item>, +arg: String,
5891 found: List<&2, String>) -> List<&2, String>:
5892 match items found:
5893 case _ head <> tail: head <> tail
5894 case Nil{} Nil{}: Nil{}
5895 case Arg{usage, help, +attrs} <> tail Nil{}:
5896 display_choices(tail, arg, Bool.pick(List<&2, String>,
5897 String.eq(arg_display(arg_info(usage, attrs)), arg),
5898 list_or_none(find_one_of(attrs)), []))
5899 case head <> tail Nil{}: display_choices(tail, arg, [])
5900
5901def owned(found: Maybe<&2, OptFound>, text: String) -> Maybe<&2, String>:
5902 match found:
5903 case None{}: None{}
5904 case Some{option}: Some{text}
5905
5906def owned_by(hit: Maybe<&2, OptFound>, token: String, prefix: String,
5907 step: String, found: Maybe<&2, String>) -> Maybe<&2, String>:
5908 match hit:
5909 case None{}: found
5910 case Some{option}:
5911 Some{String.concat(["'", token, "' belongs to '", prefix,
5912 "'; place it before '", step, "'"])}
5913
5914
5915def owner(path: List<&2, String>, +items: List<&2, Item>, +prefix: String,
5916 +token: String, +long: String, found: Maybe<&2, String>) ->
5917 Maybe<&2, String>:
5918 match path:
5919 case Nil{}: found
5920 case +step <> tail:
5921 owner(tail, sub_items(find_sub(items, step)),
5922 String.concat([prefix, " ", step]), token, long,
5923 owned_by(find_option(items, long), token, prefix, step, found))
5924
5925
5926def descendant(items: List<&2, Item>, +prefix: String, +long: String,
5927 found: Maybe<&2, String>) -> Maybe<&2, String>:
5928 match items found:
5929 case _ Some{path}: Some{path}
5930 case Nil{} None{}: None{}
5931 case Sub{name, about, +children} <> tail None{}:
5932 +path = String.concat([prefix, " ", name])
5933 descendant(tail, prefix, long, descendant(children, path, long,
5934 owned(find_option(children, long), path)))
5935 case head <> tail None{}: descendant(tail, prefix, long, None{})
5936
5937def maybe_tip(found: Maybe<&2, String>) -> List<&2, List<&2, Segment>>:
5938 match found:
5939 case None{}: Nil{}
5940 case Some{text}: tip(text)
5941
5942def belongs(found: Maybe<&2, String>, token: String) ->
5943 List<&2, List<&2, Segment>>:
5944 match found:
5945 case None{}: Nil{}
5946 case Some{path}:
5947 tip(String.concat(["'", token, "' belongs to subcommand '", path, "'"]))
5948
5949def sub_exists(found: Maybe<&2, SubFound>, name: String) ->
5950 List<&2, List<&2, Segment>>:
5951 match found:
5952 case None{}: Nil{}
5953 case Some{sub}:
5954 tip(String.concat(["subcommand '", name,
5955 "' exists; to use it, remove the '--'"]))
5956
5957def before_eq(s: String) -> String:
5958 match s:
5959 case SNil{}: SNil{}
5960 case SCon{Chr{61}, tail}: SNil{}
5961 case SCon{head, tail}: SCon{head, before_eq(tail)}
5962
5963
5964def long_tips(is_long: Bool, +token: String, root: String,
5965 path: List<&2, String>, +items: List<&2, Item>,
5966 root_items: List<&2, Item>, here: String) ->
5967 List<&2, List<&2, Segment>>:
5968 match is_long:
5969 case False{}: Nil{}
5970 case True{}:
5971 +long = before_eq(token)
5972 +name = String.drop(long, 2n)
5973 List.concat(&2, List<&2, Segment>, [
5974 maybe_tip(owner(path, root_items, root, token, long, None{})),
5975 belongs(descendant(items, here, long, None{}), token),
5976 sub_exists(find_sub(items, name), name),
5977 ])
5978
5979def operand_count(events: List<&2, ScanEvent>, count: Nat) -> Nat:
5980 match events:
5981 case Nil{}: count
5982 case Enter{name} <> tail: operand_count(tail, 0n)
5983 case Operand{token} <> tail: operand_count(tail, (count + 1n : Nat))
5984 case event <> tail: operand_count(tail, count)
5985
5986def trace_operands(trace: ScanTrace) -> Nat:
5987 match trace:
5988 case ScanTrace{path, events, ended, external, issue}:
5989 operand_count(events, 0n)
5990
5991def dash_slot(open: Bool, +token: String) -> List<&2, List<&2, Segment>>:
5992 match open:
5993 case False{}: Nil{}
5994 case True{}:
5995 tip(String.concat(["to pass '", token, "' as a value, use '-- ", token,
5996 "'"]))
5997
5998
5999
6000def dash_tip(hyphen: Bool, token: String, items: List<&2, Item>, c: Cmd,
6001 argv: List<&2, String>) -> List<&2, List<&2, Segment>>:
6002 match hyphen:
6003 case False{}: Nil{}
6004 case True{}:
6005 dash_slot(positional_can_take(items, trace_operands(scan(c, argv))),
6006 token)
6007
6008def cmd_items(c: Cmd) -> List<&2, Item>:
6009 match c:
6010 case Cmd{name, about, items}: items
6011
6012
6013def fault_tips(fault: ScanFault, +c: Cmd, +path: List<&2, String>,
6014 argv: List<&2, String>, here: String, +items: List<&2, Item>,
6015 globals: List<&2, Item>) -> List<&2, List<&2, Segment>>:
6016 match fault:
6017 case E01{+token}:
6018 List.concat(&2, List<&2, Segment>, [
6019 long_tips(long_name(token), token, cmd_name(c), path, items,
6020 cmd_items(c), here),
6021 similar("argument", suggest(token, List.concat(&2, String, [
6022 long_candidates(List.append(&2, Item, items, globals)),
6023 ["--help"],
6024 Bool.pick(List<&2, String>, Bool.and(List.is_empty(&2, String, path),
6025 has_version_item(items)), ["--version"], []),
6026 ]))),
6027 dash_tip(String.starts_with(token, "-"), token, items, c, argv),
6028 ])
6029 case E02{+name}: similar("subcommand", suggest(name, sub_candidates(items)))
6030 case E04{+arg, +value, reason}:
6031 similar("value", suggest(value,
6032 display_choices(List.append(&2, Item, items, globals), arg, [])))
6033 case E09{+token}:
6034 dash_tip(String.starts_with(token, "-"), token, items, c, argv)
6035 case _: Nil{}
6036
6037def tip_spans(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
6038 match tips:
6039 case Nil{}: Nil{}
6040 case line <> tail:
6041 List.concat(&2, Segment, [
6042 [plain(" "), Segment{"tip:", [Bold{}, Green{}]}, plain(" ")],
6043 line, [plain("\n")], tip_spans(tail)])
6044
6045def tip_block(tips: List<&2, List<&2, Segment>>) -> List<&2, Segment>:
6046 match tips:
6047 case Nil{}: Nil{}
6048 case head <> tail: plain("\n") <> tip_spans(head <> tail)
6049
6050def error_page(message: List<&2, Segment>,
6051 tips: List<&2, List<&2, Segment>>, usage: List<&2, Segment>) ->
6052 List<&2, Segment>:
6053 List.concat(&2, Segment, [
6054 [Segment{"error:", [Bold{}, Red{}]}, plain(" ")], message, [plain("\n")],
6055 tip_block(tips), [plain("\n")], usage,
6056 [plain("\n\nFor more information, try '"), Segment{"--help", [Bold{}]},
6057 plain("'.\n")],
6058 ])
6059
6060def fault_view(view: View, +fault: ScanFault, c: Cmd, path: List<&2, String>,
6061 argv: List<&2, String>, w: Nat) -> List<&2, Segment>:
6062 match view:
6063 case View{+here, about, +items, +globals, root, stuck}:
6064 error_page(fault_message(fault, here, items),
6065 fault_tips(fault, c, path, argv, here, items, globals),
6066 usage_block(usage_elems(here, items, globals), w))
6067
6068
6069def fault_err(t: Term, fault: ScanFault, +c: Cmd, +path: List<&2, String>,
6070 argv: List<&2, String>) -> String:
6071 match t:
6072 case Term{out_color, err_color, width}:
6073 emit(err_color, fault_view(view_of(c, path), fault, c, path, argv,
6074 page_width(width)))
6075
6076
6077
6078
6079
6080def unknown_text(c: Cmd, path: List<&2, String>, name: String,
6081 t: Term) -> String:
6082 fault_err(t, E02{name}, c, path, [])
6083
6084def summary_err(t: Term, view: View) -> String:
6085 match t:
6086 case Term{out_color, err_color, width}:
6087 emit(err_color, help_page(view, False{}, page_width(width)))
6088
6089
6090
6091def problem_line(p: Problem) -> String:
6092 match p:
6093 case BadUsage{at, usage, col}:
6094 String.concat([" ", at, ": usage '", usage, "' is invalid at column ",
6095 Nat.show((col + 1n : Nat))])
6096 case BadName{at, name}:
6097 String.concat([" ", at, ": '", name, "' is not a valid name"])
6098 case Dup{at, name}:
6099 String.concat([" ", at, ": '", name, "' is declared more than once"])
6100 case Reserved{at, name}:
6101 String.concat([" ", at, ": '", name, "' is reserved"])
6102 case Misplaced{at, attr}:
6103 String.concat([" ", at, ": '", attr, "' is not allowed here"])
6104 case NoHelp{at, what}:
6105 String.concat([" ", at, ": '", what, "' has no help text"])
6106 case BadDefault{at, id, value}:
6107 String.concat([" ", at, ": '", id, "' cannot have the default '", value,
6108 "'"])
6109 case BadRef{at, id}:
6110 String.concat([" ", at, ": '", id,
6111 "' is an unknown id or an unused or empty group"])
6112 case BadPositional{at, id, why}:
6113 String.concat([" ", at, ": positional '", id, "': ", why])
6114 case Mixed{at}:
6115 String.concat([" ", at, ": has both positionals and subcommands"])
6116 case BadChoices{at, id}:
6117 String.concat([" ", at, ": one_of for '", id,
6118 "' is empty or repeats a value"])
6119 case BadChoice{at, id, value}:
6120 String.concat([" ", at, ": '", id, "' cannot have the choice '", value,
6121 "'"])
6122 case NeedBare{at, id}:
6123 String.concat([" ", at, ": '", id,
6124 "' needs [=<V>] and bare() together, with no short"])
6125 case DecodeBug{at, msg}:
6126 String.concat([" ", at, ": decoder bug: ", msg])
6127
6128def problem_lines(found: List<&2, Problem>) -> String:
6129 match found:
6130 case Nil{}: ""
6131 case p <> tail: String.concat([problem_line(p), "\n", problem_lines(tail)])
6132
6133
6134
6135type Shell is Data:
6136 Bash{}
6137 Zsh{}
6138 Fish{}
6139
6140def sh_body(s: String) -> String:
6141 match s:
6142 case SNil{}: SNil{}
6143 case SCon{Chr{39}, tail}: String.append("'\\''", sh_body(tail))
6144 case SCon{c, tail}: SCon{c, sh_body(tail)}
6145
6146
6147def Sh.quote(s: String) -> String:
6148 SCon{'\'', String.append(sh_body(s), "'")}
6149
6150def fish_body(s: String) -> String:
6151 match s:
6152 case SNil{}: SNil{}
6153 case SCon{Chr{39}, tail}: String.append("\\'", fish_body(tail))
6154 case SCon{Chr{92}, tail}: String.append("\\\\", fish_body(tail))
6155 case SCon{c, tail}: SCon{c, fish_body(tail)}
6156
6157
6158def Fish.quote(s: String) -> String:
6159 SCon{'\'', String.append(fish_body(s), "'")}
6160
6161
6162
6163def Zsh.esc(s: String) -> String:
6164 match s:
6165 case SNil{}: SNil{}
6166 case SCon{Chr{92}, tail}: String.append("\\\\", Zsh.esc(tail))
6167 case SCon{Chr{91}, tail}: String.append("\\[", Zsh.esc(tail))
6168 case SCon{Chr{93}, tail}: String.append("\\]", Zsh.esc(tail))
6169 case SCon{Chr{58}, tail}: String.append("\\:", Zsh.esc(tail))
6170 case SCon{c, tail}: SCon{c, Zsh.esc(tail)}
6171
6172
6173
6174def roff_body(s: String, start: Bool) -> String:
6175 match s start:
6176 case SNil{} _: SNil{}
6177 case SCon{Chr{46}, tail} True{}: String.append("\\&.", roff_body(tail, False{}))
6178 case SCon{Chr{32}, tail} True{}: String.append("\\& ", roff_body(tail, False{}))
6179 case SCon{Chr{10}, tail} _: SCon{'\n', roff_body(tail, True{})}
6180 case SCon{Chr{92}, tail} _: String.append("\\e", roff_body(tail, False{}))
6181 case SCon{Chr{45}, tail} _: String.append("\\-", roff_body(tail, False{}))
6182 case SCon{Chr{34}, tail} _: String.append("\\(dq", roff_body(tail, False{}))
6183 case SCon{Chr{39}, tail} _: String.append("\\(aq", roff_body(tail, False{}))
6184 case SCon{Chr{96}, tail} _: String.append("\\(ga", roff_body(tail, False{}))
6185 case SCon{c, tail} _: SCon{c, roff_body(tail, False{})}
6186
6187
6188def Roff.esc(s: String) -> String:
6189 roff_body(s, True{})
6190
6191
6192def roff_text(s: String) -> String:
6193 roff_body(s, False{})
6194
6195def text_lines(lines: List<&2, String>) -> String:
6196 match lines:
6197 case Nil{}: ""
6198 case line <> tail: String.concat([line, "\n", text_lines(tail)])
6199
6200
6201def one_line(s: String) -> String:
6202 String.join(String.lines(summary(s)), " ")
6203
6204def ident_char(+c: Char) -> Char:
6205 Bool.pick(Char, Bool.or(name_start(c), Char.is_eq(c, '_')), c, '_')
6206
6207
6208def ident(s: String) -> String:
6209 match s:
6210 case SNil{}: SNil{}
6211 case SCon{c, tail}: SCon{ident_char(c), ident(tail)}
6212
6213def same_words(a: List<&2, String>, b: List<&2, String>) -> Bool:
6214 match a b:
6215 case Nil{} Nil{}: True{}
6216 case x <> xs y <> ys: Bool.and(String.eq(x, y), same_words(xs, ys))
6217 case _ _: False{}
6218
6219def last_with(lines: List<&2, String>, +suffix: String) -> List<&2, String>:
6220 match lines:
6221 case Nil{}: Nil{}
6222 case line <> Nil{}: [String.append(line, suffix)]
6223 case line <> tail: line <> last_with(tail, suffix)
6224
6225
6226
6227type Place is Data:
6228 Place{words: List<&2, String>, items: List<&2, Item>,
6229 globals: List<&2, Item>}
6230
6231
6232def places(items: List<&2, Item>, +words: List<&2, String>,
6233 +globals: List<&2, Item>) -> List<&2, Place>:
6234 match items:
6235 case Nil{}: Nil{}
6236 case Sub{name, about, +children} <> tail:
6237 +here = List.append(&2, String, words, [name])
6238 List.append(&2, Place,
6239 Bool.pick(List<&2, Place>, has_hidden(children), [],
6240 Place{here, children, globals} <> places(children, here,
6241 List.append(&2, Item, globals, global_items(children)))),
6242 places(tail, words, globals))
6243 case head <> tail: places(tail, words, globals)
6244
6245def all_places(+items: List<&2, Item>) -> List<&2, Place>:
6246 Place{[], items, []} <> places(items, [], global_items(items))
6247
6248
6249
6250type Comp is Data:
6251 Words{list: Nat, values: List<&2, String>}
6252 Files{}
6253 Dirs{}
6254 Free{}
6255
6256
6257
6258type Opt is Data:
6259 Opt{names: List<&2, String>, kind: UsageKind, ph: String, comp: Comp,
6260 help: String}
6261
6262
6263type Slot is Data:
6264 Slot{k: Nat, hidden: Bool, required: Bool, many: Bool, rest: Bool,
6265 ph: String, comp: Comp}
6266
6267
6268type Entry is Data:
6269 Entry{name: String, names: List<&2, String>, about: String}
6270
6271
6272def list_number(lists: List<&2, List<&2, String>>,
6273 +values: List<&2, String>, hit: Bool, +k: Nat) -> Nat:
6274 match lists hit:
6275 case _ True{}: k
6276 case Nil{} False{}: 0n
6277 case head <> tail False{}:
6278 list_number(tail, values, same_words(head, values), (k + 1n : Nat))
6279
6280def add_list(found: Maybe<&2, List<&2, String>>,
6281 +lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6282 match found:
6283 case None{}: lists
6284 case Some{+values}:
6285 Bool.pick(List<&2, List<&2, String>>,
6286 List.contains(~List<&2, String>, ~same_words, lists, values), lists,
6287 List.append(&2, List<&2, String>, lists, [values]))
6288
6289def visible_one_of(hidden: Bool, attrs: List<&2, Item>) ->
6290 Maybe<&2, List<&2, String>>:
6291 match hidden:
6292 case True{}: None{}
6293 case False{}: find_one_of(attrs)
6294
6295def item_lists(items: List<&2, Item>, lists: List<&2, List<&2, String>>) ->
6296 List<&2, List<&2, String>>:
6297 match items:
6298 case Nil{}: lists
6299 case Arg{usage, help, +attrs} <> tail:
6300 item_lists(tail, add_list(visible_one_of(has_hidden(attrs), attrs), lists))
6301 case head <> tail: item_lists(tail, lists)
6302
6303
6304def place_lists(places: List<&2, Place>,
6305 lists: List<&2, List<&2, String>>) -> List<&2, List<&2, String>>:
6306 match places:
6307 case Nil{}: lists
6308 case Place{words, items, globals} <> tail:
6309 place_lists(tail, item_lists(items, lists))
6310
6311def type_comp(found: Maybe<&2, ValueType>) -> Comp:
6312 match found:
6313 case Some{TFile{}}: Files{}
6314 case Some{TDir{}}: Dirs{}
6315 case _: Free{}
6316
6317def comp_of(found: Maybe<&2, List<&2, String>>, t: Maybe<&2, ValueType>,
6318 lists: List<&2, List<&2, String>>) -> Comp:
6319 match found:
6320 case Some{+values}: Words{list_number(lists, values, False{}, 0n), values}
6321 case None{}: type_comp(t)
6322
6323def head_part(parts: List<&2, String>) -> String:
6324 match parts:
6325 case head <> tail: head
6326 case Nil{}: ""
6327
6328def next_part(parts: List<&2, String>) -> String:
6329 match parts:
6330 case head <> next <> tail: next
6331 case _: ""
6332
6333def between(s: String, +open: Char, close: Char) -> String:
6334 head_part(String.split(next_part(String.split(s, open)), close))
6335
6336
6337def placeholder(+usage: String) -> String:
6338 Bool.pick(String, String.contains(usage, "<"), between(usage, '<', '>'),
6339 between(usage, '[', ']'))
6340
6341def short_alias(s: String) -> Bool:
6342 Bool.not(long_name(s))
6343
6344def short_names(parsed: Usage, attrs: List<&2, Item>) -> List<&2, String>:
6345 match parsed:
6346 case Parsed{id, +short, long, kind}:
6347 List.append(&2, String,
6348 Bool.pick(List<&2, String>, String.is_empty(short), [], [short]),
6349 List.filter(~String, ~short_alias, find_aliases(attrs)))
6350 case Invalid{col}: []
6351
6352def parsed_kind(parsed: Usage) -> UsageKind:
6353 match parsed:
6354 case Parsed{id, short, long, kind}: kind
6355 case Invalid{col}: FlagKind{}
6356
6357def arg_opt(+parsed: Usage, usage: String, help: String,
6358 +attrs: List<&2, Item>, lists: List<&2, List<&2, String>>) -> Opt:
6359 Opt{List.append(&2, String, short_names(parsed, attrs),
6360 option_names(parsed, attrs)),
6361 parsed_kind(parsed), placeholder(usage),
6362 comp_of(find_one_of(attrs), find_as(attrs), lists), one_line(help)}
6363
6364
6365def arg_opts(items: List<&2, Item>, +lists: List<&2, List<&2, String>>) ->
6366 List<&2, Opt>:
6367 match items:
6368 case Nil{}: Nil{}
6369 case Arg{+usage, help, +attrs} <> tail:
6370 +rest = arg_opts(tail, lists)
6371 +parsed = parse_usage(usage)
6372 Bool.pick(List<&2, Opt>,
6373 Bool.or(usage_kind(~kind_pos, parsed), has_hidden(attrs)), rest,
6374 arg_opt(parsed, usage, help, attrs, lists) <> rest)
6375 case head <> tail: arg_opts(tail, lists)
6376
6377def flag_opt(names: List<&2, String>, help: String) -> Opt:
6378 Opt{names, FlagKind{}, "", Free{}, help}
6379
6380def version_opts(present: Bool, args: List<&2, Item>) -> List<&2, Opt>:
6381 match present:
6382 case True{}:
6383 [flag_opt(Bool.pick(List<&2, String>,
6384 Maybe.is_some(&2, OptFound, find_option(args, "-V")),
6385 ["--version"], ["-V", "--version"]), "Print version")]
6386 case False{}: []
6387
6388
6389
6390def builtin_opts(+items: List<&2, Item>, globals: List<&2, Item>,
6391 root: Bool) -> List<&2, Opt>:
6392 +args = List.append(&2, Item, items, globals)
6393 flag_opt(Bool.pick(List<&2, String>,
6394 Maybe.is_some(&2, OptFound, find_option(args, "-h")),
6395 ["--help"], ["-h", "--help"]), "Print help") <>
6396 version_opts(Bool.and(root, has_version_item(items)), args)
6397
6398def pos_slot(parsed: Usage, k: Nat, usage: String, +attrs: List<&2, Item>,
6399 lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6400 match parsed:
6401 case Parsed{id, short, long, PosKind{required, many}}:
6402 [Slot{k, has_hidden(attrs), required, many, has_rest(attrs),
6403 placeholder(usage), comp_of(find_one_of(attrs), find_as(attrs), lists)}]
6404 case _: []
6405
6406
6407def arg_slots(items: List<&2, Item>, +k: Nat,
6408 +lists: List<&2, List<&2, String>>) -> List<&2, Slot>:
6409 match items:
6410 case Nil{}: Nil{}
6411 case Arg{+usage, help, attrs} <> tail:
6412 +parsed = parse_usage(usage)
6413 List.append(&2, Slot, pos_slot(parsed, k, usage, attrs, lists),
6414 arg_slots(tail, Bool.pick(Nat, usage_kind(~kind_pos, parsed),
6415 (k + 1n : Nat), k), lists))
6416 case head <> tail: arg_slots(tail, k, lists)
6417
6418
6419def completing(slots: List<&2, Slot>) -> List<&2, Slot>:
6420 match slots:
6421 case Nil{}: Nil{}
6422 case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6423 [Slot{k, hidden, required, True{}, rest, ph, comp}]
6424 case head <> tail: head <> completing(tail)
6425
6426def variadic(slots: List<&2, Slot>) -> Maybe<&2, Slot>:
6427 match slots:
6428 case Nil{}: None{}
6429 case Slot{k, hidden, required, True{}, rest, ph, comp} <> tail:
6430 Some{Slot{k, hidden, required, True{}, rest, ph, comp}}
6431 case head <> tail: variadic(tail)
6432
6433
6434def sub_entries(items: List<&2, Item>) -> List<&2, Entry>:
6435 match items:
6436 case Nil{}: Nil{}
6437 case Sub{+name, about, +children} <> tail:
6438 +rest = sub_entries(tail)
6439 Bool.pick(List<&2, Entry>, has_hidden(children), rest,
6440 Entry{name, name <> find_aliases(children), one_line(about)} <> rest)
6441 case head <> tail: sub_entries(tail)
6442
6443def opt_names(opts: List<&2, Opt>) -> List<&2, String>:
6444 match opts:
6445 case Nil{}: Nil{}
6446 case Opt{names, kind, ph, comp, help} <> tail:
6447 List.append(&2, String, names, opt_names(tail))
6448
6449def entry_names(entries: List<&2, Entry>) -> List<&2, String>:
6450 match entries:
6451 case Nil{}: Nil{}
6452 case Entry{name, names, about} <> tail:
6453 List.append(&2, String, names, entry_names(tail))
6454
6455
6456
6457type Stop is Data:
6458 Stop{k: Nat, words: List<&2, String>, opts: List<&2, Opt>,
6459 builtins: List<&2, Opt>, subs: List<&2, Entry>, slots: List<&2, Slot>}
6460
6461def stop_of(place: Place, k: Nat, +lists: List<&2, List<&2, String>>) ->
6462 Stop:
6463 match place:
6464 case Place{+words, +items, +globals}:
6465 Stop{k, words, List.append(&2, Opt, arg_opts(items, lists),
6466 arg_opts(globals, lists)),
6467 builtin_opts(items, globals, List.is_empty(&2, String, words)),
6468 sub_entries(items), arg_slots(items, 1n, lists)}
6469
6470def stops(places: List<&2, Place>, +k: Nat,
6471 +lists: List<&2, List<&2, String>>) -> List<&2, Stop>:
6472 match places:
6473 case Nil{}: Nil{}
6474 case place <> tail:
6475 stop_of(place, k, lists) <> stops(tail, (k + 1n : Nat), lists)
6476
6477
6478def stop_number(stops: List<&2, Stop>, +words: List<&2, String>,
6479 found: Maybe<&2, Nat>) -> Nat:
6480 match stops found:
6481 case _ Some{k}: k
6482 case Nil{} None{}: 0n
6483 case Stop{+k, here, opts, builtins, subs, slots} <> tail None{}:
6484 stop_number(tail, words, Bool.pick(Maybe<&2, Nat>,
6485 same_words(here, words), Some{k}, None{}))
6486
6487
6488
6489
6490def sh_list(words: List<&2, String>, +sep: String) -> String:
6491 match words:
6492 case Nil{}: ""
6493 case word <> Nil{}: Sh.quote(word)
6494 case word <> tail: String.concat([Sh.quote(word), sep, sh_list(tail, sep)])
6495
6496def dests(names: List<&2, String>, +dest: String) -> List<&2, String>:
6497 match names:
6498 case Nil{}: Nil{}
6499 case name <> tail: dest <> dests(tail, dest)
6500
6501
6502def entry_dests(entries: List<&2, Entry>, +words: List<&2, String>) ->
6503 List<&2, String>:
6504 match entries:
6505 case Nil{}: Nil{}
6506 case Entry{name, names, about} <> tail:
6507 List.append(&2, String,
6508 dests(names, String.join(List.append(&2, String, words, [name]), " ")),
6509 entry_dests(tail, words))
6510
6511def bash_subs(+entries: List<&2, Entry>, words: List<&2, String>) ->
6512 List<&2, String>:
6513 Bool.pick(List<&2, String>, List.is_empty(&2, Entry, entries), [], [
6514 String.concat([" subs=(", sh_list(entry_names(entries), " "), ")"]),
6515 String.concat([" dests=(", sh_list(entry_dests(entries, words), " "),
6516 ")"]),
6517 ])
6518
6519def bash_shape(found: Maybe<&2, Slot>) -> List<&2, String>:
6520 match found:
6521 case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
6522 [String.concat([" many=", Nat.show(k), " rest=",
6523 Bool.pick(String, rest, "1", "0")])]
6524 case None{}: []
6525
6526def bash_scope_arm(stop: Stop) -> List<&2, String>:
6527 match stop:
6528 case Stop{k, +words, opts, builtins, subs, slots}:
6529 String.concat([" ", Sh.quote(String.join(words, " ")), ")"]) <>
6530 last_with(List.concat(&2, String, [
6531 [String.concat([" opts=(", sh_list(List.append(&2, String,
6532 opt_names(opts), opt_names(builtins)), " "), ")"])],
6533 bash_subs(subs, words),
6534 bash_shape(variadic(slots)),
6535 ]), " ;;")
6536
6537def bash_letter(comp: Comp) -> String:
6538 match comp:
6539 case Words{list, values}: "w"
6540 case Files{}: "f"
6541 case Dirs{}: "d"
6542 case Free{}: "s"
6543
6544def bash_values(comp: Comp) -> String:
6545 match comp:
6546 case Words{list, values}: String.concat([" values=(", sh_list(values, " "), ")"])
6547 case _: ""
6548
6549def bash_kind(+comp: Comp, bare: Bool) -> String:
6550 String.concat(["kind=", bash_letter(comp),
6551 Bool.pick(String, bare, " opt=1", ""), bash_values(comp)])
6552
6553def bash_opt_case(opt: Opt) -> List<&2, String>:
6554 match opt:
6555 case Opt{names, OptionKind{many}, ph, comp, help}:
6556 [String.concat([" ", sh_list(names, "|"), ") ",
6557 bash_kind(comp, False{}), " ;;"])]
6558 case Opt{names, BareOptionKind{}, ph, comp, help}:
6559 [String.concat([" ", sh_list(names, "|"), ") ",
6560 bash_kind(comp, True{}), " ;;"])]
6561 case _: []
6562
6563def bash_opt_cases(opts: List<&2, Opt>) -> List<&2, String>:
6564 match opts:
6565 case Nil{}: Nil{}
6566 case opt <> tail: List.append(&2, String, bash_opt_case(opt), bash_opt_cases(tail))
6567
6568def bash_slot_cases(slots: List<&2, Slot>) -> List<&2, String>:
6569 match slots:
6570 case Nil{}: Nil{}
6571 case Slot{k, False{}, required, many, rest, ph, Words{list, values}} <> tail:
6572 String.concat([" ", Nat.show(k), ") ",
6573 bash_kind(Words{list, values}, False{}), " ;;"]) <> bash_slot_cases(tail)
6574 case Slot{k, False{}, required, many, rest, ph, Files{}} <> tail:
6575 String.concat([" ", Nat.show(k), ") kind=f ;;"]) <> bash_slot_cases(tail)
6576 case Slot{k, False{}, required, many, rest, ph, Dirs{}} <> tail:
6577 String.concat([" ", Nat.show(k), ") kind=d ;;"]) <> bash_slot_cases(tail)
6578 case head <> tail: bash_slot_cases(tail)
6579
6580def bash_cases(words: List<&2, String>, cases: List<&2, String>) ->
6581 List<&2, String>:
6582 match cases:
6583 case Nil{}: []
6584 case head <> tail:
6585 List.concat(&2, String, [
6586 [String.concat([" ", Sh.quote(String.join(words, " ")), ")"]),
6587 " case $1 in"],
6588 head <> tail,
6589 [" esac ;;"],
6590 ])
6591
6592def bash_arg_arm(stop: Stop) -> List<&2, String>:
6593 match stop:
6594 case Stop{k, words, opts, builtins, subs, slots}:
6595 bash_cases(words, List.append(&2, String, bash_opt_cases(opts),
6596 bash_slot_cases(completing(slots))))
6597
6598def bash_arms(~f: Stop -> List<&2, String>, stops: List<&2, Stop>) ->
6599 List<&2, String>:
6600 match stops:
6601 case Nil{}: Nil{}
6602 case stop <> tail: List.append(&2, String, f(stop), bash_arms(~f, tail))
6603
6604
6605def bash_engine(+id: String, name: String) -> List<&2, String>:
6606 [
6607 "",
6608 "# Offers each value of kind that starts with $1, written after $2 and cut to",
6609 "# the part readline completes: the text after the last \"=\" or \":\". Readline",
6610 "# quotes files and directories; words are quoted here.",
6611 String.concat([id, "_offer() {"]),
6612 " local word full i",
6613 " case $kind in",
6614 " f) compopt -o filenames; mapfile -t values < <(compgen -f -- \"$1\") ;;",
6615 " d) compopt -o filenames; mapfile -t values < <(compgen -d -- \"$1\") ;;",
6616 " w)",
6617 " for ((i = 0; i < ${#values[@]}; i++)); do",
6618 " printf -v 'values[i]' %q \"${values[i]}\"",
6619 " done ;;",
6620 " *) return ;;",
6621 " esac",
6622 " for word in \"${values[@]}\"; do",
6623 " full=$2$word",
6624 " [[ $word == \"$1\"* ]] && COMPREPLY+=(\"${full:${#2}+${#1}-${#tail}}\")",
6625 " done",
6626 "}",
6627 "",
6628 String.concat([id, "() {"]),
6629 " local -a words=() opts=() subs=() dests=() values=()",
6630 " local cur tail word line gap path= want= kind opt many rest i j n=0 end=0 help=0",
6631 " COMPREPLY=()",
6632 " # Rejoin the \"=\" and \":\" that bash splits words at. COMP_LINE has no blank",
6633 " # at such a split, so a \"=\" typed as its own word stays one.",
6634 " line=${COMP_LINE#*\"${COMP_WORDS[0]}\"}",
6635 " for ((i = 1; i <= COMP_CWORD; i++)); do",
6636 " word=${COMP_WORDS[i]}",
6637 " tail=${COMP_WORDS[i-1]}",
6638 " gap=${line%%[![:blank:]]*}",
6639 " line=${line:${#gap}+${#word}}",
6640 " if ((i > 1)) && [[ -z $gap ]] &&",
6641 " [[ -n $word && -z ${word//[=:]} || -n $tail && -z ${tail//[=:]} ]]; then",
6642 " words[${#words[@]}-1]+=$word",
6643 " else",
6644 " words+=(\"$word\")",
6645 " fi",
6646 " done",
6647 " tail=${COMP_WORDS[COMP_CWORD]}",
6648 " [[ -z ${tail//[=:]} ]] && tail=",
6649 " cur=${words[${#words[@]}-1]}",
6650 " unset 'words[${#words[@]}-1]'",
6651 String.concat([" ", id, "_scope"]),
6652 " for word in \"${words[@]}\"; do",
6653 " if [[ -n $want ]]; then",
6654 " want=",
6655 " elif ((!end)) && [[ $word == -- ]]; then",
6656 " end=1",
6657 " elif ((!end)) && [[ $word == --* ]]; then",
6658 String.concat([" ", id, "_arg \"${word%%=*}\""]),
6659 " [[ -n $kind && $opt == 0 && $word != *=* ]] && want=$word",
6660 " elif ((!end)) && [[ $word == -?* ]]; then",
6661 " for ((j = 1; j < ${#word}; j++)); do",
6662 String.concat([" ", id, "_arg \"-${word:j:1}\""]),
6663 " [[ -n $kind ]] || continue",
6664 " ((j + 1 == ${#word})) && want=-${word:j:1}",
6665 " break",
6666 " done",
6667 " elif ((${#subs[@]})); then",
6668 " ((end)) && return",
6669 " if [[ $word == help ]] && ((!help)); then",
6670 " help=1",
6671 " continue",
6672 " fi",
6673 " for ((j = 0; j < ${#subs[@]}; j++)); do",
6674 " [[ $word == \"${subs[j]}\" ]] && break",
6675 " done",
6676 " ((j < ${#subs[@]})) || return",
6677 " path=${dests[j]}",
6678 String.concat([" ", id, "_scope"]),
6679 " else",
6680 " # n counts the slots filled. The variadic slot stays open, but a rest()",
6681 " # value takes every later word, so nothing more completes.",
6682 " n=$((n + 1))",
6683 " if ((n == many && rest)); then",
6684 " end=1",
6685 " elif ((n == many)); then",
6686 " n=$((n - 1))",
6687 " fi",
6688 " fi",
6689 " done",
6690 " if [[ -n $want ]]; then",
6691 String.concat([" ", id, "_arg \"$want\""]),
6692 String.concat([" ", id, "_offer \"$cur\" ''"]),
6693 " elif ((!end)) && [[ $cur == --*=* ]]; then",
6694 String.concat([" ", id, "_arg \"${cur%%=*}\""]),
6695 String.concat([" ", id, "_offer \"${cur#*=}\" \"${cur%%=*}=\""]),
6696 " elif ((!end)) && [[ $cur == -* ]]; then",
6697 " values=(\"${opts[@]}\") kind=w",
6698 " ((help)) && values=('-h' '--help')",
6699 String.concat([" ", id, "_offer \"$cur\" ''"]),
6700 " elif ((!end && ${#subs[@]})); then",
6701 " values=(\"${subs[@]}\") kind=w",
6702 " ((help)) || values+=('help')",
6703 String.concat([" ", id, "_offer \"$cur\" ''"]),
6704 " elif ((!help)); then",
6705 String.concat([" ", id, "_arg $((n + 1))"]),
6706 String.concat([" ", id, "_offer \"$cur\" ''"]),
6707 " fi",
6708 "}",
6709 "",
6710 String.concat(["complete -F ", id, " ", name]),
6711 ]
6712
6713def bash_script(+name: String, +items: List<&2, Item>) -> String:
6714 +all = all_places(items)
6715 +found = stops(all, 0n, place_lists(all, []))
6716 +id = String.append("_", ident(name))
6717 text_lines(List.concat(&2, String, [
6718 ["# bash completion; needs bash 4 or later.",
6719 "",
6720 "# Sets the tables of command path $path: opts, the options to offer; subs and",
6721 "# dests, each subcommand name and the path it enters; many, the slot of the",
6722 "# variadic positional, or 0; rest, 1 when that slot is rest().",
6723 String.append(id, "_scope() {"),
6724 " opts=() subs=() dests=() many=0 rest=0",
6725 " case $path in"],
6726 bash_arms(~bash_scope_arm, found),
6727 [" esac",
6728 "}",
6729 "",
6730 "# Sets kind to what the option or positional $1 (a name, or a 1-based slot)",
6731 "# takes at $path: w, a word from values; f, a file; d, a directory; s, other",
6732 "# text. kind is empty for an option that takes no value and for a positional",
6733 "# that completes nothing; opt is 1 when the value can only follow \"=\".",
6734 String.append(id, "_arg() {"),
6735 " kind= opt=0 values=()",
6736 " case $path in"],
6737 bash_arms(~bash_arg_arm, found),
6738 [" esac", "}"],
6739 bash_engine(id, Sh.quote(name)),
6740 ]))
6741
6742
6743
6744def zsh_action(comp: Comp) -> String:
6745 match comp:
6746 case Words{list, values}: String.concat(["($v", Nat.show(list), ")"])
6747 case Files{}: "_files"
6748 case Dirs{}: "_files -/"
6749 case Free{}: " "
6750
6751
6752
6753def zsh_pre(kind: UsageKind, +names: List<&2, String>) -> String:
6754 match kind:
6755 case CountKind{}: "*"
6756 case OptionKind{True{}}: "*"
6757 case _:
6758 Bool.pick(String, Nat.is_lt(1n, List.length(&2, String, names)),
6759 String.concat(["(", String.join(names, " "), ")"]), "")
6760
6761def zsh_spec(+name: String, kind: UsageKind, pre: String, d: String,
6762 ph: String, act: String) -> String:
6763 match kind:
6764 case OptionKind{many}:
6765 String.concat([pre, name, Bool.pick(String, long_name(name), "=", "+"),
6766 "[", d, "]:", ph, ":", act])
6767 case BareOptionKind{}:
6768 String.concat([pre, name, "=-[", d, "]::", ph, ":", act])
6769 case _: String.concat([pre, name, "[", d, "]"])
6770
6771def zsh_names(names: List<&2, String>, +kind: UsageKind, +pre: String,
6772 +d: String, +ph: String, +act: String) -> List<&2, String>:
6773 match names:
6774 case Nil{}: Nil{}
6775 case name <> tail:
6776 zsh_spec(name, kind, pre, d, ph, act) <>
6777 zsh_names(tail, kind, pre, d, ph, act)
6778
6779def zsh_opts(opts: List<&2, Opt>) -> List<&2, String>:
6780 match opts:
6781 case Nil{}: Nil{}
6782 case Opt{+names, +kind, ph, comp, help} <> tail:
6783 List.append(&2, String,
6784 zsh_names(names, kind, zsh_pre(kind, names), Zsh.esc(help),
6785 Zsh.esc(ph), zsh_action(comp)),
6786 zsh_opts(tail))
6787
6788def zsh_slots(slots: List<&2, Slot>) -> List<&2, String>:
6789 match slots:
6790 case Nil{}: Nil{}
6791 case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
6792 zsh_slots(tail)
6793 case Slot{k, False{}, required, True{}, False{}, ph, comp} <> tail:
6794 String.concat(["*:", Zsh.esc(ph), ":", zsh_action(comp)]) <>
6795 zsh_slots(tail)
6796 case Slot{k, False{}, required, +many, rest, ph, comp} <> tail:
6797 String.concat([Bool.pick(String, many, "(-)", ""), Nat.show(k),
6798 Bool.pick(String, required, ":", "::"), Zsh.esc(ph), ":",
6799 zsh_action(comp)]) <> zsh_slots(tail)
6800
6801
6802def zsh_spec_lines(specs: List<&2, String>, +ind: String) ->
6803 List<&2, String>:
6804 match specs:
6805 case Nil{}: Nil{}
6806 case spec <> Nil{}: [String.concat([ind, " ", Sh.quote(spec)])]
6807 case spec <> tail:
6808 String.concat([ind, " ", Sh.quote(spec), " \\"]) <>
6809 zsh_spec_lines(tail, ind)
6810
6811def zsh_args(+ind: String, specs: List<&2, String>) -> List<&2, String>:
6812 String.append(ind, "_arguments -C -s -S \\") <> zsh_spec_lines(specs, ind)
6813
6814def zsh_entry(names: List<&2, String>, +ind: String, +about: String) ->
6815 List<&2, String>:
6816 match names:
6817 case Nil{}: Nil{}
6818 case name <> tail:
6819 String.concat([ind, " ", Sh.quote(String.concat([Zsh.esc(name), ":",
6820 about]))]) <> zsh_entry(tail, ind, about)
6821
6822def zsh_entries(entries: List<&2, Entry>, +ind: String) -> List<&2, String>:
6823 match entries:
6824 case Nil{}: Nil{}
6825 case Entry{name, names, +about} <> tail:
6826 List.append(&2, String, zsh_entry(names, ind, Zsh.esc(about)),
6827 zsh_entries(tail, ind))
6828
6829def zsh_commands(entries: List<&2, Entry>, with_help: Bool, label: String,
6830 +ind: String) -> List<&2, String>:
6831 List.concat(&2, String, [
6832 [String.append(ind, "local -a commands=(")],
6833 zsh_entries(entries, ind),
6834 Bool.pick(List<&2, String>, with_help, [String.append(ind,
6835 " 'help:Print this message or the help of the given subcommand(s)'")],
6836 []),
6837 [String.append(ind, ")"),
6838 String.concat([ind, "_describe -t commands ", Sh.quote(label),
6839 " commands"])],
6840 ])
6841
6842def zsh_label(+name: String, words: List<&2, String>) -> String:
6843 String.append(String.join(name <> words, " "), " command")
6844
6845def zsh_deeper(arms: List<&2, String>, +depth: Nat, +ind: String) ->
6846 List<&2, String>:
6847 match arms:
6848 case Nil{}: []
6849 case head <> tail:
6850 List.concat(&2, String, [
6851 [String.append(ind, "else"),
6852 String.concat([ind, " case $words[", Nat.show(depth), "] in"])],
6853 head <> tail,
6854 [String.append(ind, " esac")],
6855 ])
6856
6857
6858def zsh_help_tree(entries: List<&2, Entry>, label: String,
6859 arms: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6860 List.concat(&2, String, [
6861 [String.concat([ind, "if (( CURRENT == ", Nat.show(depth), " )); then"])],
6862 zsh_commands(entries, False{}, label, String.append(ind, " ")),
6863 zsh_deeper(arms, depth, ind),
6864 [String.append(ind, "fi")],
6865 ])
6866
6867def bar_names(names: List<&2, String>) -> String:
6868 match names:
6869 case Nil{}: ""
6870 case name <> Nil{}: Sh.quote(name)
6871 case name <> tail: String.concat([Sh.quote(name), "|", bar_names(tail)])
6872
6873
6874
6875def zsh_help_arms(items: List<&2, Item>, +name: String,
6876 +words: List<&2, String>, +depth: Nat, +ind: String) -> List<&2, String>:
6877 match items:
6878 case Nil{}: Nil{}
6879 case Sub{+sub, about, +children} <> tail:
6880 +here = List.append(&2, String, words, [sub])
6881 +entries = sub_entries(children)
6882 List.append(&2, String,
6883 Bool.pick(List<&2, String>,
6884 Bool.or(has_hidden(children), List.is_empty(&2, Entry, entries)), [],
6885 String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6886 last_with(zsh_help_tree(entries, zsh_label(name, here),
6887 zsh_help_arms(children, name, here, (depth + 1n : Nat),
6888 String.append(ind, " ")),
6889 (depth + 1n : Nat), String.append(ind, " ")), " ;;")),
6890 zsh_help_arms(tail, name, words, depth, ind))
6891 case head <> tail: zsh_help_arms(tail, name, words, depth, ind)
6892
6893
6894
6895def zsh_past_end(ind: String) -> String:
6896 String.append(ind, "(( ${#${(M)words[1,CURRENT-1]:#--}} < ends )) && return 1")
6897
6898
6899
6900def zsh_ends(words: List<&2, String>, +ind: String) -> List<&2, String>:
6901 match words:
6902 case Nil{}:
6903 [String.append(ind,
6904 "# _arguments -S drops a -- typed before a command's first word, so a"),
6905 String.append(ind,
6906 "# state whose words hold fewer -- than the line has is past one."),
6907 String.append(ind, "local -i ends=${#${(M)words[2,CURRENT-1]:#--}}")]
6908 case _: []
6909
6910
6911
6912def zsh_branch(+entries: List<&2, Entry>, specs: List<&2, String>,
6913 +items: List<&2, Item>, +name: String, +words: List<&2, String>,
6914 arms: List<&2, String>, +ind: String) -> List<&2, String>:
6915 +state = String.join(name <> words, "-")
6916 +i2 = String.append(ind, " ")
6917 +hstate = String.append(state, "-help")
6918 +label = zsh_label(name, words)
6919 List.concat(&2, String, [
6920 zsh_ends(words, ind),
6921 last_with(zsh_args(ind, List.append(&2, String, specs,
6922 [String.append("*:: :->", state)])), " && return"),
6923 [String.append(ind, "case $state in"),
6924 String.concat([ind, " ", Sh.quote(state), ")"]),
6925 zsh_past_end(i2),
6926 String.append(i2, "if (( CURRENT == 1 )); then")],
6927 zsh_commands(entries, True{}, label, String.append(i2, " ")),
6928 [String.append(i2, "else"), String.append(i2, " case $words[1] in")],
6929 arms,
6930 [String.append(i2, " 'help')")],
6931 last_with(zsh_args(String.append(i2, " "), ["(-h --help)-h[Print help]",
6932 "(-h --help)--help[Print help]", String.append("*:: :->", hstate)]),
6933 " && return"),
6934 [String.append(i2, " case $state in"),
6935 String.concat([i2, " ", Sh.quote(hstate), ")"]),
6936 zsh_past_end(String.append(i2, " "))],
6937 last_with(zsh_help_tree(entries, label,
6938 zsh_help_arms(items, name, words, 1n,
6939 String.append(i2, " ")),
6940 1n, String.append(i2, " ")), " ;;"),
6941 [String.append(i2, " esac ;;"), String.append(i2, " esac"),
6942 String.append(i2, "fi ;;"), String.append(ind, "esac")],
6943 ])
6944
6945def zsh_body(entries: List<&2, Entry>, specs: List<&2, String>,
6946 +items: List<&2, Item>, +lists: List<&2, List<&2, String>>,
6947 name: String, words: List<&2, String>, arms: List<&2, String>,
6948 ind: String) -> List<&2, String>:
6949 match entries:
6950 case Nil{}:
6951 zsh_args(ind, List.append(&2, String, specs,
6952 zsh_slots(completing(arg_slots(items, 1n, lists)))))
6953 case head <> tail:
6954 zsh_branch(head <> tail, specs, items, name, words, arms, ind)
6955
6956
6957
6958def zsh_cmd(+items: List<&2, Item>, +globals: List<&2, Item>,
6959 +lists: List<&2, List<&2, String>>, name: String,
6960 +words: List<&2, String>, arms: List<&2, String>, ind: String) ->
6961 List<&2, String>:
6962 zsh_body(sub_entries(items),
6963 List.append(&2, String,
6964 zsh_opts(List.append(&2, Opt, arg_opts(items, lists),
6965 arg_opts(globals, lists))),
6966 zsh_opts(builtin_opts(items, globals,
6967 List.is_empty(&2, String, words)))),
6968 items, lists, name, words, arms, ind)
6969
6970
6971
6972def zsh_arms(items: List<&2, Item>, +globals: List<&2, Item>,
6973 +lists: List<&2, List<&2, String>>, +name: String,
6974 +words: List<&2, String>, +ind: String) -> List<&2, String>:
6975 match items:
6976 case Nil{}: Nil{}
6977 case Sub{+sub, about, +children} <> tail:
6978 +here = List.append(&2, String, words, [sub])
6979 List.append(&2, String,
6980 Bool.pick(List<&2, String>, has_hidden(children), [],
6981 String.concat([ind, bar_names(sub <> find_aliases(children)), ")"]) <>
6982 last_with(zsh_cmd(children, globals, lists, name, here,
6983 zsh_arms(children, List.append(&2, Item, globals,
6984 global_items(children)), lists, name, here,
6985 String.append(ind, " ")),
6986 String.append(ind, " ")), " ;;")),
6987 zsh_arms(tail, globals, lists, name, words, ind))
6988 case head <> tail: zsh_arms(tail, globals, lists, name, words, ind)
6989
6990def zsh_lists(lists: List<&2, List<&2, String>>, +k: Nat) ->
6991 List<&2, String>:
6992 match lists:
6993 case Nil{}: Nil{}
6994 case values <> tail:
6995 String.concat(["v", Nat.show(k), "=(", sh_list(values, " "), ")"]) <>
6996 zsh_lists(tail, (k + 1n : Nat))
6997
6998def zsh_arrays(lists: List<&2, List<&2, String>>) -> List<&2, String>:
6999 match lists:
7000 case Nil{}: []
7001 case head <> tail:
7002 [String.append(" local -a ", String.join(zsh_lists(head <> tail, 1n),
7003 " "))]
7004
7005def zsh_script(+name: String, +items: List<&2, Item>) -> String:
7006 +lists = place_lists(all_places(items), [])
7007 +id = String.append("_", ident(name))
7008 text_lines(List.concat(&2, String, [
7009 [String.append("#compdef ", name), "", String.append(id, "() {"),
7010 " local curcontext=$curcontext state line"],
7011 zsh_arrays(lists),
7012 zsh_cmd(items, [], lists, name, [],
7013 zsh_arms(items, global_items(items), lists, name, [], " "),
7014 " "),
7015 ["}",
7016 "",
7017 String.concat(["if [[ $funcstack[1] == ", Sh.quote(String.append("_",
7018 name)), " ]]; then"]),
7019 String.concat([" ", id, " \"$@\""]),
7020 "else",
7021 String.concat([" compdef ", id, " ", Sh.quote(name)]),
7022 "fi"],
7023 ]))
7024
7025
7026
7027
7028def fish_list(words: List<&2, String>) -> String:
7029 match words:
7030 case Nil{}: ""
7031 case word <> Nil{}: Fish.quote(word)
7032 case word <> tail: String.concat([Fish.quote(word), " ", fish_list(tail)])
7033
7034def fish_pats(names: List<&2, String>, +pre: String) -> List<&2, String>:
7035 match names:
7036 case Nil{}: Nil{}
7037 case name <> tail: Fish.quote(String.append(pre, name)) <> fish_pats(tail, pre)
7038
7039
7040
7041def fish_dest_arms(entries: List<&2, Entry>, +pre: String,
7042 +words: List<&2, String>, +found: List<&2, Stop>) -> List<&2, String>:
7043 match entries:
7044 case Nil{}: Nil{}
7045 case Entry{name, names, about} <> tail:
7046 String.append(" case ", String.join(fish_pats(names, pre), " ")) <>
7047 String.append(" echo ", Nat.show(stop_number(found,
7048 List.append(&2, String, words, [name]), None{}))) <>
7049 fish_dest_arms(tail, pre, words, found)
7050
7051def fish_dests(stops: List<&2, Stop>, +found: List<&2, Stop>) ->
7052 List<&2, String>:
7053 match stops:
7054 case Nil{}: Nil{}
7055 case Stop{k, words, opts, builtins, subs, slots} <> tail:
7056 List.append(&2, String,
7057 fish_dest_arms(subs, String.append(Nat.show(k), " "), words, found),
7058 fish_dests(tail, found))
7059
7060def valued_names(opts: List<&2, Opt>) -> List<&2, String>:
7061 match opts:
7062 case Nil{}: Nil{}
7063 case Opt{names, OptionKind{many}, ph, comp, help} <> tail:
7064 List.append(&2, String, names, valued_names(tail))
7065 case head <> tail: valued_names(tail)
7066
7067def fish_valued_arm(k: Nat, names: List<&2, String>) -> List<&2, String>:
7068 match names:
7069 case Nil{}: []
7070 case head <> tail:
7071 [String.append(" case ", Nat.show(k)),
7072 String.append(" contains -- $argv[2] ",
7073 fish_list(head <> tail))]
7074
7075def fish_valued(stop: Stop) -> List<&2, String>:
7076 match stop:
7077 case Stop{k, words, opts, builtins, subs, slots}:
7078 fish_valued_arm(k, valued_names(opts))
7079
7080def slot_shape(found: Maybe<&2, Slot>) -> String:
7081 match found:
7082 case Some{Slot{k, hidden, required, many, rest, ph, comp}}:
7083 String.concat([Nat.show(k), " ", Bool.pick(String, rest, "1", "0")])
7084 case None{}: "0 0"
7085
7086def fish_shape_arm(shown: Bool, k: Nat, has: Bool, found: Maybe<&2, Slot>) ->
7087 List<&2, String>:
7088 match shown:
7089 case True{}:
7090 [String.append(" case ", Nat.show(k)),
7091 String.concat([" echo ", Bool.pick(String, has, "1", "0"),
7092 " ", slot_shape(found)])]
7093 case False{}: []
7094
7095def fish_shape(stop: Stop) -> List<&2, String>:
7096 match stop:
7097 case Stop{k, words, opts, builtins, subs, slots}:
7098 +has = Bool.not(List.is_empty(&2, Entry, subs))
7099 +found = variadic(slots)
7100 fish_shape_arm(Bool.or(has, Maybe.is_some(&2, Slot, found)), k, has,
7101 found)
7102
7103def fish_values(lists: List<&2, List<&2, String>>, +k: Nat) ->
7104 List<&2, String>:
7105 match lists:
7106 case Nil{}: Nil{}
7107 case values <> tail:
7108 String.append(" case ", Nat.show(k)) <>
7109 String.append(" printf '%s\\n' ", fish_list(values)) <>
7110 fish_values(tail, (k + 1n : Nat))
7111
7112def fish_values_fn(+id: String, lists: List<&2, List<&2, String>>) ->
7113 List<&2, String>:
7114 match lists:
7115 case Nil{}: []
7116 case head <> tail:
7117 List.concat(&2, String, [
7118 ["", "# Prints the values of the one_of list numbered $argv[1].",
7119 String.concat(["function ", id, "_values"]), " switch $argv[1]"],
7120 fish_values(head <> tail, 1n),
7121 [" end", "end"],
7122 ])
7123
7124def fish_flag(long: Bool, name: String) -> String:
7125 match long:
7126 case True{}: String.append("-l ", Fish.quote(String.drop(name, 2n)))
7127 case False{}: String.append("-s ", Fish.quote(String.drop(name, 1n)))
7128
7129def fish_flags(names: List<&2, String>) -> List<&2, String>:
7130 match names:
7131 case Nil{}: Nil{}
7132 case +name <> tail: fish_flag(long_name(name), name) <> fish_flags(tail)
7133
7134
7135def fish_words(+id: String, comp: Comp) -> String:
7136 match comp:
7137 case Words{list, values}:
7138 String.concat([" -a '(", id, "_values ", Nat.show(list), ")'"])
7139 case Files{}: " -F"
7140 case Dirs{}: " -a '(__fish_complete_directories)'"
7141 case Free{}: ""
7142
7143def fish_needs(comp: Comp) -> String:
7144 match comp:
7145 case Files{}: " -r"
7146 case _: " -x"
7147
7148
7149
7150def fish_kind(kind: UsageKind, +id: String, +comp: Comp) -> String:
7151 match kind:
7152 case OptionKind{many}:
7153 String.append(fish_needs(comp), fish_words(id, comp))
7154 case BareOptionKind{}: fish_words(id, comp)
7155 case _: ""
7156
7157def fish_opt_lines(opts: List<&2, Opt>, +at: String, +id: String) ->
7158 List<&2, String>:
7159 match opts:
7160 case Nil{}: Nil{}
7161 case Opt{names, kind, ph, comp, help} <> tail:
7162 String.concat([at, " n v' ", String.join(fish_flags(names), " "),
7163 fish_kind(kind, id, comp), " -d ", Fish.quote(help)]) <>
7164 fish_opt_lines(tail, at, id)
7165
7166
7167def fish_builtin_lines(opts: List<&2, Opt>, +at: String, tags: String) ->
7168 List<&2, String>:
7169 match opts:
7170 case Nil{}: Nil{}
7171 case Opt{names, kind, ph, comp, help} <> tail:
7172 String.concat([at, tags, "' ", String.join(fish_flags(names), " "),
7173 " -d ", Fish.quote(help)]) <> fish_builtin_lines(tail, at, " n")
7174
7175def fish_name_lines(names: List<&2, String>, +at: String, +about: String) ->
7176 List<&2, String>:
7177 match names:
7178 case Nil{}: Nil{}
7179 case name <> tail:
7180 String.concat([at, " n h' -a ", Fish.quote(name), " -d ", about]) <>
7181 fish_name_lines(tail, at, about)
7182
7183def fish_sub_lines(entries: List<&2, Entry>, +at: String) -> List<&2, String>:
7184 match entries:
7185 case Nil{}: Nil{}
7186 case Entry{name, names, about} <> tail:
7187 List.append(&2, String, fish_name_lines(names, at, Fish.quote(about)),
7188 fish_sub_lines(tail, at))
7189
7190def fish_slot_lines(slots: List<&2, Slot>, +at: String, +id: String) ->
7191 List<&2, String>:
7192 match slots:
7193 case Nil{}: Nil{}
7194 case Slot{k, True{}, required, many, rest, ph, comp} <> tail:
7195 fish_slot_lines(tail, at, id)
7196 case Slot{k, False{}, required, many, rest, ph, Free{}} <> tail:
7197 fish_slot_lines(tail, at, id)
7198 case Slot{k, False{}, required, many, rest, ph, comp} <> tail:
7199 String.concat([at, " p", Nat.show(k), "'", fish_words(id, comp)]) <>
7200 fish_slot_lines(tail, at, id)
7201
7202def fish_ends(subs: List<&2, Entry>, +at: String, id: String,
7203 slots: List<&2, Slot>) -> List<&2, String>:
7204 match subs:
7205 case Nil{}: fish_slot_lines(completing(slots), at, id)
7206 case head <> tail:
7207 List.append(&2, String, fish_sub_lines(head <> tail, at),
7208 [String.append(at, " n' -a 'help' -d 'Print this message or the help of the given subcommand(s)'")])
7209
7210def fish_lines(stops: List<&2, Stop>, +pre: String, +id: String) ->
7211 List<&2, String>:
7212 match stops:
7213 case Nil{}: Nil{}
7214 case Stop{k, words, opts, builtins, subs, slots} <> tail:
7215 +at = String.append(pre, Nat.show(k))
7216 List.concat(&2, String, [
7217 fish_opt_lines(opts, at, id),
7218 fish_builtin_lines(builtins, at, " n h"),
7219 fish_ends(subs, at, id, slots),
7220 fish_lines(tail, pre, id),
7221 ])
7222
7223
7224
7225def fish_engine(+id: String) -> List<&2, String>:
7226 [
7227 "",
7228 "# Succeeds when the words before the cursor reach path $argv[1] and the next",
7229 "# word can be one of $argv[2..-1]: n, an option or subcommand; h, a",
7230 "# subcommand after help; v, an option's value; p<k>, positional slot k.",
7231 String.concat(["function ", id, "_is"]),
7232 " # fish 4 deprecates -o, which fish 3 needs.",
7233 " set -l words (commandline -xpc 2>/dev/null; or commandline -opc)",
7234 " set -e words[1]",
7235 " set -l path 0",
7236 String.concat([" set -l shape (", id, "_shape 0 | string split ' ')"]),
7237 " set -l want 0",
7238 " set -l end 0",
7239 " set -l help 0",
7240 " set -l n 0",
7241 " for word in $words",
7242 " if test $want = 1",
7243 " set want 0",
7244 " else if test $end = 0; and test \"$word\" = --",
7245 " set end 1",
7246 " else if test $end = 0; and string match -q -r -- '^--.' $word",
7247 String.concat([" if not string match -q -- '*=*' $word; and ", id, "_valued $path $word"]),
7248 " set want 1",
7249 " end",
7250 " else if test $end = 0; and string match -q -r -- '^-.' $word",
7251 " set -l chars (string split '' -- (string sub -s 2 -- $word))",
7252 " for i in (seq (count $chars))",
7253 String.concat([" if ", id, "_valued $path -$chars[$i]"]),
7254 " test $i = (count $chars); and set want 1",
7255 " break",
7256 " end",
7257 " end",
7258 " else if test $shape[1] = 1",
7259 " if test $end = 1",
7260 " return 1",
7261 " else if test $help = 0; and test \"$word\" = help",
7262 " set help 1",
7263 " else",
7264 String.concat([" set path (", id, "_dest $path $word); or return 1"]),
7265 String.concat([" set shape (", id, "_shape $path | string split ' ')"]),
7266 " end",
7267 " else",
7268 " # n counts the slots filled. The variadic slot stays open, but a",
7269 " # rest() value takes every later word, so nothing more completes.",
7270 " set n (math $n + 1)",
7271 " if test $n = $shape[2]; and test $shape[3] = 1",
7272 " set end 1",
7273 " else if test $n = $shape[2]",
7274 " set n (math $n - 1)",
7275 " end",
7276 " end",
7277 " end",
7278 " test $path = $argv[1]; or return 1",
7279 " set -l tags",
7280 " if test $want = 1",
7281 " set tags v",
7282 " else",
7283 " test $end = 0; and test $help = 0; and set -a tags n",
7284 " test $end = 0; and test $help = 1; and set -a tags h",
7285 " test $shape[1] = 0; and test $help = 0; and set -a tags p(math $n + 1)",
7286 " end",
7287 " for tag in $argv[2..-1]",
7288 " contains -- $tag $tags; and return 0",
7289 " end",
7290 " return 1",
7291 "end",
7292 ]
7293
7294def fish_script(+name: String, +items: List<&2, Item>) -> String:
7295 +all = all_places(items)
7296 +lists = place_lists(all, [])
7297 +found = stops(all, 0n, lists)
7298 +id = String.append("__", ident(name))
7299 +cq = Fish.quote(name)
7300 text_lines(List.concat(&2, String, [
7301 ["# fish completion; needs fish 3.3 or later.",
7302 "",
7303 "# Prints the number of the path that subcommand $argv[2] of path $argv[1]",
7304 "# enters; paths are numbered in pre-order from 0, the root.",
7305 String.concat(["function ", id, "_dest"]),
7306 " switch \"$argv[1] $argv[2]\""],
7307 fish_dests(found, found),
7308 [" case '*'", " return 1", " end", "end", "",
7309 "# Succeeds when option $argv[2] takes the next word as its value at path",
7310 "# $argv[1].",
7311 String.concat(["function ", id, "_valued"]),
7312 " switch $argv[1]"],
7313 bash_arms(~fish_valued, found),
7314 [" case '*'", " return 1", " end", "end", "",
7315 "# Prints how path $argv[1] takes words: 1 if it has subcommands, else 0; the",
7316 "# slot of its variadic positional, or 0; 1 if that slot is rest(), else 0.",
7317 String.concat(["function ", id, "_shape"]),
7318 " switch $argv[1]"],
7319 bash_arms(~fish_shape, found),
7320 [" case '*'", " echo 0 0 0", " end", "end"],
7321 fish_values_fn(id, lists),
7322 fish_engine(id),
7323 ["",
7324 "# Option lines also hold at v, so that fish can read grouped short options",
7325 "# before a value, as in -vC DIR.",
7326 String.concat(["complete -c ", cq, " -f"])],
7327 fish_lines(found, String.concat(["complete -c ", cq, " -n '", id, "_is "]),
7328 id),
7329 ]))
7330
7331
7332
7333def roff_bold(s: String) -> String:
7334 String.concat(["\\fB", roff_text(s), "\\fR"])
7335
7336def roff_bolds(names: List<&2, String>) -> List<&2, String>:
7337 match names:
7338 case Nil{}: Nil{}
7339 case name <> tail: roff_bold(String.trim_start(name)) <> roff_bolds(tail)
7340
7341def roff_texts(texts: List<&2, String>) -> List<&2, String>:
7342 match texts:
7343 case Nil{}: Nil{}
7344 case text <> tail: roff_text(text) <> roff_texts(tail)
7345
7346def spaces_only(s: String) -> Bool:
7347 match s:
7348 case SNil{}: True{}
7349 case SCon{Chr{32}, tail}: spaces_only(tail)
7350 case SCon{head, tail}: False{}
7351
7352def line_mark(blank: Bool, line: String) -> List<&2, Maybe<&2, String>>:
7353 match blank:
7354 case True{}: []
7355 case False{}: [Some{line}]
7356
7357
7358
7359def para_marks(lines: List<&2, String>) -> List<&2, Maybe<&2, String>>:
7360 match lines:
7361 case Nil{}: Nil{}
7362 case SNil{} <> tail: None{} <> para_marks(tail)
7363 case +line <> tail:
7364 List.append(&2, Maybe<&2, String>, line_mark(spaces_only(line), line),
7365 para_marks(tail))
7366
7367
7368
7369def roff_marks(marks: List<&2, Maybe<&2, String>>, +sep: String,
7370 gap: Maybe<&2, String>) -> List<&2, String>:
7371 match marks gap:
7372 case Nil{} _: Nil{}
7373 case None{} <> tail None{}: roff_marks(tail, sep, None{})
7374 case None{} <> tail Some{g}: roff_marks(tail, sep, Some{sep})
7375 case Some{line} <> tail None{}:
7376 Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7377 case Some{line} <> tail Some{g}:
7378 g <> Roff.esc(line) <> roff_marks(tail, sep, Some{".br"})
7379
7380def roff_paras(text: String, sep: String) -> List<&2, String>:
7381 roff_marks(para_marks(String.lines(text)), sep, None{})
7382
7383def ph_end(s: String) -> Nat:
7384 match s:
7385 case SNil{}: 0n
7386 case SCon{Chr{62}, tail}: 0n
7387 case SCon{head, tail}: 1n+ph_end(tail)
7388
7389
7390def roff_ph(+s: String) -> String:
7391 +end = ph_end(s)
7392 String.concat([roff_text(String.take(s, end)), ">\\fR",
7393 roff_text(String.drop(s, (end + 1n : Nat)))])
7394
7395def roff_value(rest: String) -> String:
7396 match rest:
7397 case SCon{Chr{32}, SCon{Chr{60}, tail}}: String.append(" \\fI<", roff_ph(tail))
7398 case SCon{Chr{91}, SCon{Chr{61}, SCon{Chr{60}, tail}}}:
7399 String.append("[=\\fI<", roff_ph(tail))
7400 case SNil{}: ""
7401 case SCon{head, tail}: roff_text(SCon{head, tail})
7402
7403
7404def roff_option(+usage: String) -> String:
7405 +cut = value_cut(usage)
7406 String.append(String.join(roff_bolds(String.split(String.take(usage, cut),
7407 ',')), ", "), roff_value(String.drop(usage, cut)))
7408
7409
7410def roff_positional(+usage: String) -> String:
7411 +cut = value_cut(usage)
7412 String.concat(["\\fI", roff_text(String.take(usage, cut)), "\\fR",
7413 roff_text(String.drop(usage, cut))])
7414
7415def roff_entry(left: String, help: String, attrs: List<&2, Item>) ->
7416 List<&2, String>:
7417 ".TP" <> left <> roff_paras(right_text(True{}, help, attrs), ".IP")
7418
7419def roff_positional_entry(shown: Bool, usage: String, help: String,
7420 attrs: List<&2, Item>) -> List<&2, String>:
7421 match shown:
7422 case True{}: roff_entry(roff_positional(usage), help, attrs)
7423 case False{}: []
7424
7425def roff_positionals(items: List<&2, Item>) -> List<&2, String>:
7426 match items:
7427 case Nil{}: Nil{}
7428 case Arg{+usage, help, +attrs} <> tail:
7429 List.append(&2, String,
7430 roff_positional_entry(Bool.and(arg_pos(usage),
7431 Bool.not(has_hidden(attrs))), usage, help, attrs),
7432 roff_positionals(tail))
7433 case head <> tail: roff_positionals(tail)
7434
7435def roff_option_entry(shown: Bool, usage: String, help: String,
7436 attrs: List<&2, Item>) -> List<&2, String>:
7437 match shown:
7438 case True{}: roff_entry(roff_option(usage), help, attrs)
7439 case False{}: []
7440
7441
7442def roff_options(items: List<&2, Item>, +heading: String) -> List<&2, String>:
7443 match items:
7444 case Nil{}: Nil{}
7445 case Arg{+usage, help, +attrs} <> tail:
7446 List.append(&2, String,
7447 roff_option_entry(Bool.and(Bool.not(arg_pos(usage)),
7448 Bool.and(Bool.not(has_hidden(attrs)),
7449 String.eq(find_heading(attrs), heading))), usage, help, attrs),
7450 roff_options(tail, heading))
7451 case head <> tail: roff_options(tail, heading)
7452
7453def roff_builtins(opts: List<&2, Opt>) -> List<&2, String>:
7454 match opts:
7455 case Nil{}: Nil{}
7456 case Opt{names, kind, ph, comp, help} <> tail:
7457 ".TP" <> String.join(roff_bolds(names), ", ") <> help <>
7458 roff_builtins(tail)
7459
7460def roff_headings(titles: List<&2, String>, +args: List<&2, Item>) ->
7461 List<&2, String>:
7462 match titles:
7463 case Nil{}: Nil{}
7464 case +title <> tail:
7465 String.concat([".SS \"", roff_text(title), "\""]) <>
7466 List.append(&2, String, roff_options(args, title),
7467 roff_headings(tail, args))
7468
7469def roff_command(hidden: Bool, +page: String, name: String, about: String,
7470 children: List<&2, Item>) -> List<&2, String>:
7471 match hidden:
7472 case True{}: []
7473 case False{}:
7474 [".TP", String.append(roff_bold(String.concat([page, "-", name])), "(1)"),
7475 Roff.esc(join_filled([one_line(about),
7476 tag("aliases", find_aliases(children))]))]
7477
7478def roff_commands(items: List<&2, Item>, +page: String) -> List<&2, String>:
7479 match items:
7480 case Nil{}: Nil{}
7481 case Sub{name, about, +children} <> tail:
7482 List.append(&2, String,
7483 roff_command(has_hidden(children), page, name, about, children),
7484 roff_commands(tail, page))
7485 case head <> tail: roff_commands(tail, page)
7486
7487def roff_arg(pos: Bool, +usage: String, attrs: List<&2, Item>) -> String:
7488 match pos:
7489 case True{}: roff_positional(usage)
7490 case False{}: roff_option(arg_display(arg_info(usage, attrs)))
7491
7492def roff_env_names(names: List<&2, String>) -> List<&2, String>:
7493 match names:
7494 case Nil{}: Nil{}
7495 case name <> tail: roff_bold(name) <> roff_env_names(tail)
7496
7497
7498def roff_env_order(rest: List<&2, String>) -> String:
7499 match rest:
7500 case Nil{}: ""
7501 case head <> tail: "; the first that is set wins"
7502
7503def roff_env(hidden: Bool, names: List<&2, String>, +usage: String,
7504 attrs: List<&2, Item>) -> List<&2, String>:
7505 match hidden names:
7506 case False{} name <> +tail:
7507 [".TP", String.join(roff_env_names(name <> tail), ", "),
7508 String.concat(["Used for ", roff_arg(arg_pos(usage), usage, attrs),
7509 " when it is not given", roff_env_order(tail)])]
7510 case _ _: []
7511
7512def roff_envs(items: List<&2, Item>) -> List<&2, String>:
7513 match items:
7514 case Nil{}: Nil{}
7515 case Arg{usage, help, +attrs} <> tail:
7516 List.append(&2, String,
7517 roff_env(has_hidden(attrs), find_envs(attrs), usage, attrs),
7518 roff_envs(tail))
7519 case head <> tail: roff_envs(tail)
7520
7521def roff_examples(items: List<&2, Item>, before: List<&2, String>) ->
7522 List<&2, String>:
7523 match items:
7524 case Nil{}: Nil{}
7525 case Example{line, what} <> tail:
7526 List.concat(&2, String, [
7527 before,
7528 [Roff.esc(String.append(what, ":")), ".IP", ".nf",
7529 Roff.esc(String.append("$ ", line)), ".fi"],
7530 roff_examples(tail, [".PP"]),
7531 ])
7532 case head <> tail: roff_examples(tail, before)
7533
7534def sub_pages(items: List<&2, Item>, +page: String) -> List<&2, String>:
7535 match items:
7536 case Nil{}: Nil{}
7537 case Sub{name, about, +children} <> tail:
7538 +rest = sub_pages(tail, page)
7539 Bool.pick(List<&2, String>, has_hidden(children), rest,
7540 String.concat([page, "-", name]) <> rest)
7541 case head <> tail: sub_pages(tail, page)
7542
7543def init_words(words: List<&2, String>) -> List<&2, String>:
7544 match words:
7545 case Nil{}: Nil{}
7546 case word <> Nil{}: Nil{}
7547 case word <> tail: word <> init_words(tail)
7548
7549
7550def parent_pages(words: List<&2, String>) -> List<&2, String>:
7551 match words:
7552 case Nil{}: []
7553 case word <> Nil{}: []
7554 case word <> tail: [String.join(word <> init_words(tail), "-")]
7555
7556def roff_refs(pages: List<&2, String>) -> List<&2, String>:
7557 match pages:
7558 case Nil{}: Nil{}
7559 case page <> tail: String.append(roff_bold(page), "(1)") <> roff_refs(tail)
7560
7561def roff_see(pages: List<&2, String>) -> List<&2, String>:
7562 match pages:
7563 case Nil{}: []
7564 case head <> tail: [String.join(roff_refs(head <> tail), ", ")]
7565
7566def roff_synopsis(texts: List<&2, String>) -> String:
7567 match texts:
7568 case Nil{}: ""
7569 case path <> tail: String.join(roff_bold(path) <> roff_texts(tail), " ")
7570
7571def roff_section(title: String, lines: List<&2, String>) -> List<&2, String>:
7572 match lines:
7573 case Nil{}: []
7574 case head <> tail: String.append(".SH ", title) <> head <> tail
7575
7576def roff_view(view: View, title: String) -> String:
7577 match view:
7578 case View{+path, +about, +items, +globals, root, stuck}:
7579 +words = String.split(path, ' ')
7580 +page = String.join(words, "-")
7581 +args = List.append(&2, Item, items, globals)
7582 text_lines(List.concat(&2, String, [
7583 [String.concat([".TH \"", roff_text(String.to_upper(page)),
7584 "\" \"1\" \"\" \"", roff_text(title), "\" \"User Commands\""]),
7585 ".SH NAME",
7586 Roff.esc(String.concat([page, " - ", one_line(about)])),
7587 ".SH SYNOPSIS",
7588 roff_synopsis(segment_texts(usage_elems(path, items, globals)))],
7589 roff_section("DESCRIPTION", roff_paras(about, ".PP")),
7590 roff_section("ARGUMENTS", roff_positionals(items)),
7591 [".SH OPTIONS"],
7592 roff_options(args, ""),
7593 roff_builtins(builtin_opts(items, globals, root)),
7594 roff_headings(heading_titles(args, []), args),
7595 roff_section("COMMANDS", roff_commands(items, page)),
7596 roff_section("ENVIRONMENT", roff_envs(args)),
7597 [".SH EXIT STATUS", ".TP", "\\fB0\\fR", "Success", ".TP", "\\fB1\\fR",
7598 "Failure", ".TP", "\\fB2\\fR", "Usage error", ".TP", "\\fB70\\fR",
7599 "Internal software error"],
7600 roff_section("EXAMPLES", roff_examples(items, [])),
7601 roff_section("NOTES",
7602 roff_paras(String.join(after_texts(items), "\n\n"), ".PP")),
7603 roff_section("SEE ALSO", roff_see(List.append(&2, String,
7604 parent_pages(words), sub_pages(items, page)))),
7605 ]))
7606
7607def find_version(items: List<&2, Item>) -> String:
7608 match items:
7609 case Nil{}: ""
7610 case Version{value} <> tail: value
7611 case head <> tail: find_version(tail)
7612
7613
7614def roff(c: Cmd, path: List<&2, String>) -> String:
7615 match c:
7616 case Cmd{+name, about, +items}:
7617 roff_view(view_walk(path, View{name, about, items, [], True{}, False{}}),
7618 join_filled([name, find_version(items)]))
7619
7620
7621def script(c: Cmd, s: Shell) -> String:
7622 match c s:
7623 case Cmd{name, about, items} Bash{}: bash_script(name, items)
7624 case Cmd{name, about, items} Zsh{}: zsh_script(name, items)
7625 case Cmd{name, about, items} Fish{}: fish_script(name, items)
7626
7627
7628
7629type Outcome is Data:
7630 Shown{code: U32, out: String, err: String}
7631 Matched{m: Matches}
7632
7633
7634def sub_at(path: List<&2, String>, items: List<&2, Item>,
7635 found: Maybe<&2, SubFound>) -> Maybe<&2, SubFound>:
7636 match path:
7637 case Nil{}: found
7638 case name <> tail:
7639 +next = find_sub(items, name)
7640 sub_at(tail, sub_items(next), next)
7641
7642def builtin_name(found: Maybe<&2, SubFound>) -> Maybe<&2, String>:
7643 match found:
7644 case Some{SubFound{name, items}}:
7645 Bool.pick(Maybe<&2, String>, has_builtin(items), Some{name}, None{})
7646 case None{}: None{}
7647
7648def field_values(f: Field) -> List<&2, String>:
7649 match f:
7650 case Field{id, values, source}: values
7651
7652
7653def shell_named(+name: String) -> Shell:
7654 Bool.pick(Shell, String.eq(name, "zsh"), Zsh{},
7655 Bool.pick(Shell, String.eq(name, "fish"), Fish{}, Bash{}))
7656
7657def builtin_page(man: Bool, c: Cmd, fields: List<&2, Field>) -> String:
7658 match man:
7659 case True{}: roff(c, field_values(field_find(fields, "command")))
7660 case False{}:
7661 script(c, shell_named(head_part(field_values(field_find(fields,
7662 "shell")))))
7663
7664
7665
7666def outcome_accepted(builtin: Maybe<&2, String>, c: Cmd,
7667 path: List<&2, String>, fields: List<&2, Field>,
7668 external: Maybe<&2, ExternalArgs>) -> Outcome:
7669 match builtin:
7670 case Some{name}: Shown{0, builtin_page(String.eq(name, "man"), c, fields), ""}
7671 case None{}: Matched{Matches{c, path, fields, external}}
7672
7673def outcome_parse(result: ParseResult, +c: Cmd, h: Host,
7674 +argv: List<&2, String>) -> Outcome:
7675 match result:
7676 case Accepted{+path, fields, external}:
7677 outcome_accepted(builtin_name(sub_at(path, cmd_items(c), None{})), c,
7678 path, fields, external)
7679 case HelpRequested{path, long}:
7680 Shown{0, help_out(term(c, h, argv), view_of(c, path), long), ""}
7681 case VersionRequested{}:
7682 Shown{0, String.concat([cmd_name(c), " ", find_version(cmd_items(c)),
7683 "\n"]), ""}
7684 case SummaryRequired{}:
7685 Shown{2, "", summary_err(term(c, h, argv), view_of(c, []))}
7686 case Rejected{path, fault}:
7687 Shown{2, "", fault_err(term(c, h, argv), fault, c, path, argv)}
7688
7689def outcome_lint(found: List<&2, Problem>, +c: Cmd, +h: Host,
7690 +argv: List<&2, String>) -> Outcome:
7691 match found:
7692 case Nil{}: outcome_parse(parse(c, h, argv), c, h, argv)
7693 case p <> tail:
7694 Shown{70, "", String.append("bug: invalid command-line definition\n",
7695 problem_lines(p <> tail))}
7696
7697
7698
7699def outcome(+c: Cmd, h: Host, argv: List<&2, String>) -> Outcome:
7700 outcome_lint(problems(c), c, h, argv)
7701
7702
7703type Linted is Data:
7704 Linted{cmd: Cmd}
7705
7706def linted_found(found: List<&2, Problem>, c: Cmd) ->
7707 Result<&2, &2, List<&2, Problem>, Linted>:
7708 match found:
7709 case Nil{}: Done{Linted{c}}
7710 case p <> tail: Fail{p <> tail}
7711
7712
7713def linted(+c: Cmd) -> Result<&2, &2, List<&2, Problem>, Linted>:
7714 linted_found(problems(c), c)
7715
7716
7717def outcome_linted(l: Linted, +h: Host, +argv: List<&2, String>) -> Outcome:
7718 match l:
7719 case Linted{+c}: outcome_parse(parse(c, h, argv), c, h, argv)
7720
7721
7722
7723type Step<-A: Type> is Type:
7724 Go{value: A, term: Term}
7725 Quit{code: U32, out: String, err: String}
7726
7727
7728
7729def usage_err(t: Term, msg: String, view: View) -> String:
7730 match t view:
7731 case Term{out_color, err_color, width}
7732 View{here, about, items, globals, root, stuck}:
7733 emit(err_color, error_page([plain(msg)], [],
7734 usage_block(usage_elems(here, items, globals), page_width(width))))
7735
7736def faulted(-A: Type, f: Fault, +cmd: Cmd, path: List<&2, String>, h: Host,
7737 argv: List<&2, String>) -> Step<A>:
7738 match f:
7739 case Bug{msg}: Quit{70, "", String.concat(["bug: ", msg, "\n"])}
7740 case Usage{msg}:
7741 Quit{2, "", usage_err(term(cmd, h, argv), msg, view_of(cmd, path))}
7742
7743
7744def decoded(-A: Type, d: Dec(A), cmd: Cmd, path: List<&2, String>, h: Host,
7745 argv: List<&2, String>) -> Step<A>:
7746 match d:
7747 case Done{a}: Go{a, term(cmd, h, argv)}
7748 case Fail{f}: faulted(A, f, cmd, path, h, argv)
7749
7750def eval_step(-A: Type, decode: @+m: Matches -> Dec(A), o: Outcome, h: Host,
7751 argv: List<&2, String>) -> Step<A>:
7752 match o:
7753 case Shown{code, out, err}: Quit{code, out, err}
7754 case Matched{Matches{+cmd, +path, fields, external}}:
7755 decoded(A, decode(Matches{cmd, path, fields, external}), cmd, path, h,
7756 argv)
7757
7758
7759def eval_with(-A: Type, decode: @+m: Matches -> Dec(A), c: Cmd, +h: Host,
7760 +argv: List<&2, String>) -> Step<A>:
7761 eval_step(A, decode, outcome(c, h, argv), h, argv)
7762
7763
7764
7765def eval(~A: Type, ~decode: @+m: Matches -> Dec(A), c: Cmd, h: Host,
7766 argv: List<&2, String>) -> Step<A>:
7767 eval_with(A, decode, c, h, argv)
7768
7769
7770def eval_linted_with(-A: Type, decode: @+m: Matches -> Dec(A), l: Linted,
7771 +h: Host, +argv: List<&2, String>) -> Step<A>:
7772 eval_step(A, decode, outcome_linted(l, h, argv), h, argv)
7773
7774
7775
7776
7777def eval_linted(~A: Type, ~decode: @+m: Matches -> Dec(A), l: Linted,
7778 h: Host, argv: List<&2, String>) -> Step<A>:
7779 eval_linted_with(A, decode, l, h, argv)
7780
7781
7782def env_names(items: List<&2, Item>) -> List<&2, String>:
7783 match items:
7784 case Nil{}: Nil{}
7785 case Arg{usage, help, attrs} <> tail:
7786 List.append(&2, String, find_envs(attrs), env_names(tail))
7787 case Sub{name, about, children} <> tail:
7788 List.append(&2, String, env_names(children), env_names(tail))
7789 case head <> tail: env_names(tail)
7790
7791
7792def distinct(names: List<&2, String>, kept: List<&2, String>) ->
7793 List<&2, String>:
7794 match names:
7795 case Nil{}: kept
7796 case name <> tail: distinct(tail, append_unique(kept, name))
7797
7798def vars(c: Cmd) -> List<&2, String>:
7799 match c:
7800 case Cmd{name, about, items}:
7801 distinct(List.append(&2, String, env_names(items),
7802 ["NO_COLOR", "CLICOLOR", "CLICOLOR_FORCE", "TERM", "COLUMNS"]), [])
7803
7804
7805
7806
7807def columns_ok(+value: String) -> Bool:
7808 digits = Nat.read.trim(value)
7809 Bool.and(valid_nat(value), Bool.not(String.is_empty(digits)))
7810
7811def columns_valid(ok: Bool, value: String) -> Maybe<&2, Nat>:
7812 match ok:
7813 case True{}: Some{nat_of(value)}
7814 case False{}: None{}
7815
7816def columns_width(found: Maybe<&2, String>) -> Maybe<&2, Nat>:
7817 match found:
7818 case None{}: None{}
7819 case Some{+value}: columns_valid(columns_ok(value), value)
7820
7821
7822def tty_columns(out_tty: Bool, err_tty: Bool, out_cols: U32,
7823 err_cols: U32) -> Nat:
7824 match out_tty err_tty:
7825 case True{} _: U32.to_nat(out_cols)
7826 case False{} True{}: U32.to_nat(err_cols)
7827 case False{} False{}: 0n
7828
7829
7830def nonzero(first: Nat, then: Nat) -> Nat:
7831 match first:
7832 case 0n: then
7833 case 1n+p: 1n+p
7834
7835def width_or(found: Maybe<&2, Nat>, then: Nat) -> Nat:
7836 match found:
7837 case None{}: then
7838 case Some{width}: width
7839
7840
7841
7842def probe(+vars: List<&2, Var>, +out_tty: Bool, +err_tty: Bool,
7843 out_cols: U32, err_cols: U32) -> Host:
7844 Host{width_or(columns_width(var_value(vars, "COLUMNS")),
7845 nonzero(tty_columns(out_tty, err_tty, out_cols, err_cols), 80n)),
7846 vars, out_tty, err_tty}
7847
7848def act_code(fits: Bool, n: U32) -> (U32 & String):
7849 match fits:
7850 case True{}: (n, "")
7851 case False{}:
7852 (70, String.concat(["bug: exit code ", U32.show(n),
7853 " is out of range 0..255\n"]))
7854
7855
7856def act_exit(+n: U32) -> (U32 & String):
7857 act_code(U32.is_le(n, 255), n)
7858
7859
7860
7861
7862
7863type Ran is Data:
7864 Ran{code: U32, out: String, err: String}
7865
7866def owed_end(newline: Bool) -> String:
7867 match newline:
7868 case True{}: ""
7869 case False{}: "\n"
7870
7871def owed_after(t: String, c: Char) -> String:
7872 match t:
7873 case SNil{}: owed_end(Char.is_eq(c, '\n'))
7874 case SCon{d, u}: owed_after(u, d)
7875
7876
7877def owed(s: String) -> String:
7878 match s:
7879 case SNil{}: ""
7880 case SCon{c, t}: owed_after(t, c)
7881
7882def ran_exit(e: (U32 & String), out: String, +err: String) -> Ran:
7883 match e:
7884 case (code, bug): Ran{code, out, String.concat([err, owed(err), bug])}
7885
7886
7887
7888def ran_act(r: Ran) -> Ran:
7889 match r:
7890 case Ran{code, out, err}: ran_exit(act_exit(code), out, err)
7891
7892def ran_step(~A: Type, ~plan: @+t: Term -> A -> Ran, s: Step<A>) -> Ran:
7893 match s:
7894 case Quit{code, out, err}: Ran{code, out, err}
7895 case Go{a, t}: ran_act(plan(t, a))
7896
7897
7898
7899
7900
7901def invoke(~A: Type, ~decode: @+m: Matches -> Dec(A),
7902 ~plan: @+t: Term -> A -> Ran, c: Cmd, h: Host,
7903 argv: List<&2, String>) -> Ran:
7904 ran_step(~A, ~plan, eval(~A, ~decode, c, h, argv))
7905
7906
7907
7908
7909
7910
7911
7912type Utf8Lead is Data:
7913 NotLead{}
7914 Utf8Lead{more: Nat, bits: U32, least: U32}
7915
7916def utf8_lead(+b: U32) -> Utf8Lead:
7917 Bool.pick(Utf8Lead, U32.is_lt(b, 194), NotLead{},
7918 Bool.pick(Utf8Lead, U32.is_lt(b, 224), Utf8Lead{1n, (b - 192 : U32), 128},
7919 Bool.pick(Utf8Lead, U32.is_lt(b, 240),
7920 Utf8Lead{2n, (b - 224 : U32), 2048},
7921 Bool.pick(Utf8Lead, U32.is_lt(b, 245),
7922 Utf8Lead{3n, (b - 240 : U32), 65536}, NotLead{}))))
7923
7924def utf8_next(ok: Bool, found: Maybe<&2, U32>) -> Maybe<&2, U32>:
7925 match ok:
7926 case True{}: found
7927 case False{}: None{}
7928
7929
7930
7931def utf8_tail(rest: List<&2, U32>, more: Nat, acc: U32) -> Maybe<&2, U32>:
7932 match rest more:
7933 case Nil{} 0n: Some{acc}
7934 case Nil{} 1n+m: None{}
7935 case Con{b, t} 0n: Some{acc}
7936 case Con{+b, t} 1n+m:
7937 utf8_next(U32.is_lt((b - 128 : U32), 64),
7938 utf8_tail(t, m, (acc * 64 + (b - 128) : U32)))
7939
7940
7941def utf8_scalar(found: Maybe<&2, U32>, least: U32) -> Maybe<&2, U32>:
7942 match found:
7943 case None{}: None{}
7944 case Some{+x}:
7945 utf8_next(Bool.and(Bool.and(U32.is_ge(x, least),
7946 U32.is_le(x, 1114111)), U32.is_ge((x - 55296 : U32), 2048)), Some{x})
7947
7948
7949
7950def byte_escape(+b: U32) -> Char:
7951 Chr{Bool.pick(U32, U32.is_le(b, 255), (b + 56320 : U32), 65533)}
7952
7953
7954
7955type ArgChar is Data:
7956 ArgChar{c: Char, more: Nat}
7957
7958def arg_found(found: Maybe<&2, U32>, more: Nat, b: U32) -> ArgChar:
7959 match found:
7960 case None{}: ArgChar{byte_escape(b), 0n}
7961 case Some{x}: ArgChar{Chr{x}, more}
7962
7963def arg_seq(lead: Utf8Lead, b: U32, rest: List<&2, U32>) -> ArgChar:
7964 match lead:
7965 case NotLead{}: ArgChar{byte_escape(b), 0n}
7966 case Utf8Lead{+more, bits, least}:
7967 arg_found(utf8_scalar(utf8_tail(rest, more, bits), least), more, b)
7968
7969def arg_char_by(ascii: Bool, +b: U32, rest: List<&2, U32>) -> ArgChar:
7970 match ascii:
7971 case True{}: ArgChar{Chr{b}, 0n}
7972 case False{}: arg_seq(utf8_lead(b), b, rest)
7973
7974
7975def arg_char(+b: U32, rest: List<&2, U32>) -> ArgChar:
7976 arg_char_by(U32.is_lt(b, 128), b, rest)
7977
7978
7979def arg_text_go(bytes: List<&2, U32>, step: ArgChar) -> String:
7980 match bytes step:
7981 case Nil{} ArgChar{c, more}: SCon{c, SNil{}}
7982 case Con{b, rest} ArgChar{c, 1n+m}: arg_text_go(rest, ArgChar{c, m})
7983 case Con{b, +rest} ArgChar{c, 0n}:
7984 SCon{c, arg_text_go(rest, arg_char(b, rest))}
7985
7986
7987
7988
7989def arg_text(bytes: List<&2, U32>) -> String:
7990 match bytes:
7991 case Nil{}: SNil{}
7992 case Con{b, +rest}: arg_text_go(rest, arg_char(b, rest))
7993
7994def cont_byte(x: U32) -> U32:
7995 (128 + x % 64 : U32)
7996
7997
7998def utf8_bytes(two: Bool, three: Bool, +x: U32) -> List<&2, U32>:
7999 match two three:
8000 case True{} _: [(192 + x / 64 : U32), cont_byte(x)]
8001 case False{} True{}:
8002 [(224 + x / 4096 : U32), cont_byte((x / 64 : U32)), cont_byte(x)]
8003 case False{} False{}:
8004 [(240 + x / 262144 : U32), cont_byte((x / 4096 : U32)),
8005 cont_byte((x / 64 : U32)), cont_byte(x)]
8006
8007def code_bytes(ascii: Bool, escape: Bool, scalar: Bool, +x: U32) ->
8008 List<&2, U32>:
8009 match ascii escape scalar:
8010 case True{} _ _: [x]
8011 case False{} True{} _: [(x - 56320 : U32)]
8012 case False{} False{} True{}:
8013 utf8_bytes(U32.is_lt(x, 2048), U32.is_lt(x, 65536), x)
8014 case False{} False{} False{}: [239, 191, 189]
8015
8016
8017
8018
8019def char_bytes(c: Char) -> List<&2, U32>:
8020 match c:
8021 case Chr{+x}:
8022 code_bytes(U32.is_lt(x, 128), U32.is_lt((x - 56448 : U32), 128),
8023 Bool.and(U32.is_le(x, 1114111), U32.is_ge((x - 55296 : U32), 2048)),
8024 x)
8025
8026
8027
8028def arg_bytes(s: String) -> List<&2, U32>:
8029 match s:
8030 case SNil{}: []
8031 case SCon{c, t}: List.append(&2, U32, char_bytes(c), arg_bytes(t))