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. 13 import Base 14 import ./src/cli.bend as S 15 import ./src/args.bend as Args 16 import ./src/check.bend as K 17 18 # a program spec 19 def Cli() -> Data: 20 S.Cli 21 22 # a nested command 23 def 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 27 def 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 32 def Matched() -> Data: 33 S.Matched 34 35 # a failed parse 36 def ParseErr() -> Data: 37 S.ParseErr 38 39 # one way a spec contradicts itself 40 def 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 44 def 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 54 def 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 63 def 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 72 def 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 85 def 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 97 def 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 107 def 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 121 def check(spec: S.Cli) -> List<&2, K.SpecErr>: 122 K.check(spec) 123 124 # a report of `check` as one line 125 def spec_err_text(err: K.SpecErr) -> String: 126 K.text(err) 127 128 # argv against a program spec 129 def 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 133 def 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 137 def 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 141 def 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 146 def 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 150 def 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 154 def 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 159 def 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 164 def 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` 169 def 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 174 def 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 `, `--help`), 192 # or None 193 # when the parse failed for another reason 194 def 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 216 def argv() -> IO(List<&2, String>): # noqa: L001 IO: reads argv 217 Args.argv()