main.bend
shake@0.5.0.0 · 252 lines · 0 definitions
1# shake: a proven command-line argument parser for Bend 2, with subcommands and help.
2#
3# Describe a program with `app`, `sub`, `flag`, `opt`, `many`, `pos` and
4# `rest`, and give an argument an env var to fall back to with `env`; `parse`
5# binds argv against it (`parse_env` also takes the values of the env vars,
6# which `env_vars` reads), and
7# the result is read one command at a time, as clap's: `get`, `get_all` and
8# `on` read a command's own bindings, and `sub_name`, `sub_of`, `at` and
9# `path_of` reach the subcommands selected under it. `help` writes the usage
10# page for a command path. For a failed parse, `help_path` tells a request for help
11# from an error, `err_text` writes the message and `err_path` names the command
12# it failed in, and `suggestion` the long spelling a mistyped one was likely
13# meant to be. `check` reports a spec that contradicts itself. `argv` is the
14# process's arguments. This file is the interface: everything under src/ is
15# internal.
16import Base
17import shake@0.5.0.0/src/cli.bend as S
18import shake@0.5.0.0/src/args.bend as Args
19import shake@0.5.0.0/src/check.bend as K
20
21# a program spec
22def Cli() -> Data:
23 S.Cli
24
25# a nested command
26def Sub() -> Data: # noqa: L001 a type alias of the interface
27 S.Sub
28
29# one argument: a flag, an option, a positional or a rest positional
30def Arg() -> Data: # noqa: L001 a type alias of the interface
31 S.Arg
32
33# a successful parse, one command at a time: the bindings the command made,
34# and the subcommand selected under it with its own Matched
35def Matched() -> Data:
36 S.Matched
37
38# an env var's name and its value, a pair `(name, value)`: `parse_env` takes a
39# list of them, and `env_vars` reads one for each env var that is set
40def Var() -> Data:
41 S.Var
42
43# a failed parse
44def ParseErr() -> Data:
45 S.ParseErr
46
47# one way a spec contradicts itself
48def SpecErr() -> Data: # noqa: L001 a type alias of the interface
49 K.SpecErr
50
51# a program: name, about, optional version, top-level args and commands
52def app(
53 +name: String,
54 +about: String,
55 version: Maybe<&2, String>,
56 args: List<&2, S.Arg>,
57 subs: List<&2, S.Sub>
58) -> S.Cli:
59 S.app(name, about, version, args, subs)
60
61# a nested command
62def sub( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
63 +name: String,
64 +about: String,
65 args: List<&2, S.Arg>,
66 subs: List<&2, S.Sub>
67) -> S.Sub:
68 S.sub(name, about, args, subs)
69
70# a boolean flag
71def flag( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
72 +name: String,
73 short: Maybe<&2, String>,
74 long: Maybe<&2, String>,
75 +help: String
76) -> S.Arg:
77 S.flag(name, short, long, help)
78
79# a valued option
80def opt( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
81 +name: String,
82 short: Maybe<&2, String>,
83 long: Maybe<&2, String>,
84 +help: String,
85 required: Bool,
86 default: Maybe<&2, String>,
87 choices: List<&2, String>
88) -> S.Arg:
89 S.opt(name, short, long, help, required, default, choices)
90
91# an option that may be given more than once; `get_all` reads every value, in
92# order, and `get` the last
93def many( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
94 +name: String,
95 short: Maybe<&2, String>,
96 long: Maybe<&2, String>,
97 +help: String,
98 required: Bool,
99 default: Maybe<&2, String>,
100 choices: List<&2, String>
101) -> S.Arg:
102 S.many(name, short, long, help, required, default, choices)
103
104# a positional
105def pos( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
106 +name: String,
107 +help: String,
108 required: Bool,
109 default: Maybe<&2, String>,
110 choices: List<&2, String>
111) -> S.Arg:
112 S.pos(name, help, required, default, choices)
113
114# a rest positional: every leftover word, as one name
115def rest( # noqa: L001 a builder: its result is the Arg or Sub it names, nothing to state
116 +name: String,
117 +help: String,
118 required: Bool,
119 default: Maybe<&2, String>,
120 choices: List<&2, String>
121) -> S.Arg:
122 S.rest(name, help, required, default, choices)
123
124# this argument, falling back to the env var `var` when the words leave it
125# unbound, before its default, as clap's `.env(var)`: `parse_env` reads it,
126# and help shows `[env: VAR]`
127def env(arg: S.Arg, +var: String) -> S.Arg:
128 S.env(arg, var)
129
130# every way a spec contradicts itself: a rest positional that is not the
131# last, a repeated argument name or spelling, a repeated subcommand name, a
132# subcommand named `help`, a default outside its choices, a required
133# positional after an optional one. Empty for a well-formed spec, which is
134# what SPEC.md's parse guarantees are about; `parse` does not call it
135def check(spec: S.Cli) -> List<&2, K.SpecErr>:
136 K.check(spec)
137
138# a report of `check` as one line
139def spec_err_text(err: K.SpecErr) -> String:
140 K.text(err)
141
142# argv against a program spec
143def parse(spec: S.Cli, words: List<&2, String>) -> Result<&2, &2, S.ParseErr, S.Matched>:
144 S.parse(spec, words)
145
146# argv against a program spec, with `vars`, name and value pairs, for the env
147# vars: an argument the words leave unbound falls back to its env var's value
148# when it is set and not empty, then to its default
149def parse_env(
150 +spec: S.Cli,
151 words: List<&2, String>,
152 +vars: List<&2, S.Var>
153) -> Result<&2, &2, S.ParseErr, S.Matched>:
154 S.parse_env(spec, words, vars)
155
156# the value this command bound to `name` last, or None when it bound none
157def get(found: S.Matched, +name: String) -> Maybe<&2, String>:
158 S.get(found, name)
159
160# every value this command bound to `name`, in argv order
161def get_all(found: S.Matched, +name: String) -> List<&2, String>:
162 S.get_all(found, name)
163
164# whether this command's flag named `name` was set
165def on(found: S.Matched, +name: String) -> Bool:
166 S.on(found, name)
167
168# the names of the subcommands selected below this command, in order: the
169# whole selected path for the Matched `parse` answers
170def path_of(found: S.Matched) -> List<&2, String>:
171 S.path_of(found)
172
173# the name of the subcommand selected under this command, or None
174def sub_name(found: S.Matched) -> Maybe<&2, String>:
175 S.sub_name(found)
176
177# the Matched of subcommand `name` when it is the one selected, else None
178def sub_of(found: S.Matched, +name: String) -> Maybe<&2, S.Matched>:
179 S.sub_of(found, name)
180
181# the Matched of the command `path` names below this one, following `sub_of`
182# name by name, or None where a name is not the one selected
183def at(found: S.Matched, path: List<&2, String>) -> Maybe<&2, S.Matched>:
184 S.at(found, path)
185
186# the usage page for `path` under `spec`; an unknown name in the path is
187# skipped
188def help(+spec: S.Cli, path: List<&2, String>) -> String:
189 S.help(spec, path)
190
191# the message for a failed parse, ending with the root usage line; empty for
192# a request for help, which the caller answers with `help`
193def err_text(+spec: S.Cli, err: S.ParseErr) -> String:
194 S.err_text(spec, err)
195
196# for an unknown long option, the long option word of the current command it
197# was likely meant to be, as `--name` for `--nmae`, which `err_text` shows as
198# clap's tip; None for every other error and when nothing is close
199def suggestion(+spec: S.Cli, err: S.ParseErr) -> Maybe<&2, String>:
200 S.suggestion(spec, err)
201
202# the command path where the parse failed: the path selected when it did, or
203# for a request for help the path it asks about
204def err_path(err: S.ParseErr) -> List<&2, String>:
205 match err:
206 case S.UnknownFlag{at, _flag}:
207 at
208 case S.Missing{at, _name}:
209 at
210 case S.NoValue{at, _name}:
211 at
212 case S.BadValue{at, _name, _value}:
213 at
214 case S.NeedHelp{path}:
215 path
216 case S.Unexpected{at, _arg}:
217 at
218 case S.Repeated{at, _name}:
219 at
220
221# the command path of a request for help (`help`, `help <path>`, `--help`),
222# or None
223# when the parse failed for another reason
224def help_path(err: S.ParseErr) -> Maybe<&2, List<&2, String>>:
225 match err:
226 case S.NeedHelp{path}:
227 Some{path}
228 case S.UnknownFlag{_at, _flag}:
229 None{}
230 case S.Missing{_at, _name}:
231 None{}
232 case S.NoValue{_at, _name}:
233 None{}
234 case S.BadValue{_at, _name, _value}:
235 None{}
236 case S.Unexpected{_at, _arg}:
237 None{}
238 case S.Repeated{_at, _name}:
239 None{}
240
241# the env vars `spec` gives its arguments, each that is set with its value,
242# for `parse_env`
243def env_vars(spec: S.Cli) -> IO(List<&2, S.Var>): # noqa: L001 IO: reads env vars
244 Args.env_vars(spec)
245
246# the process's arguments, each word reusable, without the program name
247# that `IO.args` starts with (bend 2.0.32 and later). A compiled program's
248# runtime has already acted on `--bend-help`, `--gpu-build`, `--threads N`
249# and `--gpu X` and taken the first `--`, passing every word after it here
250# unexamined; so the `--` that `parse` sees is the user's second one
251def argv() -> IO(List<&2, String>): # noqa: L001 IO: reads argv
252 Args.argv()