src/args.bend

bolt@1.19.0.0 · 182 lines · 0 definitions

1# src/args: bolt's command line, parsed by shake. `bolt [lint] [file..]`,
2# `bolt check file..`, `bolt lsp`, `bolt --version`. A rest positional at the root (and on
3# `lint` / `check`) takes every leftover word, so `bolt a.bend b.bend` lints
4# those files. `help` is shake's usage page, and `--help` asks for the same
5# page (`line`). IO.args gives the program as invoked first, then its
6# arguments (bend 2.0.32); `argv` drops the program. A compiled binary passes
7# its whole line through except its runtime options, which stand between
8# `+RTS` and `-RTS` (bend 2.0.38): `--threads` and `--gpu` outside that
9# bracket reach the program, and `--RTS` hands everything after it to the
10# program. `--help` reaches the program (bend 2.0.29).
11# `out` is every decision the line makes, as data: what to print, where, and
12# the exit code, or the subcommand to run and its files. main only does it.
13# Only shake's interface (main.bend) is read here; what `parse` answers for a
14# given argv is shake's guarantee (SPEC.md BOLT-TRUST-8), not bolt's.
15import Base
16import shake@0.6.0.0/main.bend as Shake
17import bolt@1.19.0.0/src/version.bend as Ver
18import bolt@1.19.0.0/src/build_rev.bend as Rev
19import bolt@1.19.0.0/src/lazy/lazy.bend as Lazy
20
21# the line at the quantity the rest of bolt reads it at (IO.args gives each
22# argument once)
23def widen(ss: List<&1, String>) -> List<&2, String>:
24  match ss:
25    case Nil{}:
26      Nil{}
27    case Con{s, t}:
28      s <> widen(t)
29
30# the arguments of IO.args's line: the program as invoked comes first, and is
31# dropped
32def args_of(ss: List<&1, String>) -> List<&2, String>:
33  match ss:
34    case Nil{}:
35      Nil{}
36    case Con{_prog, t}:
37      widen(t)
38
39# bolt's command line: its arguments, without the program
40def argv() -> IO(List<&2, String>):  # noqa: L001 IO: reads argv
41  do IO<List<&2, String>>:
42    ss : List<&1, String> <- IO.args()
43    return args_of(ss)
44
45# whether a word names a subcommand
46def is_cmd(+ww: String) -> Bool:
47  Bool.or(String.eq(ww, "lint"), Bool.or(String.eq(ww, "check"), String.eq(ww, "lsp")))
48
49# whether `--help` stands among the words before the first `--` (after it,
50# every word is a file); the search stops at either
51def asks_help(ws: List<&2, String>) -> Bool:
52  match ws:
53    case Nil{}:
54      False{}
55    case Con{+ww, rest}:
56      Lazy.and_then(Bool.not(String.eq(ww, "--")), _u => Lazy.or_else(String.eq(ww, "--help"), _v => asks_help(rest)))
57
58# the command path `--help` asks about: the first word other than `--help`,
59# when it names a subcommand, else the root's
60def help_path(ws: List<&2, String>) -> List<&2, String>:
61  match ws:
62    case Nil{}:
63      Nil{}
64    case Con{+ww, rest}:
65      Lazy.either(List<&2, String>, String.eq(ww, "--help"), _u => help_path(rest),
66        _u => Bool.pick(List<&2, String>, is_cmd(ww), [ww], []))
67
68# the line, given whether it asks for `--help`
69def line.at(ask: Bool, +ws: List<&2, String>) -> List<&2, String>:
70  match ask:
71    case True{}:
72      "help" <> help_path(ws)
73    case False{}:
74      ws
75
76# the line shake parses: one that asks for `--help` is `help` for its command
77# path, the page `bolt help` prints; any other is itself
78def line(+ws: List<&2, String>) -> List<&2, String>:
79  line.at(asks_help(ws), ws)
80
81# the files rest, optional, last among positionals
82def spec.files(+help: String) -> Shake.Arg:
83  Shake.rest("files", help, False{}, None{}, [])
84
85# the release, with the short commit in parentheses when it is present
86def version.at(+rel: String, +rev: String) -> String:
87  Bool.pick(String, String.is_empty(rev), rel, rel ++ " (" ++ rev ++ ")")
88
89# the release, and the short commit when the build has one
90def version() -> String:
91  version.at(Ver.text(), Rev.text())
92
93# bolt's program spec
94def spec() -> Shake.Cli:
95  Shake.app("bolt", "A linter, checker and language server for Bend 2.", Some{version()},
96    [Shake.flag("version", None{}, Some{"version"}, "Print the version"),
97     spec.files("Files to lint")],
98    [Shake.sub("lint", "Lint files", [spec.files("Files to lint")], []),
99     Shake.sub("check", "Check files", [spec.files("Files to check")], []),
100     Shake.sub("lsp", "Serve an editor over stdio", [], [])])
101
102# argv against bolt's spec
103def parse(av: List<&2, String>) -> Result<&2, &2, Shake.ParseErr, Shake.Matched>:
104  Shake.parse(spec(), av)
105
106# usage for a command path
107def help_text(path: List<&2, String>) -> String:
108  Shake.help(spec(), path)
109
110# a parse error as the text the binary prints
111def err_text(ee: Shake.ParseErr) -> String:
112  Shake.err_text(spec(), ee)
113
114# a subcommand
115type Cmd is Data:
116  CLint{}
117  CCheck{}
118  CLsp{}
119
120# a subcommand by its name; the empty name (no subcommand) is `lint`
121def command(+name: String) -> Cmd:
122  Bool.pick(Cmd, String.eq(name, "check"), CCheck{},
123    Bool.pick(Cmd, String.eq(name, "lsp"), CLsp{}, CLint{}))
124
125# where a line that stops puts its text
126type Stream is Data:
127  Line{}
128  Page{}
129  Err{}
130
131# what a parsed line does: print a text and exit with a code, or run a
132# subcommand over files (which then exits as that command decides)
133type Out is Data:
134  Stop{to: Stream, text: String, code: U32}
135  Go{cmd: Cmd, paths: List<&2, String>}
136
137# the files the selected command bound: each command keeps its own
138# bindings, so they are read from the Matched at the end of the selected
139# path, which is the root's own when no subcommand was selected
140def files.at(sel: Maybe<&2, Shake.Matched>, +mm: Shake.Matched) -> List<&2, String>:
141  match sel:
142    case Some{ss}:
143      Shake.get_all(ss, "files")
144    case None{}:
145      Shake.get_all(mm, "files")
146
147# the files a successful parse bound, to the command it selected
148def files(+mm: Shake.Matched) -> List<&2, String>:
149  files.at(Shake.at(mm, Shake.path_of(mm)), mm)
150
151# a successful parse: the version, or the selected command and its files
152def out.run(ask: Bool, +mm: Shake.Matched) -> Out:
153  match ask:
154    case True{}:
155      Stop{Line{}, "bolt " ++ version(), 0}
156    case False{}:
157      Go{command(String.join(Shake.path_of(mm), "/")), files(mm)}
158
159# a failed parse, given whether it asked for usage: that usage page, or the
160# error
161def out.fail.at(asked: Maybe<&2, List<&2, String>>, +ee: Shake.ParseErr) -> Out:
162  match asked:
163    case Some{path}:
164      Stop{Page{}, help_text(path), 0}
165    case None{}:
166      Stop{Err{}, err_text(ee), 1}
167
168# a failed parse: help text, or an error
169def out.fail(+ee: Shake.ParseErr) -> Out:
170  out.fail.at(Shake.help_path(ee), ee)
171
172# a successful parse, read once for the version flag
173def out.done(+mm: Shake.Matched) -> Out:
174  out.run(Shake.on(mm, "version"), mm)
175
176# what a parse result does
177def out(rr: Result<&2, &2, Shake.ParseErr, Shake.Matched>) -> Out:
178  match rr:
179    case Fail{e}:
180      out.fail(e)
181    case Done{m}:
182      out.done(m)