hare

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

getopts.ha (7681B)


      1 // License: MPL-2.0
      2 // (c) 2021 Alexey Yerin <yyp@disroot.org>
      3 // (c) 2021 Byron Torres <b@torresjrjr.com>
      4 // (c) 2021 Drew DeVault <sir@cmpwn.com>
      5 // (c) 2021 Eyal Sawady <ecs@d2evs.net>
      6 // (c) 2021 Jonathan Halmen <slowjo@halmen.xyz>
      7 // (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz>
      8 use encoding::utf8;
      9 use fmt;
     10 use io;
     11 use os;
     12 use strings;
     13 
     14 // A flag which does not take a parameter, e.g. "-a".
     15 export type flag = rune;
     16 
     17 // An option with an included parameter, e.g. "-a foo".
     18 export type parameter = str;
     19 
     20 // A command line option.
     21 export type option = (flag, parameter);
     22 
     23 // The result of parsing the set of command line arguments, including any
     24 // options specified and the list of non-option arguments.
     25 export type command = struct {
     26 	opts: []option,
     27 	args: []str,
     28 };
     29 
     30 // Help text providing a short, one-line summary of the command; or providing
     31 // the name of an argument.
     32 export type cmd_help = str;
     33 
     34 // Help text for a flag, formatted as "-a: help text".
     35 export type flag_help = (flag, str);
     36 
     37 // Help text for a parameter, formatted as "-a param: help text" where "param"
     38 // is the first string and "help text" is the second string.
     39 export type parameter_help = (flag, str, str);
     40 
     41 // Help text for a command or option. [[cmd_help]], [[flag_help]], and
     42 // [[parameter_help]] compose such that the following []help:
     43 //
     44 // 	[
     45 // 		"foo bars in order",
     46 // 		('a', "a help text"),
     47 // 		('b', "b help text"),
     48 // 		('c', "cflag", "c help text"),
     49 // 		('d', "dflag", "d help text"),
     50 // 		"files...",
     51 // 	]
     52 //
     53 // will produce this help text:
     54 //
     55 // 	foo: foo bars in order
     56 //
     57 // 	Usage: foo [-ab] [-c <cflag>] [-d <dflag>] files...
     58 //
     59 // 	-a: a help text
     60 // 	-b: b help text
     61 // 	-c <cflag>: c help text
     62 // 	-d <dflag>: d help text
     63 export type help = (cmd_help | flag_help | parameter_help);
     64 
     65 // Parses command line arguments and returns a tuple of the options specified,
     66 // and the remaining arguments. If an error occurs, details are printed to
     67 // [[os::stderr]] and [[os::exit]] is called with a nonzero exit status. The
     68 // argument list must include the command name as the first item; [[os::args]]
     69 // fulfills this criteria.
     70 export fn parse(args: []str, help: help...) command = {
     71 	let opts: []option = [];
     72 	let i = 1z;
     73 	for (i < len(args); i += 1) :arg {
     74 		const arg = args[i];
     75 		if (len(arg) == 0 || arg == "-"
     76 				|| !strings::hasprefix(arg, "-")) {
     77 			break;
     78 		};
     79 		if (arg == "--") {
     80 			i += 1;
     81 			break;
     82 		};
     83 
     84 		let d = utf8::decode(arg);
     85 		assert(utf8::next(&d) as rune == '-');
     86 		let next = utf8::next(&d);
     87 		for (next is rune; next = utf8::next(&d)) :flag {
     88 			const r = next as rune;
     89 			for (let j = 0z; j < len(help); j += 1) :help {
     90 				let p: parameter_help = match (help[j]) {
     91 				case cmd_help =>
     92 					continue :help;
     93 				case let f: flag_help =>
     94 					if (r == f.0) {
     95 						append(opts, (r, ""));
     96 						continue :flag;
     97 					} else {
     98 						continue :help;
     99 					};
    100 				case let p: parameter_help =>
    101 					yield if (r == p.0) p else
    102 						continue :help;
    103 				};
    104 				if (len(d.src) == d.offs) {
    105 					if (i + 1 >= len(args)) {
    106 						errmsg(args[0], "option requires an argument: ",
    107 							r, help);
    108 						os::exit(1);
    109 					};
    110 					i += 1;
    111 					append(opts, (r, args[i]));
    112 				} else {
    113 					let s = strings::fromutf8(d.src[d.offs..]);
    114 					append(opts, (r, s));
    115 				};
    116 				continue :arg;
    117 			};
    118 			if (r =='h') {
    119 				printhelp(os::stderr, args[0], help);
    120 				os::exit(0);
    121 			};
    122 			errmsg(args[0], "unrecognized option: ", r, help);
    123 			os::exit(1);
    124 		};
    125 		match (next) {
    126 		case void => void;
    127 		case rune =>
    128 			abort(); // Unreachable
    129 		case (utf8::more | utf8::invalid) =>
    130 			errmsg(args[0], "invalid UTF-8 in arguments", void,
    131 				help);
    132 			os::exit(1);
    133 		};
    134 	};
    135 	return command {
    136 		opts = opts,
    137 		args = args[i..],
    138 	};
    139 };
    140 
    141 // Frees resources associated with the return value of [[parse]].
    142 export fn finish(cmd: *command) void = {
    143 	if (cmd == null) return;
    144 	free(cmd.opts);
    145 };
    146 
    147 fn _printusage(out: io::handle, name: str, indent: bool, help: []help) size = {
    148 	let z = fmt::fprint(out, "Usage:", name) as size;
    149 
    150 	let started_flags = false;
    151 	for (let i = 0z; i < len(help); i += 1) if (help[i] is flag_help) {
    152 		if (!started_flags) {
    153 			z += fmt::fprint(out, " [-") as size;
    154 			started_flags = true;
    155 		};
    156 		const help = help[i] as flag_help;
    157 		z += fmt::fprint(out, help.0: rune) as size;
    158 	};
    159 	if (started_flags) {
    160 		z += fmt::fprint(out, "]") as size;
    161 	};
    162 
    163 	for (let i = 0z; i < len(help); i += 1) if (help[i] is parameter_help) {
    164 		const help = help[i] as parameter_help;
    165 		if (indent) {
    166 			z += fmt::fprintf(out, "\n\t") as size;
    167 		};
    168 		z += fmt::fprintf(out, " [-{} <{}>]", help.0: rune, help.1) as size;
    169 	};
    170 	let first_arg = true;
    171 	for (let i = 1z; i < len(help); i += 1) if (help[i] is cmd_help) {
    172 		if (first_arg) {
    173 			if (indent) {
    174 				z += fmt::fprintf(out, "\n\t") as size;
    175 			};
    176 			first_arg = false;
    177 		};
    178 		z += fmt::fprintf(out, " {}", help[i] as cmd_help: str) as size;
    179 	};
    180 
    181 	return z + fmt::fprint(out, "\n") as size;
    182 };
    183 
    184 // Prints command usage to the provided stream.
    185 export fn printusage(out: io::handle, name: str, help: []help) void = {
    186 	let z = _printusage(io::empty, name, false, help);
    187 	_printusage(out, name, if (z > 72) true else false, help);
    188 };
    189 
    190 // Prints command help to the provided stream.
    191 export fn printhelp(out: io::handle, name: str, help: []help) void = {
    192 	if (len(help) == 0) {
    193 		return;
    194 	};
    195 
    196 	if (help[0] is cmd_help) {
    197 		fmt::fprintfln(out, "{}: {}\n", name, help[0] as cmd_help: str)!;
    198 	};
    199 
    200 	printusage(out, name, help);
    201 
    202 	for (let i = 0z; i < len(help); i += 1) match (help[i]) {
    203 	case cmd_help => void;
    204 	case (flag_help | parameter_help) =>
    205 		// Only print this if there are flags to show
    206 		fmt::fprint(out, "\n")!;
    207 		break;
    208 	};
    209 
    210 	for (let i = 0z; i < len(help); i += 1) match (help[i]) {
    211 	case cmd_help => void;
    212 	case let f: flag_help =>
    213 		fmt::fprintfln(out, "-{}: {}", f.0: rune, f.1)!;
    214 	case let p: parameter_help =>
    215 		fmt::fprintfln(out, "-{} <{}>: {}", p.0: rune, p.1, p.2)!;
    216 	};
    217 };
    218 
    219 fn errmsg(name: str, err: str, opt: (rune | void), help: []help) void = {
    220 	fmt::errorfln("{}: {}{}", name, err, match (opt) {
    221 	case let r: rune =>
    222 		yield r;
    223 	case void =>
    224 		yield "";
    225 	})!;
    226 	printusage(os::stderr, name, help);
    227 };
    228 
    229 @test fn parse() void = {
    230 	let args: []str = ["cat", "-v", "a.out"];
    231 	let cat = parse(args,
    232 		"concatenate files",
    233 		('v', "cause Rob Pike to make a USENIX presentation"),
    234 		"files...",
    235 	);
    236 	defer finish(&cat);
    237 	assert(len(cat.args) == 1 && cat.args[0] == "a.out");
    238 	assert(len(cat.opts) == 1 && cat.opts[0].0 == 'v' && cat.opts[0].1 == "");
    239 
    240 	args = ["ls", "-Fahs", "--", "-j"];
    241 	let ls = parse(args,
    242 		"list files",
    243 		('F', "Do some stuff"),
    244 		('h', "Do some other stuff"),
    245 		('s', "Do a third type of stuff"),
    246 		('a', "Do a fourth type of stuff"),
    247 		"files...",
    248 	);
    249 	defer finish(&ls);
    250 	assert(len(ls.args) == 1 && ls.args[0] == "-j");
    251 	assert(len(ls.opts) == 4);
    252 	assert(ls.opts[0].0 == 'F' && ls.opts[0].1 == "");
    253 	assert(ls.opts[1].0 == 'a' && ls.opts[1].1 == "");
    254 	assert(ls.opts[2].0 == 'h' && ls.opts[2].1 == "");
    255 	assert(ls.opts[3].0 == 's' && ls.opts[3].1 == "");
    256 
    257 	args = ["sed", "-e", "s/C++//g", "-f/tmp/turing.sed", "-"];
    258 	let sed = parse(args,
    259 		"edit streams",
    260 		('e', "script", "Add the editing commands specified by the "
    261 			"script option to the end of the script of editing "
    262 			"commands"),
    263 		('f', "script_file", "Add the editing commands in the file "
    264 			"script_file to the end of the script of editing "
    265 			"commands"),
    266 		"files...",
    267 	);
    268 	defer finish(&sed);
    269 	assert(len(sed.args) == 1 && sed.args[0] == "-");
    270 	assert(len(sed.opts) == 2);
    271 	assert(sed.opts[0].0 == 'e' && sed.opts[0].1 == "s/C++//g");
    272 	assert(sed.opts[1].0 == 'f' && sed.opts[1].1 == "/tmp/turing.sed");
    273 };