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()