getopts.ha (9576B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use fmt; 5 use io; 6 use os; 7 use strings; 8 9 // The result of parsing the set of command line arguments, including any 10 // options specified and the list of non-option arguments. If a subcommand 11 // is present in the help passed to [[parse]], then there will be no args. 12 export type command = struct { 13 opts: [](rune, str), 14 subcmd: (void | (str, *command)), 15 args: []str, 16 help: []help, 17 }; 18 19 // Help text providing a short, one-line summary of the command; or providing 20 // the name of an argument. 21 export type cmd_help = str; 22 23 // Help text for a flag, formatted as "-a: help text". 24 export type flag_help = (rune, str); 25 26 // Help text for a parameter, formatted as "-a param: help text" where "param" 27 // is the first string and "help text" is the second string. 28 export type parameter_help = (rune, str, str); 29 30 // Definition of a named subcommand. 31 export type subcmd_help = (str, []help); 32 33 // Help text for a command or option. [[cmd_help]], [[flag_help]], and 34 // [[parameter_help]] compose such that the following []help: 35 // 36 // [ 37 // "foo bars in order", 38 // ('a', "a help text"), 39 // ('b', "b help text"), 40 // ('c', "cflag", "c help text"), 41 // ('d', "dflag", "d help text"), 42 // "files...", 43 // ] 44 // 45 // will produce this help text: 46 // 47 // foo: foo bars in order 48 // 49 // Usage: foo [-ab] [-c <cflag>] [-d <dflag>] files... 50 // 51 // -a: a help text 52 // -b: b help text 53 // -c <cflag>: c help text 54 // -d <dflag>: d help text 55 export type help = (cmd_help | flag_help | parameter_help | subcmd_help); 56 57 export type requires_arg = rune; 58 export type unknown_option = rune; 59 export type unknown_subcmd = str; 60 export type error = !( 61 str, []help, 62 (requires_arg | unknown_option | unknown_subcmd), 63 ); 64 65 // Converts a parsing error into a human-friendly string. The result may be 66 // statically allocated. 67 export fn strerror(err: error) str = { 68 static let buf: [1024]u8 = [0...]; 69 match (err.2) { 70 case let r: requires_arg => 71 return fmt::bsprintf(buf, "{}: option -{} requires an argument", 72 err.0, r: rune); 73 case let r: unknown_option => 74 return fmt::bsprintf(buf, "{}: unrecognized option: -{}", 75 err.0, r: rune); 76 case let s: unknown_subcmd => 77 return fmt::bsprintf(buf, "{}: unrecognized subcommand: {}", 78 err.0, s: str); 79 }; 80 }; 81 82 // A wrapper for [[tryparse]] in which if an error occurs, details are printed 83 // to [[os::stderr]] (as in [[printusage]]), and [[os::exit]] is called with 84 // [[os::status::FAILURE]]. 85 export fn parse(args: []str, help: help...) command = { 86 match (tryparse(args, help...)) { 87 case let c: command => return c; 88 case let e: error => 89 fmt::errorln(strerror(e))!; 90 if (e.2 is unknown_subcmd) { 91 printsubcmds(os::stderr, e.1)!; 92 fmt::errorln()!; 93 }; 94 printusage(os::stderr, e.0, e.1)!; 95 os::exit(os::status::FAILURE); 96 }; 97 }; 98 99 // Parses command line arguments and returns a [[command]], or an [[error]] 100 // if an error occurs. The argument list must include the command name as 101 // the first item; [[os::args]] fulfills this criteria. 102 export fn tryparse(args: []str, help: help...) (command | error) = { 103 let opts: [](rune, str) = []; 104 let i = 1z; 105 for :arg (i < len(args); i += 1) { 106 const arg = args[i]; 107 if (len(arg) == 0 || arg == "-" 108 || !strings::hasprefix(arg, "-")) { 109 break; 110 }; 111 if (arg == "--") { 112 i += 1; 113 break; 114 }; 115 116 let iter = strings::iter(arg); 117 assert(strings::next(&iter) as rune == '-'); 118 for (let r => strings::next(&iter)) { 119 let found = false; 120 for (let j = 0z; j < len(help); j += 1) match (help[j]) { 121 case let f: flag_help => 122 if (r == f.0) { 123 append(opts, (r, "")); 124 found = true; 125 break; 126 }; 127 case let p: parameter_help => 128 if (r == p.0) { 129 let value = strings::iterstr(&iter); 130 if (len(value) == 0) { 131 if (i == len(args) - 1) { 132 free(opts); 133 return (args[0], help, r: requires_arg): error; 134 }; 135 i += 1; 136 append(opts, (r, args[i])); 137 } else { 138 append(opts, (r, value)); 139 }; 140 continue :arg; 141 }; 142 case => 143 continue; 144 }; 145 if (found) continue; 146 if (r =='h') { 147 printhelp(os::stderr, args[0], help)!; 148 os::exit(os::status::SUCCESS); 149 }; 150 free(opts); 151 return (args[0], help, r: unknown_option): error; 152 }; 153 }; 154 let subcmd: (void | (str, *command)) = void; 155 if (i < len(args)) { 156 let expects_subcmd = false; 157 for (let j = 0z; j < len(help); j += 1) match (help[j]) { 158 case let s: subcmd_help => 159 expects_subcmd = true; 160 if (s.0 == args[i]) match (tryparse(args[i..], s.1...)) { 161 case let c: command => 162 subcmd = (s.0, alloc(c)); 163 case let e: error => 164 free(opts); 165 return e; 166 }; 167 case => continue; 168 }; 169 if (expects_subcmd && subcmd is void) { 170 free(opts); 171 return (args[0], help, args[i]: unknown_subcmd): error; 172 }; 173 }; 174 return command { 175 opts = opts, 176 subcmd = subcmd, 177 args = if (subcmd is void) args[i..] else [], 178 help = help, 179 }; 180 }; 181 182 // Frees resources associated with the return value of [[parse]]. 183 export fn finish(cmd: *command) void = { 184 free(cmd.opts); 185 match (cmd.subcmd) { 186 case void => void; 187 case let s: (str, *command) => 188 finish(s.1); 189 free(s.1); 190 }; 191 }; 192 193 // Prints command usage to the provided stream. 194 export fn printusage( 195 out: io::handle, 196 name: str, 197 help: []help 198 ) (void | io::error) = { 199 let h = contains_h(help); 200 let z = _printusage(io::empty, name, false, h, help)?; 201 _printusage(out, name, if (z > 72) true else false, h, help)?; 202 }; 203 204 fn _printusage( 205 out: io::handle, 206 name: str, 207 indent: bool, 208 contains_h: bool, 209 help: []help, 210 ) (size | io::error) = { 211 let z = fmt::fprint(out, "Usage:", name)?; 212 213 let started_flags = false; 214 if (!contains_h) { 215 z += fmt::fprint(out, " [-h")?; 216 started_flags = true; 217 }; 218 for (let h .. help) { 219 match (h) { 220 case let h: flag_help => 221 if (!started_flags) { 222 z += fmt::fprint(out, " [-")?; 223 started_flags = true; 224 }; 225 z += fmt::fprint(out, h.0)?; 226 case => void; 227 }; 228 }; 229 if (started_flags) { 230 z += fmt::fprint(out, "]")?; 231 }; 232 233 for (let h .. help) { 234 match (h) { 235 case let h: parameter_help => 236 if (indent) { 237 z += fmt::fprintf(out, "\n\t")?; 238 }; 239 z += fmt::fprintf(out, " [-{} <{}>]", h.0, h.1)?; 240 case => void; 241 }; 242 }; 243 244 let first_arg = true; 245 for (let i = 1z; i < len(help); i += 1) if (help[i] is cmd_help) { 246 if (first_arg) { 247 if (indent) { 248 z += fmt::fprintf(out, "\n\t")?; 249 }; 250 first_arg = false; 251 }; 252 z += fmt::fprintf(out, " {}", help[i] as cmd_help: str)?; 253 }; 254 255 return z + fmt::fprint(out, "\n")?; 256 }; 257 258 fn contains_h(help: []help) bool = { 259 for (let h .. help) { 260 const r = match (h) { 261 case let h: flag_help => yield h.0; 262 case let h: parameter_help => yield h.0; 263 case => continue; 264 }; 265 if (r == 'h') { 266 return true; 267 }; 268 }; 269 return false; 270 }; 271 272 // Prints command help to the provided stream. 273 export fn printhelp( 274 out: io::handle, 275 name: str, 276 help: []help 277 ) (void | io::error) = { 278 if (len(help) == 0) { 279 return; 280 }; 281 282 if (help[0] is cmd_help) { 283 fmt::fprintfln(out, "{}: {}\n", name, help[0] as cmd_help: str)?; 284 }; 285 286 let contains_h = contains_h(help); 287 let z = _printusage(io::empty, name, false, contains_h, help)?; 288 _printusage(out, name, if (z > 72) true else false, contains_h, help)?; 289 290 fmt::fprint(out, "\n")?; 291 if (!contains_h) { 292 fmt::fprintln(out, "-h: print this help text")?; 293 }; 294 for (let h .. help) { 295 match (h) { 296 case let f: flag_help => 297 fmt::fprintfln(out, "-{}: {}", f.0, f.1)?; 298 case let p: parameter_help => 299 fmt::fprintfln(out, "-{} <{}>: {}", p.0, p.1, p.2)?; 300 case => void; 301 }; 302 }; 303 304 printsubcmds(out, help)?; 305 }; 306 307 fn printsubcmds(out: io::handle, help: []help) (void | io::error) = { 308 let first = true; 309 for (let h .. help) { 310 match (h) { 311 case let s: subcmd_help => 312 // Only print this if there are subcommands to show 313 if (first) { 314 fmt::fprintln(out, "\nSubcommands:")?; 315 first = false; 316 }; 317 if (len(s.1) == 0 || !(s.1[0] is cmd_help)) { 318 fmt::fprintfln(out, " {}", s.0)?; 319 } else { 320 fmt::fprintfln(out, " {}: {}", s.0, 321 s.1[0] as cmd_help: str)?; 322 }; 323 case => void; 324 }; 325 }; 326 }; 327 328 @test fn parse() void = { 329 let args: []str = ["cat", "-v", "a.out"]; 330 let cat = parse(args, 331 "concatenate files", 332 ('v', "cause Rob Pike to make a USENIX presentation"), 333 "files...", 334 ); 335 defer finish(&cat); 336 assert(len(cat.args) == 1 && cat.args[0] == "a.out"); 337 assert(len(cat.opts) == 1 && cat.opts[0].0 == 'v' && cat.opts[0].1 == ""); 338 339 args = ["ls", "-Fahs", "--", "-j"]; 340 let ls = parse(args, 341 "list files", 342 ('F', "Do some stuff"), 343 ('h', "Do some other stuff"), 344 ('s', "Do a third type of stuff"), 345 ('a', "Do a fourth type of stuff"), 346 "files...", 347 ); 348 defer finish(&ls); 349 assert(len(ls.args) == 1 && ls.args[0] == "-j"); 350 assert(len(ls.opts) == 4); 351 assert(ls.opts[0].0 == 'F' && ls.opts[0].1 == ""); 352 assert(ls.opts[1].0 == 'a' && ls.opts[1].1 == ""); 353 assert(ls.opts[2].0 == 'h' && ls.opts[2].1 == ""); 354 assert(ls.opts[3].0 == 's' && ls.opts[3].1 == ""); 355 356 args = ["sed", "-e", "s/C++//g", "-f/tmp/turing.sed", "-"]; 357 let sed = parse(args, 358 "edit streams", 359 ('e', "script", "Add the editing commands specified by the " 360 "script option to the end of the script of editing " 361 "commands"), 362 ('f', "script_file", "Add the editing commands in the file " 363 "script_file to the end of the script of editing " 364 "commands"), 365 "files...", 366 ); 367 defer finish(&sed); 368 assert(len(sed.args) == 1 && sed.args[0] == "-"); 369 assert(len(sed.opts) == 2); 370 assert(sed.opts[0].0 == 'e' && sed.opts[0].1 == "s/C++//g"); 371 assert(sed.opts[1].0 == 'f' && sed.opts[1].1 == "/tmp/turing.sed"); 372 };