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