args.bend
cswap-bend-cli@0.1.0.0 · 447 lines · 76 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/usage/text.bend as UText
3
4
5
6
7
8type Parsed is Data:
9 PHelp{}
10 PVersion{}
11 PList{json: Bool, token_status: Bool}
12 PStatus{json: Bool}
13 PAdd{slot: Maybe<&2, U32>, alias: Maybe<&2, String>}
14 PAddToken{token: Maybe<&2, String>, email: Maybe<&2, String>, slot: Maybe<&2, U32>}
15 PRemove{ident: String}
16 PImportUsage{path: String, hold: Maybe<&2, U32>}
17 PConfig{args: List<&2, String>, json: Bool}
18 PProfile{args: List<&2, String>, json: Bool}
19 PSwitch{ident: Maybe<&2, String>, force: Bool, strategy: Maybe<&2, String>, model: Maybe<&2, String>, json: Bool}
20 PTui{args: List<&2, String>}
21 PAuto{args: List<&2, String>}
22 PDeferred{name: String}
23 PBad{msg: String}
24
25
26
27type Opt is Data:
28 Opt{name: String, value: String}
29
30type KV is Data:
31 KV{k: String, v: String}
32
33def verbs() -> List<&2, KV>:
34 [KV{"help", "help"}, KV{"--help", "help"}, KV{"-h", "help"}, KV{"--version", "version"}, KV{"list", "list"}, KV{"ls", "list"}, KV{"--list", "list"},
35 KV{"status", "status"}, KV{"--status", "status"}, KV{"add", "add"}, KV{"--add-account", "add"}, KV{"add-token", "add-token"}, KV{"--add-token", "add-token"},
36 KV{"remove", "remove"}, KV{"rm", "remove"}, KV{"--remove-account", "remove"}, KV{"import-usage", "import-usage"}, KV{"--import-usage", "import-usage"},
37 KV{"config", "config"}, KV{"profile", "profile"}, KV{"switch", "switch"}, KV{"--switch", "switch"}, KV{"--switch-to", "switch"}, KV{"auto", "auto"},
38 KV{"run", "run"}, KV{"map", "map"}, KV{"unmap", "unmap"}, KV{"unclaimed", "unclaimed"}, KV{"alias", "alias"}, KV{"swap", "swap"}, KV{"move", "move"},
39 KV{"disable", "disable"}, KV{"enable", "enable"}, KV{"export", "export"}, KV{"import", "import"}, KV{"purge", "purge"}, KV{"upgrade", "upgrade"},
40 KV{"update", "upgrade"}, KV{"tui", "tui"}, KV{"watch", "watch"}, KV{"menubar", "menubar"}]
41
42def verb.step(hit: Bool, v: String, rest: Maybe<&2, String>) -> Maybe<&2, String>:
43 match hit:
44 case True{}:
45 Some{v}
46 case False{}:
47 rest
48
49def verb.find(+t: String, vs: List<&2, KV>) -> Maybe<&2, String>:
50 match vs:
51 case Nil{}:
52 None{}
53 case Con{KV{k, v}, rest}:
54 verb.step(String.eq(k, t), v, verb.find(t, rest))
55
56def verb.pick(+t: String) -> String:
57 Maybe.default(&2, String, verb.find(t, verbs()), "")
58
59
60def verb_of(+t: String) -> String:
61 verb.pick(t)
62
63def valued() -> List<&2, String>:
64 ["--slot", "--email", "--alias", "--hold", "--strategy", "--model", "--account"]
65
66def switches() -> List<&2, String>:
67 ["--json", "--token-status", "--debug", "--force", "--full", "--install-service", "--uninstall-service", "--service-status"]
68
69def has_str(+x: String, xs: List<&2, String>) -> Bool:
70 match xs:
71 case Nil{}:
72 False{}
73 case Con{h, t}:
74 String.eq(h, x) || has_str(x, t)
75
76def is_flag(+t: String) -> Bool:
77 String.starts_with(t, "-") && Nat.is_gt(String.length(t), 1n)
78
79def split_eq.go(ps: List<&2, String>) -> String & Maybe<&2, String>:
80 match ps:
81 case Con{k, Nil{}}:
82 (k, None{})
83 case Con{k, Con{v, rest}}:
84 (k, Some{String.join(v <> rest, "=")})
85 case Nil{}:
86 ("", None{})
87
88
89def split_eq(+t: String) -> String & Maybe<&2, String>:
90 split_eq.go(String.split(t, '='))
91
92def opt.has(+name: String, os: List<&2, Opt>) -> Bool:
93 match os:
94 case Nil{}:
95 False{}
96 case Con{Opt{n, v}, t}:
97 String.eq(n, name) || opt.has(name, t)
98
99def opt.pick(hit: Bool, v: String, rest: Maybe<&2, String>) -> Maybe<&2, String>:
100 match hit:
101 case True{}:
102 Some{v}
103 case False{}:
104 rest
105
106def opt.val(+name: String, os: List<&2, Opt>) -> Maybe<&2, String>:
107 match os:
108 case Nil{}:
109 None{}
110 case Con{Opt{n, v}, t}:
111 opt.pick(String.eq(n, name), v, opt.val(name, t))
112
113def pos.pick(hit: Bool, v: String, rest: List<&2, String>) -> List<&2, String>:
114 match hit:
115 case True{}:
116 v <> rest
117 case False{}:
118 rest
119
120def positionals(os: List<&2, Opt>) -> List<&2, String>:
121 match os:
122 case Nil{}:
123 []
124 case Con{Opt{n, v}, t}:
125 pos.pick(String.is_empty(n), v, positionals(t))
126
127def first_of(xs: List<&2, String>) -> Maybe<&2, String>:
128 match xs:
129 case Con{h, t}:
130 Some{h}
131 case Nil{}:
132 None{}
133
134def prog() -> String:
135 "cswap"
136
137def usage_line() -> String:
138 "usage: cswap <command> [args] [options]"
139
140def flag.msg(on: Bool, msg: String, rest: String) -> String:
141 match on:
142 case True{}:
143 msg
144 case False{}:
145 rest
146
147def first.pick(empty: Bool, rest: String, h: String) -> String:
148 match empty:
149 case True{}:
150 rest
151 case False{}:
152 h
153
154def first_err(msgs: List<&2, String>) -> String:
155 match msgs:
156 case Nil{}:
157 ""
158 case Con{+h, t}:
159 first.pick(String.is_empty(h), first_err(t), h)
160
161def err_if(on: Bool, msg: String) -> String:
162 flag.msg(on, msg, "")
163
164def pos_err(+vb: String, +ps: List<&2, String>, +max: Nat) -> String:
165 err_if(Nat.is_gt(List.length(&2, String, ps), max), "unrecognized arguments: " ++ String.join(List.drop(&2, String, ps, max), " "))
166
167def strategy_err(m: Maybe<&2, String>) -> String:
168 match m:
169 case None{}:
170 ""
171 case Some{+v}:
172 err_if(Bool.not(has_str(v, ["best", "next-available"])), "argument --strategy: invalid choice: '" ++ v ++ "' (choose from 'best', 'next-available')")
173
174def common_errs(+vb: String, +os: List<&2, Opt>) -> List<&2, String>:
175 [err_if(opt.has("--token-status", os) && Bool.not(String.eq(vb, "list")), "--token-status can only be used with 'list'"),
176 err_if(opt.has("--json", os) && Bool.not(has_str(vb, ["list", "status", "switch"])), "--json can only be used with 'list', 'status', or 'switch'"),
177 err_if(opt.has("--json", os) && opt.has("--token-status", os), "--token-status cannot be combined with --json"),
178 err_if(opt.has("--strategy", os) && Bool.not(String.eq(vb, "switch")), "--strategy can only be used with bare 'switch'"),
179 err_if(opt.has("--model", os) && Bool.not(opt.has("--strategy", os)), "--model can only be used with 'switch --strategy best' or 'switch --strategy next-available'"),
180 err_if(opt.has("--slot", os) && Bool.not(has_str(vb, ["add", "add-token"])), "--slot can only be used with 'add' or 'add-token'"),
181 err_if(opt.has("--email", os) && Bool.not(String.eq(vb, "add-token")), "--email can only be used with 'add-token'"),
182 err_if(opt.has("--account", os) && Bool.not(String.eq(vb, "export")), "--account can only be used with 'export'"),
183 err_if(opt.has("--alias", os) && Bool.not(String.eq(vb, "add")), "--alias can only be used with 'add'"),
184 err_if(opt.has("--force", os) && Bool.not(has_str(vb, ["import", "switch"])), "--force can only be used with 'import' or 'switch <num|email>'"),
185 err_if(opt.has("--full", os) && Bool.not(String.eq(vb, "export")), "--full can only be used with 'export'"),
186 err_if(opt.has("--hold", os) && Bool.not(String.eq(vb, "import-usage")), "--hold can only be used with 'import-usage'"),
187 err_if(opt.has("--strategy", os) && String.eq(vb, "switch") && Bool.not(List.is_empty(&2, String, positionals(os))), "--strategy can only be used with bare 'switch'"),
188 err_if(opt.has("--force", os) && String.eq(vb, "switch") && List.is_empty(&2, String, positionals(os)), "--force can only be used with 'import' or 'switch <num|email>'"),
189 strategy_err(opt.val("--strategy", os)),
190 err_if((opt.has("--install-service", os) || opt.has("--uninstall-service", os) || opt.has("--service-status", os)) && Bool.not(String.eq(vb, "menubar")),
191 "--install-service, --uninstall-service and --service-status can only be used with 'menubar'")]
192
193type Slot is Data:
194 SlotOk{n: Maybe<&2, U32>}
195 SlotBad{msg: String}
196
197def slot.read(r: Maybe<&2, U32>, +s: String) -> Slot:
198 match r:
199 case Some{n}:
200 SlotOk{Some{n}}
201 case None{}:
202 SlotBad{"argument --slot: invalid int value: '" ++ s ++ "'"}
203
204def slot.of(m: Maybe<&2, String>) -> Slot:
205 match m:
206 case None{}:
207 SlotOk{None{}}
208 case Some{+s}:
209 slot.read(U32.read(s), s)
210
211type Hold is Data:
212 HoldOk{secs: Maybe<&2, U32>}
213 HoldBad{msg: String}
214
215def hold.bad() -> Hold:
216 HoldBad{"--hold must be a non-negative number of seconds"}
217
218def hold.int(r: Maybe<&2, U32>) -> Hold:
219 match r:
220 case Some{n}:
221 HoldOk{Some{n}}
222 case None{}:
223 hold.bad()
224
225def hold.parts(ps: List<&2, String>) -> Hold:
226 match ps:
227 case Con{i, t}:
228 hold.int(U32.read(i))
229 case Nil{}:
230 hold.bad()
231
232def hold.norm(m: Maybe<&2, String>) -> Hold:
233 match m:
234 case Some{t}:
235 hold.parts(String.split(t, '.'))
236 case None{}:
237 hold.bad()
238
239def hold.of(m: Maybe<&2, String>) -> Hold:
240 match m:
241 case None{}:
242 HoldOk{None{}}
243 case Some{s}:
244 hold.norm(UText.float(s))
245
246def bad_text(msg: String) -> String:
247 usage_line() ++ "\n" ++ prog() ++ ": error: " ++ msg
248
249def bad(msg: String) -> Parsed:
250 PBad{bad_text(msg)}
251
252def sel.p(c: Bool, a: Parsed, b: Parsed) -> Parsed:
253 match c:
254 case True{}:
255 a
256 case False{}:
257 b
258
259def with_err(+e: String, p: Parsed) -> Parsed:
260 sel.p(String.is_empty(e), p, bad(e))
261
262def build.add(sl: Slot, alias: Maybe<&2, String>) -> Parsed:
263 match sl:
264 case SlotOk{n}:
265 PAdd{n, alias}
266 case SlotBad{msg}:
267 bad(msg)
268
269def build.token(sl: Slot, token: Maybe<&2, String>, email: Maybe<&2, String>) -> Parsed:
270 match sl:
271 case SlotOk{n}:
272 PAddToken{token, email, n}
273 case SlotBad{msg}:
274 bad(msg)
275
276def build.import(h: Hold, path: Maybe<&2, String>) -> Parsed:
277 match h path:
278 case HoldBad{msg} _:
279 bad(msg)
280 case HoldOk{secs} Some{p}:
281 PImportUsage{p, secs}
282 case HoldOk{secs} None{}:
283 bad("argument --import-usage: expected one argument")
284
285def build.remove(m: Maybe<&2, String>) -> Parsed:
286 match m:
287 case Some{i}:
288 PRemove{i}
289 case None{}:
290 bad("argument --remove-account: expected one argument")
291
292def max_pos(one: Bool) -> Nat:
293 match one:
294 case True{}:
295 1n
296 case False{}:
297 0n
298
299def arity(+vb: String, +ps: List<&2, String>) -> String:
300 pos_err(vb, ps, max_pos(has_str(vb, ["add-token", "remove", "import-usage", "switch"])))
301
302
303type Alt is Data:
304 Alt{key: String, val: Parsed}
305
306def choose.pick(hit: Bool, v: Parsed, rest: Parsed) -> Parsed:
307 match hit:
308 case True{}:
309 v
310 case False{}:
311 rest
312
313def choose(+vb: String, alts: List<&2, Alt>, dflt: Parsed) -> Parsed:
314 match alts:
315 case Nil{}:
316 dflt
317 case Con{Alt{k, v}, t}:
318 choose.pick(String.eq(k, vb), v, choose(vb, t, dflt))
319
320def build.cmd(+vb: String, +os: List<&2, Opt>, +ps: List<&2, String>) -> Parsed:
321 choose(vb, [
322 Alt{"help", PHelp{}}, Alt{"version", PVersion{}},
323 Alt{"list", PList{opt.has("--json", os), opt.has("--token-status", os)}},
324 Alt{"status", PStatus{opt.has("--json", os)}},
325 Alt{"add", build.add(slot.of(opt.val("--slot", os)), opt.val("--alias", os))},
326 Alt{"add-token", build.token(slot.of(opt.val("--slot", os)), first_of(ps), opt.val("--email", os))},
327 Alt{"remove", build.remove(first_of(ps))},
328 Alt{"switch", PSwitch{first_of(ps), opt.has("--force", os), opt.val("--strategy", os), opt.val("--model", os), opt.has("--json", os)}},
329 Alt{"import-usage", build.import(hold.of(opt.val("--hold", os)), first_of(ps))}],
330 PDeferred{vb})
331
332def build.go(+vb: String, +os: List<&2, Opt>, +ps: List<&2, String>) -> Parsed:
333 with_err(arity(vb, ps), build.cmd(vb, os, ps))
334
335def build.verb(+vb: String, +os: List<&2, Opt>) -> Parsed:
336 build.go(vb, os, positionals(os))
337
338def build(+vb: String, +os: List<&2, Opt>) -> Parsed:
339 with_err(first_err(common_errs(vb, os)), build.verb(vb, os))
340
341def not_flag(+t: String) -> Bool:
342 Bool.not(String.eq(t, "--json") || String.eq(t, "--debug"))
343
344def config.keep(keep: Bool, h: String, rest: List<&2, String>) -> List<&2, String>:
345 match keep:
346 case True{}:
347 h <> rest
348 case False{}:
349 rest
350
351def config_args(ts: List<&2, String>) -> List<&2, String>:
352 match ts:
353 case Nil{}:
354 []
355 case Con{+h, t}:
356 config.keep(not_flag(h), h, config_args(t))
357
358type Scan is Data:
359 Scan{opts: List<&2, Opt>, pending: String, err: String}
360
361def sel.s(c: Bool, a: String, b: String) -> String:
362 match c:
363 case True{}:
364 a
365 case False{}:
366 b
367
368def first_msg(+err: String, +name: String) -> String:
369 sel.s(String.is_empty(err), "unrecognized arguments: " ++ name, err)
370
371def scan.fail(err: String, +name: String) -> String:
372 first_msg(err, name)
373
374def scan.flag2(valued: Bool, switch: Bool, +name: String, inline: Maybe<&2, String>, opts: List<&2, Opt>, err: String) -> Scan:
375 match valued switch inline:
376 case True{} _ Some{v}:
377 Scan{Opt{name, v} <> opts, "", err}
378 case True{} _ None{}:
379 Scan{opts, name, err}
380 case False{} True{} _:
381 Scan{Opt{name, ""} <> opts, "", err}
382 case False{} False{} _:
383 Scan{opts, "", scan.fail(err, name)}
384
385def scan.flag(kv: String & Maybe<&2, String>, opts: List<&2, Opt>, err: String) -> Scan:
386 (+k, v) = kv
387 scan.flag2(has_str(k, valued()), has_str(k, switches()), k, v, opts, err)
388
389def scan.free(flag: Bool, +t: String, opts: List<&2, Opt>, err: String) -> Scan:
390 match flag:
391 case True{}:
392 scan.flag(split_eq(t), opts, err)
393 case False{}:
394 Scan{Opt{"", t} <> opts, "", err}
395
396def scan.step2(pending: Bool, +h: String, +name: String, opts: List<&2, Opt>, err: String) -> Scan:
397 match pending:
398 case True{}:
399 Scan{Opt{name, h} <> opts, "", err}
400 case False{}:
401 scan.free(is_flag(h), h, opts, err)
402
403def scan.step(+h: String, sc: Scan) -> Scan:
404 match sc:
405 case Scan{opts, +pending, err}:
406 scan.step2(Bool.not(String.is_empty(pending)), h, pending, opts, err)
407
408def scan.go(ts: List<&2, String>, sc: Scan) -> Scan:
409 match ts:
410 case Nil{}:
411 sc
412 case Con{h, t}:
413 scan.go(t, scan.step(h, sc))
414
415def scan.dangling(+pending: String, err: String) -> String:
416 sel.s(String.is_empty(pending), err, "argument " ++ pending ++ ": expected one argument")
417
418def scan.end(sc: Scan) -> Scan:
419 match sc:
420 case Scan{opts, pending, err}:
421 Scan{List.reverse(&2, Opt, opts), "", scan.dangling(pending, err)}
422
423def scan(ts: List<&2, String>) -> Scan:
424 scan.end(scan.go(ts, Scan{[], "", ""}))
425
426def scan.done(+err: String, +vb: String, +opts: List<&2, Opt>) -> Parsed:
427 sel.p(String.is_empty(err), build(vb, opts), bad(err))
428
429def parse.scan(+vb: String, sc: Scan) -> Parsed:
430 match sc:
431 case Scan{opts, pending, err}:
432 scan.done(err, vb, opts)
433
434def parse.verb(+vb: String, +rest: List<&2, String>) -> Parsed:
435 sel.p(String.eq(vb, "tui") || String.eq(vb, "watch"), PTui{rest}, sel.p(String.eq(vb, "auto"), PAuto{rest}, sel.p(String.eq(vb, "config"), PConfig{config_args(rest), has_str("--json", rest)}, sel.p(String.eq(vb, "profile"), PProfile{config_args(rest), has_str("--json", rest)}, sel.p(has_str(vb, ["help", "version", "list", "status", "add", "add-token", "remove", "import-usage", "switch"]), parse.scan(vb, scan(rest)), PDeferred{vb})))))
436
437def parse.first(+vb: String, +h: String, +rest: List<&2, String>) -> Parsed:
438 sel.p(String.is_empty(vb), bad("unrecognized arguments: " ++ h), parse.verb(vb, rest))
439
440
441
442def parse(ts: List<&2, String>) -> Parsed:
443 match ts:
444 case Nil{}:
445 bad("no command given - try 'cswap help'")
446 case Con{+h, rest}:
447 parse.first(verb_of(h), h, rest)