commit 05b6b962da3713ff7830c5b8b8a251493ea166ed
parent 30f0e29d17478c166bc9d8a06be78a253b46a5a4
Author: Eyal Sawady <ecs@d2evs.net>
Date: Thu, 11 Mar 2021 10:59:38 -0500
getopt: new module
Diffstat:
A | getopt/getopts.ha | | | 287 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 287 insertions(+), 0 deletions(-)
diff --git a/getopt/getopts.ha b/getopt/getopts.ha
@@ -0,0 +1,287 @@
+// getopt provides an interface for parsing command line arguments and
+// automatically generates a brief help message explaining the command usage.
+// See [getopts] for the main entry point.
+//
+// The help text is brief and should serve only as a reminder. It is recommended
+// that your command line program be accompanied by a man page to provide
+// detailed usage information.
+use encoding::utf8;
+use fmt;
+use io;
+use os;
+use strings;
+
+// A flag which does not take a parameter, e.g. "-a".
+export type flag = rune;
+
+// An option with an included parameter, e.g. "-a foo".
+export type parameter = str;
+
+// A command line option.
+export type option = (flag, (parameter | void));
+
+// The result of parsing the set of command line arguments, including any
+// options specified and the list of non-option arguments.
+export type command = struct {
+ opts: []option,
+ args: []str,
+};
+
+// Help text providing a short, one-line summary of the command; or providing
+// the name of an argument.
+export type cmd_help = str;
+
+// Help text for a flag, formatted as "-a: help text".
+export type flag_help = (flag, str);
+
+// Help text for a parameter, formatted as "-a param: help text" where "param"
+// is the first string and "help text" is the second string.
+export type parameter_help = (flag, str, str);
+
+// Help text for a command or option.
+//
+// cmd_help, flag_help, and parameter_help compose such that the help output for
+//
+// [
+// "foo bars in order",
+// ('a', "a help text"),
+// ('b', "b help text"),
+// ('c', "cflag", "c help text"),
+// ('d', "dflag", "d help text"),
+// "files...",
+// ]
+//
+// is:
+//
+// foo: foo bars in order
+//
+// Usage: foo [-ab] [-c <cflag>] [-d <dflag>] files...
+//
+// -a: a help text
+// -b: b help text
+// -c <cflag>: c help text
+// -d <dflag>: d help text
+export type help = (cmd_help | flag_help | parameter_help);
+
+// Parses command line arguments and returns a tuple of the options specified,
+// and the remaining arguments. If an error occurs, details are printed to
+// [os::stderr] and [os::exit] is called with a nonzero exit status. The
+// argument list must include the command name as the first item; [os::args]
+// fulfills this criteria.
+//
+// The caller provides [help] arguments to specify which command line flags and
+// parameters are supported, and to provide some brief help text which describes
+// their use. Provide [flag_help] to add a flag which does not take a parameter,
+// and [parameter_help] to add a flag with a required parameter. The first
+// [cmd_help] is used as a short, one-line summary of the command's purpose, and
+// any later [cmd_help] arguments are used to provide the name of any arguments
+// which follow the options list.
+//
+// By convention, the caller should sort the list of options, first providing
+// all flags, then all parameters, alpha-sorted within each group by the flag
+// rune.
+//
+// // Usage for sed
+// let cmd = getopt::getopts(os::args
+// "stream editor",
+// ('E', "use extended regular expressions"),
+// ('s', "treat files as separate, rather than one continuous stream"),
+// ('i', "edit files in place"),
+// ('z', "separate lines by NUL characeters"),
+// ('e', "script", "execute commands from script"),
+// ('f', "file", "execute commands from a file"),
+// "files...",
+// );
+// defer getopts::finish(&cmd);
+//
+// for (let i = 0z; i < len(cmd.opts); i += 1) {
+// let opt = cmd.opts[i];
+// switch (opt.0) {
+// 'E' => extended = true,
+// 's' => continuous = false,
+// // ...
+// 'e' => script = opt.1 as parameter,
+// 'f' => file = opt.1 as parameter,
+// };
+// };
+//
+// for (let i = 0z; i < len(cmd.args); i += 1) {
+// let arg = cmd.args[i];
+// // ...
+// };
+//
+// If "-h" is not among the options defined by the caller, the "-h" option will
+// will cause a summary of the command usage to be printed to stderr, and
+// [os::exit] will be called with a successful exit status.
+export fn getopts(args: []str, help: help...) command = {
+ let opts: []option = [];
+ let i = 1z;
+ :arg for (i < len(args); i += 1) {
+ const arg = args[i];
+ if (len(arg) == 0 || arg == "-"
+ || !strings::has_prefix(arg, "-")) {
+ break;
+ };
+ if (arg == "--") {
+ i += 1;
+ break;
+ };
+
+ let d = utf8::decode(arg);
+ assert(utf8::next(&d) as rune == '-');
+ let next = utf8::next(&d);
+ :flag for (next is rune; next = utf8::next(&d)) {
+ const r = next as rune;
+ :help for (let j = 0z; j < len(help); j += 1) {
+ let p: parameter_help = match (help[j]) {
+ cmd_help => continue :help,
+ f: flag_help => if (r == f.0) {
+ append(opts, (r, void));
+ continue :flag;
+ } else continue :help,
+ p: parameter_help => if (r == p.0) p
+ else continue :help,
+ };
+ if (len(d.src) == d.offs) {
+ if (i + 1 >= len(args)) {
+ errmsg(args[0], "option requires an argument: ",
+ r, help);
+ os::exit(1);
+ };
+ i += 1;
+ append(opts, (r, args[i]));
+ } else {
+ let s = strings::from_utf8(d.src[d.offs..]);
+ append(opts, (r, s));
+ };
+ continue :arg;
+ };
+ if (r =='h') {
+ print_help(os::stderr, args[0], help);
+ os::exit(0);
+ };
+ errmsg(args[0], "unrecognized option: ", r, help);
+ os::exit(1);
+ };
+ match (next) {
+ rune => abort(), // Unreachable
+ void => void,
+ (utf8::more | utf8::invalid) => {
+ errmsg(args[9], "invalid UTF-8 in arguments",
+ void, help);
+ os::exit(1);
+ },
+ };
+ };
+ return command {
+ opts = opts,
+ args = args[i..],
+ };
+};
+
+// Frees resources associated with the return value of [getopts].
+export fn finish(cmd: *command) void = {
+ if (cmd == null) return;
+ free(cmd.opts);
+};
+
+fn print_usage(s: *io::stream, name: str, help: []help) void = {
+ fmt::fprint(s, "Usage:", name);
+
+ let started_flags = false;
+ for (let i = 0z; i < len(help); i += 1) if (help[i] is flag_help) {
+ if (!started_flags) {
+ fmt::fprint(s, " [-");
+ started_flags = true;
+ };
+ const help = help[i] as flag_help;
+ fmt::fprint(s, help.0: rune);
+ };
+ if (started_flags) {
+ fmt::fprint(s, "]");
+ };
+
+ for (let i = 0z; i < len(help); i += 1) if (help[i] is parameter_help) {
+ const help = help[i] as parameter_help;
+ fmt::fprintf(s, " [-{} <{}>]", help.0: rune, help.1);
+ };
+ for (let i = 1z; i < len(help); i += 1) if (help[i] is cmd_help) {
+ fmt::fprintf(s, " {}", help[i] as cmd_help: str);
+ };
+
+ fmt::fprint(s, "\n");
+};
+
+fn print_help(s: *io::stream, name: str, help: []help) void = {
+ print_usage(s, name, help);
+
+ if (help[0] is cmd_help) {
+ fmt::fprintfln(s, "{}: {}", name, help[0] as cmd_help: str);
+ };
+
+ fmt::fprint(s, "\n");
+
+ for (let i = 0z; i < len(help); i += 1) match (help[i]) {
+ cmd_help => void,
+ f: flag_help => {
+ fmt::fprintfln(s, "-{}: {}", f.0: rune, f.1);
+ },
+ p: parameter_help => {
+ fmt::fprintfln(s, "-{} <{}>: {}", p.0: rune, p.1, p.2);
+ },
+ };
+};
+
+fn errmsg(name: str, err: str, opt: (rune | void), help: []help) void = {
+ fmt::errorfln("{}: {}{}", name, err, match (opt) {
+ r: rune => r,
+ void => "",
+ });
+ print_usage(os::stderr, name, help);
+};
+
+@test fn getopts() void = {
+ let args: []str = ["cat", "-v", "a.out"];
+ let cat = getopts(args,
+ "concatenate files",
+ ('v', "cause Rob Pike to make a USENIX presentation"),
+ "files...",
+ );
+ defer finish(&cat);
+ assert(len(cat.args) == 1 && cat.args[0] == "a.out");
+ assert(len(cat.opts) == 1 && cat.opts[0].0 == 'v' && cat.opts[0].1 is void);
+
+ args = ["ls", "-Fahs", "--", "-j"];
+ let ls = getopts(args,
+ "list files",
+ ('F', "Do some stuff"),
+ ('h', "Do some other stuff"),
+ ('s', "Do a third type of stuff"),
+ ('a', "Do a fourth type of stuff"),
+ "files...",
+ );
+ defer finish(&ls);
+ assert(len(ls.args) == 1 && ls.args[0] == "-j");
+ assert(len(ls.opts) == 4);
+ assert(ls.opts[0].0 == 'F' && ls.opts[0].1 is void);
+ assert(ls.opts[1].0 == 'a' && ls.opts[1].1 is void);
+ assert(ls.opts[2].0 == 'h' && ls.opts[2].1 is void);
+ assert(ls.opts[3].0 == 's' && ls.opts[3].1 is void);
+
+ args = ["sed", "-e", "s/C++//g", "-f/tmp/turing.sed", "-"];
+ let sed = getopts(args,
+ "edit streams",
+ ('e', "script", "Add the editing commands specified by the "
+ "script option to the end of the script of editing "
+ "commands"),
+ ('f', "script_file", "Add the editing commands in the file "
+ "script_file to the end of the script of editing "
+ "commands"),
+ "files...",
+ );
+ defer finish(&sed);
+ assert(len(sed.args) == 1 && sed.args[0] == "-");
+ assert(len(sed.opts) == 2);
+ assert(sed.opts[0].0 == 'e' && sed.opts[0].1 as parameter == "s/C++//g");
+ assert(sed.opts[1].0 == 'f' && sed.opts[1].1 as parameter == "/tmp/turing.sed");
+};