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.6.0, and bend fetches it from
the hub into ~/.bend/lib on the first run.
import shake@0.6.0.0/main.bend as Shake
shake@0.6.0.0 resolves to 0xa383b26a5a0ef35663b3615d7aedf9e3;
import 0xa383b26a5a0ef35663b3615d7aedf9e3/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.38.
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.6.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, Shake.Read, Shake.ReadErr), the builders (app, sub,
flag, opt, many, pos, rest, env, read_flag, read_value),
check and spec_err_text, check_reads and read_err_text, parse and
parse_env, the readers (get, get_all, on, sub_name, sub_of, at,
path_of), help, err_text, help_path, version_of, err_path,
suggestion, argv and env_vars.
Everything under src/ is
internal and may change in any release; import only main.bend.
LAWS.bend lists what shake guarantees: one file under
laws/ each, proved by the file of the same name under
proofs/.
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.
get(m, "nmae") answers None for a misspelled name, just as for an option
the user left out. To catch that, list what your program reads, and check
the list against the spec the same way:
def reads() -> List<&2, Shake.Read>:
[Shake.read_flag([], "verbose"),
Shake.read_value(["greet"], "name"), Shake.read_value(["greet"], "tag")]
check_reads(spec(), reads()) is [] when every read fits. Otherwise it
reports, in order, a path that names no command, a name no argument of that
command has, an on of an argument that is not a flag, and a get of a
flag; read_err_text writes a report as one line.
parse fails with a request for help on tool help, tool help <command>,
tool --help, tool -h or tool <command> --help (a command that declares
its own long help gets --help as that argument instead, and one that
declares a short h or a long help gets -h): help_path gives its
command path, for help to print. A spec with a version answers
tool --version and tool -V at the root with NeedVersion{} (unless the
root declares its own --version), and version_of gives name version to
print. Both are None for every other error, which err_text describes,
naming an argument as clap does ('--base <BASE>', <B>) and listing the
choices of a value outside them. A help page lists the positionals under
Arguments:, and ends its Options with the -h, --help row, and the root's
with -V, --version, for the spellings shake answers there. 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.38), but only its own words:
- Runtime options go between
+RTSand-RTS, as intool +RTS --threads 4 -RTS add 1 2(a trailing-RTSmay be left off, and the env varBENDRTSholds them too).+RTS --help -RTSprints the runtime's own usage and exits, and+RTS --gpu-build -RTSbuilds the GPU image and exits; neither runsmain. --RTSis taken, and every word after it is passed on unexamined.- Every other word reaches shake:
--helpis a request for help, and--threads,--gpuand--gpu-buildoutside the bracket are the program's own words, an unknown argument unless the spec declares them.
So a -- ends shake's option parsing (every later word is a positional):
tool add -- -5 3 binds -5 and 3. On bend 2.0.37 and earlier the
runtime took the first -- and users typed it twice.
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.laws/andproofs/: one file per guarantee and its proof, imported byLAWS.bendandPROOF.bend.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.