main.bend

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