src/ez/main.bend

ezx@1.10.0.0 · 130 lines · 0 definitions

1# ez: the one binary. The line is a Shake.Cli (`L.spec`): `parse` binds the
2# command and its flags, and `get` / `on` read the bindings. How a line ends
3# is decided in ez/line.bend (`L.line`, `L.status`): a command runs, help is
4# shown and the line exits 0, or the line is misused and exits 1. This file
5# prints and exits as they say and runs the command, which is trusted under
6# EZ-TRUST-2. `IO.args()` is copied so each word can be read more than once.
7# The repo's top-level main.bend calls this `main`, and is the file that gets
8# built. Build it native (`bend main.bend -o bin/ez.bin`); interpreted, bend's
9# own CLI would take the flags meant for ez.
10import Base
11import shake@0.6.0.0/main.bend as Shake
12import ezx@1.10.0.0/src/share/args.bend as Args
13import ezx@1.10.0.0/src/ez/cmd.bend as Cmd
14import ezx@1.10.0.0/src/ez/test.bend as Test
15import ezx@1.10.0.0/src/ez/prove.bend as Prove
16import ezx@1.10.0.0/src/doctor/run.bend as DoctorRun
17import ezx@1.10.0.0/src/tool/run.bend as ToolRun
18import ezx@1.10.0.0/src/tool/world.bend as TW
19import ezx@1.10.0.0/src/share/say.bend as Say
20import ezx@1.10.0.0/src/io/file.bend as F
21import ezx@1.10.0.0/src/ez/line.bend as L
22import ezx@1.10.0.0/src/ws/run.bend as WsRun
23
24# what a subcommand runs. `L.line` never runs `CHelp`, which names no
25# command; it is here so the match is whole, and it writes the help a bare
26# `ez` would. The directories `ez check` and `ez build` write into are made
27# by those commands once they know they will start bend (`Env.dirs`), so a
28# refused one leaves the directory as it found it.
29def dispatch(sub: L.Sub, +matched: Shake.Matched, +as: List<&2, String>) -> IO(Unit):
30  match sub:
31    case L.CInit{}:
32      Cmd.init(L.get(matched, "name"), L.get(matched, "entry"),
33        L.get(matched, "description"))
34    case L.CAdd{}:
35      +target = L.get(matched, "target")
36      +ref = L.get(matched, "ref")
37      +entry = L.get(matched, "entry")
38      +rename = L.get(matched, "rename")
39      WsRun.in.member(_u => Cmd.add(target, ref, entry, rename))
40    case L.CRemove{}:
41      +name = L.get(matched, "name")
42      WsRun.in.member(_u => Cmd.remove(name))
43    case L.CLock{}:
44      +up = Shake.on(matched, "upgrade")
45      +only = L.get(matched, "package")
46      WsRun.at.root(_u => Cmd.lock(up, only))
47    case L.CFetch{}:
48      WsRun.at.root(_u => Cmd.fetch())
49    case L.CCheck{}:
50      WsRun.in.member(_u => Cmd.check())
51    case L.CBuild{}:
52      +out = L.get(matched, "out")
53      WsRun.in.member(_u => Cmd.build(out))
54    case L.CRun{}:
55      WsRun.in.member(_u => Cmd.run(as))
56    case L.CToolSync{}:
57      ToolRun.sync()
58    case L.CToolRun{}:
59      ToolRun.run(TW.Run{Args.tool.rest(as)}, L.get(matched, "target"),
60        L.get(matched, "entry"))
61    case L.CToolInstall{}:
62      ToolRun.run(TW.Install{}, L.get(matched, "target"), L.get(matched, "entry"))
63    case L.CToolUpgrade{}:
64      ToolRun.run(TW.Upgrade{}, L.get(matched, "target"), L.get(matched, "entry"))
65    case L.CPublish{}:
66      WsRun.in.member(_u => Cmd.publish())
67    case L.CTest{}:
68      Test.run()
69    case L.CProve{}:
70      Prove.run()
71    case L.CDoctor{}:
72      WsRun.at.root(_u => DoctorRun.run())
73    case L.CHelp{}:
74      IO.write(Shake.help(L.spec(), []))
75
76# `sync` with no tools is lock or install; a ledger that pins tools already
77# names the command that builds them. Anything else is the parser's own error.
78def hint.sync(is_sync: Bool) -> IO(Unit):
79  match is_sync:
80    case False{}:
81      IO.pure(Unit, Unit{})
82    case True{}:
83      do IO<Unit>:
84        m : Maybe<&2, String> <- F.read("ez.toml")
85        IO.print_err(Say.hint(Say.has.tools(F.text_of(m))))
86
87# what a line says: the command it names run, its help on stdout, or what
88# was wrong with it on stderr, with a hint for `ez sync`
89def go.say(how: L.Line, +as: List<&2, String>) -> IO(Unit):
90  match how:
91    case L.Runs{sub, +m}:
92      dispatch(sub, m, as)
93    case L.Shows{text}:
94      IO.write(text)
95    case L.Misuse{text}:
96      do IO<Unit>:
97        IO.print_err(text)
98        hint.sync(String.eq(Args.at(as, 0n), "sync"))
99
100# a status that is 0 ends nothing, and any other exits with it
101def go.exit(zero: Bool, +code: U32) -> IO(Unit):
102  match zero:
103    case True{}:
104      IO.pure(Unit, Unit{})
105    case False{}:
106      IO.die(Unit, code, "")
107
108# a status decided before any command ran, exited with; a command's own end
109# is its own
110def go.end(st: L.Status) -> IO(Unit):
111  match st:
112    case L.Exits{+code}:
113      go.exit(U32.is_eq(code, 0), code)
114    case L.Ends{}:
115      IO.pure(Unit, Unit{})
116
117# how the line ends, as `L.line` decided, and the status `L.status` gives it
118def go.line(+how: L.Line, +as: List<&2, String>) -> IO(Unit):
119  do IO<Unit>:
120    go.say(how, as)
121    go.end(L.status(how))
122
123# the line taken apart
124def go(+app: Shake.Cli, +as: List<&2, String>) -> IO(Unit):
125  go.line(L.line(app, Shake.parse(app, Args.argv.of(as))), as)
126
127def main() -> IO(Unit):
128  do IO<Unit>:
129    as : List<&2, String> <- Args.all()
130    go(L.spec(), as)