CLI argument parser for Bend 2. A program
spec is Bend data: parse binds flags, options, positionals, and nested
commands; help writes usage.
With Bend alone there is nothing to
install: import shake by its hub name, at v0.4.0, and bend fetches it from
the hub into ~/.bend/lib on the first run.
import shake@0.4.0.0/main.bend as Shake
shake@0.4.0.0 resolves to 0xcab8a7a189cec2b51e8db0484f69c593;
import 0xcab8a7a189cec2b51e8db0484f69c593/main.bend pins it by content.
shake needs bend 2.0.32 or later (see below), and is built and checked on
bend 2.0.34.
Or with ez, which records the
package in ez.toml (ez init makes one):
ez add Emerging-Patterns/shake
A spec, a parse, and a read. Shake.argv() is the command line without
the program name; parse answers Done with what the words bound, or
Fail with why they could not be bound:
import shake@0.4.0.0/main.bend as Shake
def spec() -> Shake.Cli:
Shake.app("hi", "Say hello.", None{},
[Shake.opt("name", Some{"n"}, Some{"name"}, "Who to greet", False{},
Some{"world"}, [])],
[])
def greet(got: Result<&2, &2, Shake.ParseErr, Shake.Matched>) -> String:
match got:
case Done{mm}:
"hello " ++ Maybe.default(&2, String, Shake.get(mm, "name"), "")
case Fail{ee}:
Shake.err_text(spec(), ee)
def main() -> IO(Unit):
do IO<Unit>:
av : List<&2, String> <- Shake.argv()
IO.print(greet(Shake.parse(spec(), av)))
bend hi.bend -- --name Ada prints hello Ada, and so does a compiled
hi.bin --name Ada. The same code works in an ez project, where
ez add has put shake in the ledger.
On bend 2.0.32 and later, IO.args() starts with the program as invoked
(hi.bin, or hi.bend when interpreted), so a program that passes
IO.args() straight to parse must drop that first word; Shake.argv()
does it for you. shake needs bend 2.0.32 or later for that reason.
main.bend is the whole interface: the types (Shake.Cli, Shake.Sub,
Shake.Arg, Shake.Matched, Shake.ParseErr, Shake.SpecErr,
Shake.Var), the builders (app, sub, flag, opt, many, pos,
rest, env), check and spec_err_text, parse and parse_env, the
readers (get, get_all, on, sub_name, sub_of, at, path_of),
help, err_text, help_path, err_path, suggestion, argv and
env_vars.
Everything under src/ is
internal and may change in any release; import only main.bend.
SPEC.md lists what shake guarantees, and which of it is proved.
check(spec) lists every way a spec contradicts itself (a rest positional
that is not the last, a repeated name or spelling, a subcommand named
help, a default outside its choices, a required positional after an
optional one). parse does not call it; the guarantees hold for a spec it
passes, so check yours once, at start or in your own laws.
parse fails with a request for help on tool help, tool help <command>,
tool --help or tool <command> --help (a command that declares its own
long help gets --help as that argument instead): help_path gives its
command path, for help to print, and is None for every other error,
which err_text describes. For a mistyped long option, err_text adds
clap's tip (tip: a similar argument exists: '--name' for --nmae), and
suggestion gives that long option word on its own. It picks the closest
long spelling of the current command's flags and options, and nothing when
none is close.
A successful parse is read one command at a time, as clap's ArgMatches
is: get, get_all and on read the bindings a command made, and
sub_name(m) and sub_of(m, name) give the subcommand selected under it and
its own Matched (at(m, path) follows a whole path). A root -v and a
subcommand's -v are different arguments, so tool -v greet -v sets both;
a flag or opt option given twice to one command is refused. A rest
positional keeps every leftover word under one name; get_all reads that
list, and get still reads one value. argv is the process's
arguments, each word reusable.
env(arg, "NAME") gives an argument an env var to fall back to, as clap's
.env("NAME"): parse_env(spec, words, vars) binds what the words give,
then fills an argument they left unbound from its env var, then from its
default. env_vars(spec) reads the env vars the spec names, as
(name, value) pairs; a test passes its own, as in
parse_env(spec(), [], [("HI_NAME", "Ada")]). An env var set to the empty
string counts as unset. A flag's env var sets it unless its value, in any
case, is 0, false, no, off, n or f. An env value outside the
argument's choices fails the parse with BadValue. Help shows
[env: NAME] after the argument's help text. parse ignores env vars.
def main() -> IO(Unit):
do IO<Unit>:
av : List<&2, String> <- Shake.argv()
ev : List<&2, Shake.Var> <- Shake.env_vars(spec())
IO.print(greet(Shake.parse_env(spec(), av, ev)))
A compiled Bend program's runtime reads the command line before shake does (bend 2.0.34):
--bend-helpprints the runtime's own usage and exits, and--gpu-buildbuilds the GPU image and exits; neither runsmain.--helpreaches the program, and shake reads it as a request for help.--threads Nand--gpu Xare taken, with their value, and a bad value stops the program.- The first
--is taken too, and every word after it is passed on unexamined.
So a -- ends shake's option parsing (every later word is a positional)
only when it is the second one: tool add -- -- -5 3 binds -5 and 3.
git clone https://github.com/Emerging-Patterns/shake
cd shake
mkdir -p bin
bend examples/demo/main.bend -o bin/demo.bin
bin/demo.bin help
bin/demo.bin greet --help
bin/demo.bin greet --name Ada -v
bin/demo.bin add 2 3 --times 2
nix build builds the same fixture to result/bin/demo.
main.bend: the interface.src/: the implementation, withsrc/LAWS.bendstating the parser's laws andsrc/PROOF.bendproving them.nix flake checkruns the proof gate.examples/demo/: a small program that uses onlymain.bend.ez.tomlandez.lock.toml: the ledger and lock; bolt, the linter, is pinned there as[tools.bolt], andnix flake checkruns it.docs/rfc/: the specification in progress.