src/ez/line.bend source
src/ez/line.bend on the hub · documented module
# ez/line: the command line, taken apart before any command runs. `spec` is# the program, `command` names what a matched path runs, and `line` decides# how a parse ends: a command runs, help is shown, or the line is misused.# All of it is pure, so the status a line exits with before any command runs# is a law (EZ-OUT-1); ez/main.bend prints and exits as `line` and `status`# say, and runs the command a line names, whose own end decides the rest.import Baseimport 0xcab8a7a189cec2b51e8db0484f69c593/main.bend as Shake# a subcommandtype Sub is Data: CInit{} CAdd{} CRemove{} CLock{} CFetch{} CCheck{} CBuild{} CRun{} CPublish{} CTest{} CProve{} CDoctor{} CToolSync{} CToolRun{} CToolInstall{} CToolUpgrade{} CHelp{}# the program: every command, and the flags and positionals it bindsdef spec() -> Shake.Cli: Shake.app("ez", "dependency tracking and builds for Bend 2", None{}, [], [ Shake.sub("init", "scaffold a project: ez.toml, ignore, entry, src/", [ Shake.pos("name", "package name", False{}, None{}, []), Shake.pos("entry", "entry file", False{}, None{}, []), Shake.opt("description", None{}, Some{"description"}, "one line: the entry's first line, which the hub shows", False{}, None{}, []) ], []), Shake.sub("add", "record a git or hub package", [ Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{}, []), Shake.pos("ref", "commit, tag or branch", False{}, None{}, []), Shake.pos("entry", "entry file", False{}, None{}, []), Shake.opt("rename", None{}, Some{"rename"}, "record it under this name, ahead of its own (a TOML bare key)", False{}, None{}, []) ], []), Shake.sub("remove", "drop a package from the ledger", [ Shake.pos("name", "package name", True{}, None{}, []) ], []), Shake.sub("lock", "resolve every import, write ez.lock.toml", [ Shake.flag("upgrade", None{}, Some{"upgrade"}, "re-pin git deps and tools that moved, then write the lock"), Shake.opt("package", None{}, Some{"package"}, "with --upgrade, only this dependency or tool", False{}, None{}, []) ], []), Shake.sub("fetch", "fill BEND_LIB from the lock", [], []), Shake.sub("check", "check the entry, without running it", [], []), Shake.sub("build", "build the entry to a native binary", [ Shake.pos("out", "output path", False{}, None{}, []) ], []), Shake.sub("run", "check and run the entry", [], []), Shake.sub("tool", "build or run another project's binary", [], [ Shake.sub("sync", "build and link every pinned tool at its lock rev", [], []), Shake.sub("run", "fetch, build and run the binary a repo declares", [ Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{}, []), Shake.opt("entry", None{}, Some{"entry"}, "build this file of the checkout, not the one it names", False{}, None{}, []) ], []), Shake.sub("install", "build the binary and link it on PATH", [ Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{}, []), Shake.opt("entry", None{}, Some{"entry"}, "build this file of the checkout, not the one it names", False{}, None{}, []) ], []), Shake.sub("upgrade", "rebuild when the commit moved, and refresh the link", [ Shake.pos("target", "owner/repo, git url, path, or name@version", True{}, None{}, []), Shake.opt("entry", None{}, Some{"entry"}, "build this file of the checkout, not the one it names", False{}, None{}, []) ], []) ]), Shake.sub("publish", "send the entry to the hub, under ez's 0x name", [], []), Shake.sub("test", "run every */tests/*.bend against its trailer", [], []), Shake.sub("prove", "check every PROOF.bend: the proof gate", [], []), Shake.sub("doctor", "report on the toolchain and the project", [], []) ])# a subcommand by its path, the words joined with `/`. A path that names no# command, the empty one of a bare `ez` or a group such as `tool`, is `CHelp`.def command(+name: String) -> Sub: Bool.pick(Sub, String.eq(name, "init"), CInit{}, Bool.pick(Sub, String.eq(name, "add"), CAdd{}, Bool.pick(Sub, String.eq(name, "remove"), CRemove{}, Bool.pick(Sub, String.eq(name, "lock"), CLock{}, Bool.pick(Sub, String.eq(name, "fetch"), CFetch{}, Bool.pick(Sub, String.eq(name, "check"), CCheck{}, Bool.pick(Sub, String.eq(name, "build"), CBuild{}, Bool.pick(Sub, String.eq(name, "run"), CRun{}, Bool.pick(Sub, String.eq(name, "publish"), CPublish{}, Bool.pick(Sub, String.eq(name, "test"), CTest{}, Bool.pick(Sub, String.eq(name, "prove"), CProve{}, Bool.pick(Sub, String.eq(name, "doctor"), CDoctor{}, Bool.pick(Sub, String.eq(name, "tool/sync"), CToolSync{}, Bool.pick(Sub, String.eq(name, "tool/run"), CToolRun{}, Bool.pick(Sub, String.eq(name, "tool/install"), CToolInstall{}, Bool.pick(Sub, String.eq(name, "tool/upgrade"), CToolUpgrade{}, CHelp{}))))))))))))))))# whether a subcommand is the one that names no commanddef is.help(sub: Sub) -> Bool: match sub: case CHelp{}: True{} case _: False{}# the path a match names, its words joined with `/`def path(matched: Shake.Matched) -> String: String.join(Shake.path_of(matched), "/")# the Matched of the command the path names, whose bindings are the ones the# line made for that command (SHAKE-GET-2: `at(m, path_of(m))` follows the# selected path). A None here would be a Matched `parse` never answers.def leaf(+matched: Shake.Matched) -> Shake.Matched: Maybe.default(&2, Shake.Matched, Shake.at(matched, Shake.path_of(matched)), matched)# the value the command bound to `name`, or "" when it bound nonedef get(found: Shake.Matched, +name: String) -> String: Maybe.default(&2, String, Shake.get(found, name), "")# ---------------------------------------------------------------------------# how a line ends# a command runs, with what the line bound for it; help is shown, on stdout;# or the line is misused, and says so on stderrtype Line is Data: Runs{sub: Sub, matched: Shake.Matched} Shows{text: String} Misuse{text: String}# a line Shake would not parse: a request for help, whose path Shake has# already checked word by word (SHAKE-PARSE-8: `help bogus` fails as# `Unexpected`, not as a request for help), or a usage errordef line.err(help: Maybe<&2, List<&2, String>>, +app: Shake.Cli, err: Shake.ParseErr) -> Line: match help: case Some{path}: Shows{Shake.help(app, path)} case None{}: Misuse{Shake.err_text(app, err)}# a path that names no command: a bare `ez` is help, as a bare `cargo` is,# and a bare group such as `ez tool` is a line that did not say which of its# commands to run, as `cargo report` with no command is, so its help goes to# stderr and the line failsdef line.group(bare: Bool, +app: Shake.Cli, +matched: Shake.Matched) -> Line: match bare: case True{}: Shows{Shake.help(app, [])} case False{}: Misuse{Shake.help(app, Shake.path_of(matched))}# a line Shake parsed: the command its path names runs, or it names nonedef line.run(help: Bool, sub: Sub, +app: Shake.Cli, +matched: Shake.Matched) -> Line: match help: case True{}: line.group(String.is_empty(path(matched)), app, matched) case False{}: Runs{sub, leaf(matched)}# how a line ends, from what Shake made of itdef line(+app: Shake.Cli, parsed: Result<&2, &2, Shake.ParseErr, Shake.Matched>) -> Line: match parsed: case Fail{+e}: line.err(Shake.help_path(e), app, e) case Done{+m}: +sub = command(path(m)) line.run(is.help(sub), sub, app, m)# ---------------------------------------------------------------------------# the status# the status a line exits with, when it is decided here, or the command's# own end (EZ-OUT-1)type Status is Data: Exits{code: U32} Ends{}# help exits 0, a misused line 1, and a command that runs ends as it endsdef status(how: Line) -> Status: match how: case Runs{_sub, _m}: Ends{} case Shows{_text}: Exits{0} case Misuse{_text}: Exits{1}