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. 7 import Base 8 import 0xa383b26a5a0ef35663b3615d7aedf9e3/main.bend as Shake 9 10 # a subcommand 11 type 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 31 def 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`. 105 def 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 127 def 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 `/` 135 def 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. 141 def 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 145 def 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 153 type 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 161 def 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 172 def 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 180 def 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 188 def 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) 201 type 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 206 def 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}