src/ez/line.bend

ezx@1.8.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.6.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}