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. 16 import Base 17 import ./src/cli.bend as S 18 import ./src/args.bend as Args 19 import ./src/check.bend as K 20 21 # a program spec 22 def Cli() -> Data: 23 S.Cli 24 25 # a nested command 26 def 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 30 def 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 35 def 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 40 def Var() -> Data: 41 S.Var 42 43 # a failed parse 44 def ParseErr() -> Data: 45 S.ParseErr 46 47 # one way a spec contradicts itself 48 def 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 52 def 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 62 def 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 71 def 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 80 def 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 93 def 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 105 def 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 115 def 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]` 127 def 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 135 def check(spec: S.Cli) -> List<&2, K.SpecErr>: 136 K.check(spec) 137 138 # a report of `check` as one line 139 def spec_err_text(err: K.SpecErr) -> String: 140 K.text(err) 141 142 # argv against a program spec 143 def 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 149 def 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 157 def 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 161 def 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 165 def 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 170 def 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 174 def 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 178 def 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 183 def 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 188 def 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` 193 def 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 199 def 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 204 def 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 `, `--help`), 222 # or None 223 # when the parse failed for another reason 224 def 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` 243 def 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 251 def argv() -> IO(List<&2, String>): # noqa: L001 IO: reads argv 252 Args.argv()