src/ez/line.bend
ezx@1.5.0.0 · 213 lines · 0 definitions
1# ez/line: the command line, taken apart before any command runs. `spec` is
2# the program, `command` names what a matched path runs, and `line` decides
3# how a parse ends: a command runs, help is shown, or the line is misused.
4# All of it is pure, so the status a line exits with before any command runs
5# is a law (EZ-OUT-1); ez/main.bend prints and exits as `line` and `status`
6# say, and runs the command a line names, whose own end decides the rest.
7import Base
8import shake@0.4.0.0/main.bend as Shake
9
10# a subcommand
11type Sub is Data:
12 CInit{}
13 CAdd{}
14 CRemove{}
15 CLock{}
16 CFetch{}
17 CCheck{}
18 CBuild{}
19 CRun{}
20 CPublish{}
21 CTest{}
22 CProve{}
23 CDoctor{}
24 CToolSync{}
25 CToolRun{}
26 CToolInstall{}
27 CToolUpgrade{}
28 CHelp{}
29
30# the program: every command, and the flags and positionals it binds
31def spec() -> Shake.Cli:
32 Shake.app("ez", "dependency tracking and builds for Bend 2", None{},
33 [],
34 [
35 Shake.sub("init", "scaffold a project: ez.toml, ignore, entry, src/",
36 [
37 Shake.pos("name", "package name", False{}, None{}, []),
38 Shake.pos("entry", "entry file", False{}, None{}, []),
39 Shake.opt("description", None{}, Some{"description"},
40 "one line: the entry's first line, which the hub shows", False{}, None{}, [])
41 ], []),
42 Shake.sub("add", "record a git or hub package",
43 [
44 Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{},
45 []),
46 Shake.pos("ref", "commit, tag or branch", False{}, None{}, []),
47 Shake.pos("entry", "entry file", False{}, None{}, []),
48 Shake.opt("rename", None{}, Some{"rename"},
49 "record it under this name, ahead of its own (a TOML bare key)",
50 False{}, None{}, [])
51 ], []),
52 Shake.sub("remove", "drop a package from the ledger",
53 [
54 Shake.pos("name", "package name", True{}, None{}, [])
55 ], []),
56 Shake.sub("lock", "resolve every import, write ez.lock.toml",
57 [
58 Shake.flag("upgrade", None{}, Some{"upgrade"},
59 "re-pin git deps and tools that moved, then write the lock"),
60 Shake.opt("package", None{}, Some{"package"},
61 "with --upgrade, only this dependency or tool", False{}, None{}, [])
62 ], []),
63 Shake.sub("fetch", "fill BEND_LIB from the lock", [], []),
64 Shake.sub("check", "check the entry, without running it", [], []),
65 Shake.sub("build", "build the entry to a native binary",
66 [
67 Shake.pos("out", "output path", False{}, None{}, [])
68 ], []),
69 Shake.sub("run", "check and run the entry", [], []),
70 Shake.sub("tool", "build or run another project's binary", [], [
71 Shake.sub("sync", "build and link every pinned tool at its lock rev",
72 [], []),
73 Shake.sub("run", "fetch, build and run the binary a repo declares",
74 [
75 Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{},
76 []),
77 Shake.opt("entry", None{}, Some{"entry"},
78 "build this file of the checkout, not the one it names", False{}, None{}, [])
79 ], []),
80 Shake.sub("install", "build the binary and link it on PATH",
81 [
82 Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{},
83 []),
84 Shake.opt("entry", None{}, Some{"entry"},
85 "build this file of the checkout, not the one it names", False{}, None{}, [])
86 ], []),
87 Shake.sub("upgrade", "rebuild when the commit moved, and refresh the link",
88 [
89 Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{},
90 []),
91 Shake.opt("entry", None{}, Some{"entry"},
92 "build this file of the checkout, not the one it names", False{}, None{}, [])
93 ], [])
94 ]),
95 Shake.sub("publish", "send the entry to the hub, under ez's 0x name",
96 [], []),
97 Shake.sub("test", "run every */tests/*.bend against its trailer", [], []),
98 Shake.sub("prove", "check every PROOF.bend: the proof gate",
99 [], []),
100 Shake.sub("doctor", "report on the toolchain and the project", [], [])
101 ])
102
103# a subcommand by its path, the words joined with `/`. A path that names no
104# command, the empty one of a bare `ez` or a group such as `tool`, is `CHelp`.
105def command(+name: String) -> Sub:
106 Bool.pick(Sub, String.eq(name, "init"), CInit{},
107 Bool.pick(Sub, String.eq(name, "add"), CAdd{},
108 Bool.pick(Sub, String.eq(name, "remove"), CRemove{},
109 Bool.pick(Sub, String.eq(name, "lock"), CLock{},
110 Bool.pick(Sub, String.eq(name, "fetch"), CFetch{},
111 Bool.pick(Sub, String.eq(name, "check"), CCheck{},
112 Bool.pick(Sub, String.eq(name, "build"), CBuild{},
113 Bool.pick(Sub, String.eq(name, "run"), CRun{},
114 Bool.pick(Sub, String.eq(name, "publish"), CPublish{},
115 Bool.pick(Sub, String.eq(name, "test"), CTest{},
116 Bool.pick(Sub, String.eq(name, "prove"), CProve{},
117 Bool.pick(Sub, String.eq(name, "doctor"), CDoctor{},
118 Bool.pick(Sub, String.eq(name, "tool/sync"), CToolSync{},
119 Bool.pick(Sub, String.eq(name, "tool/run"), CToolRun{},
120 Bool.pick(Sub, String.eq(name, "tool/install"),
121 CToolInstall{},
122 Bool.pick(Sub, String.eq(name, "tool/upgrade"),
123 CToolUpgrade{},
124 CHelp{}))))))))))))))))
125
126# whether a subcommand is the one that names no command
127def is.help(sub: Sub) -> Bool:
128 match sub:
129 case CHelp{}:
130 True{}
131 case _:
132 False{}
133
134# the path a match names, its words joined with `/`
135def path(matched: Shake.Matched) -> String:
136 String.join(Shake.path_of(matched), "/")
137
138# the Matched of the command the path names, whose bindings are the ones the
139# line made for that command (SHAKE-GET-2: `at(m, path_of(m))` follows the
140# selected path). A None here would be a Matched `parse` never answers.
141def leaf(+matched: Shake.Matched) -> Shake.Matched:
142 Maybe.default(&2, Shake.Matched, Shake.at(matched, Shake.path_of(matched)), matched)
143
144# the value the command bound to `name`, or "" when it bound none
145def get(found: Shake.Matched, +name: String) -> String:
146 Maybe.default(&2, String, Shake.get(found, name), "")
147
148# ---------------------------------------------------------------------------
149# how a line ends
150
151# a command runs, with what the line bound for it; help is shown, on stdout;
152# or the line is misused, and says so on stderr
153type Line is Data:
154 Runs{sub: Sub, matched: Shake.Matched}
155 Shows{text: String}
156 Misuse{text: String}
157
158# a line Shake would not parse: a request for help, whose path Shake has
159# already checked word by word (SHAKE-PARSE-8: `help bogus` fails as
160# `Unexpected`, not as a request for help), or a usage error
161def line.err(help: Maybe<&2, List<&2, String>>, +app: Shake.Cli, err: Shake.ParseErr) -> Line:
162 match help:
163 case Some{path}:
164 Shows{Shake.help(app, path)}
165 case None{}:
166 Misuse{Shake.err_text(app, err)}
167
168# a path that names no command: a bare `ez` is help, as a bare `cargo` is,
169# and a bare group such as `ez tool` is a line that did not say which of its
170# commands to run, as `cargo report` with no command is, so its help goes to
171# stderr and the line fails
172def line.group(bare: Bool, +app: Shake.Cli, +matched: Shake.Matched) -> Line:
173 match bare:
174 case True{}:
175 Shows{Shake.help(app, [])}
176 case False{}:
177 Misuse{Shake.help(app, Shake.path_of(matched))}
178
179# a line Shake parsed: the command its path names runs, or it names none
180def line.run(help: Bool, sub: Sub, +app: Shake.Cli, +matched: Shake.Matched) -> Line:
181 match help:
182 case True{}:
183 line.group(String.is_empty(path(matched)), app, matched)
184 case False{}:
185 Runs{sub, leaf(matched)}
186
187# how a line ends, from what Shake made of it
188def line(+app: Shake.Cli, parsed: Result<&2, &2, Shake.ParseErr, Shake.Matched>) -> Line:
189 match parsed:
190 case Fail{+e}:
191 line.err(Shake.help_path(e), app, e)
192 case Done{+m}:
193 +sub = command(path(m))
194 line.run(is.help(sub), sub, app, m)
195
196# ---------------------------------------------------------------------------
197# the status
198
199# the status a line exits with, when it is decided here, or the command's
200# own end (EZ-OUT-1)
201type Status is Data:
202 Exits{code: U32}
203 Ends{}
204
205# help exits 0, a misused line 1, and a command that runs ends as it ends
206def status(how: Line) -> Status:
207 match how:
208 case Runs{_sub, _m}:
209 Ends{}
210 case Shows{_text}:
211 Exits{0}
212 case Misuse{_text}:
213 Exits{1}