hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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 };