commit 46e3da37b31120d66cba2cdd1b450903c2469878
parent f9c311ea2cf2fdd7f13a0c73940b88af8f3fe3ba
Author: Autumn! <autumnull@posteo.net>
Date: Thu, 20 Apr 2023 20:31:34 +0000
getopt: add subcommands and error handling
Signed-off-by: Autumn! <autumnull@posteo.net>
Diffstat:
4 files changed, 245 insertions(+), 201 deletions(-)
diff --git a/cmd/hare/main.ha b/cmd/hare/main.ha
@@ -9,36 +9,96 @@ def VERSION: str = "unknown";
def PLATFORM: str = "unknown";
def HAREPATH: str = ".";
+const help: []getopt::help = [
+ "compile, run, and test Hare programs",
+ "<subcommand>",
+ "args...",
+ ("build", [
+ "compiles the Hare program at <path>",
+ ('c', "build object instead of executable"),
+ ('v', "print executed commands"),
+ ('D', "ident[:type]=value", "define a constant"),
+ ('j', "jobs", "set parallelism for build"),
+ ('L', "libdir", "add directory to linker library search path"),
+ ('l', "name", "link with a system library"),
+ ('N', "namespace", "override namespace for module"),
+ ('o', "path", "set output file name"),
+ ('t', "arch", "set target architecture"),
+ ('T', "tags...", "set build tags"),
+ ('X', "tags...", "unset build tags"),
+ "<path>"
+ ]: []getopt::help),
+ ("cache", [
+ "manages the build cache",
+ ('c', "cleans the specified modules"),
+ "modules...",
+ ]: []getopt::help),
+ ("deps", [
+ "prints dependency information for a Hare program",
+ ('d', "print dot syntax for use with graphviz"),
+ ('M', "build-dir", "print rules for POSIX make"),
+ ('T', "tags...", "set build tags"),
+ ('X', "tags...", "unset build tags"),
+ "<path|module>",
+ ]: []getopt::help),
+ ("release", [
+ "prepares a new release for a program or library",
+ ('d', "enable dry-run mode; do not perform any changes"),
+ "<major|minor|patch|x.y.z>",
+ ]: []getopt::help),
+ ("run", [
+ "compiles and runs the Hare program at <path>",
+ ('v', "print executed commands"),
+ ('D', "ident[:type]=value", "define a constant"),
+ ('j', "jobs", "set parallelism for build"),
+ ('L', "libdir", "add directory to linker library search path"),
+ ('l', "name", "link with a system library"),
+ ('T', "tags...", "set build tags"),
+ ('X', "tags...", "unset build tags"),
+ "<path>", "<args...>",
+ ]: []getopt::help),
+ ("test", [
+ "compiles and runs tests for Hare programs",
+ ('v', "print executed commands"),
+ ('D', "ident[:type]=value", "define a constant"),
+ ('j', "jobs", "set parallelism for build"),
+ ('L', "libdir", "add directory to linker library search path"),
+ ('l', "name", "link with a system library"),
+ ('o', "path", "set output file name"),
+ ('T', "tags...", "set build tags"),
+ ('X', "tags...", "unset build tags"),
+ "[tests...]"
+ ]: []getopt::help),
+ ("version", [
+ "provides version information for the Hare environment",
+ ('v', "print build parameters"),
+ ]: []getopt::help),
+];
+
export fn main() void = {
- const help: []getopt::help = [
- "compile, run, and test Hare programs",
- "<build | cache | deps | release | run | test | version>",
- "args...",
- ];
const cmd = getopt::parse(os::args, help...);
defer getopt::finish(&cmd);
- if (len(cmd.args) < 1) {
- getopt::printusage(os::stderr, os::args[0], help...);
- os::exit(1);
- };
- const task = switch (cmd.args[0]) {
- case "build" =>
- yield &build;
- case "cache" =>
- yield &cache;
- case "deps" =>
- yield &deps;
- case "release" =>
- yield &release;
- case "run" =>
- yield &run;
- case "test" =>
- yield &test;
- case "version" =>
- yield &version;
- case =>
- getopt::printusage(os::stderr, os::args[0], help...);
+ match (cmd.subcmd) {
+ case void =>
+ getopt::printusage(os::stderr, os::args[0], help...)!;
os::exit(1);
+ case let subcmd: (str, *getopt::command) =>
+ const task = switch (subcmd.0) {
+ case "build" =>
+ yield &build;
+ case "cache" =>
+ yield &cache;
+ case "deps" =>
+ yield &deps;
+ case "release" =>
+ yield &release;
+ case "run" =>
+ yield &run;
+ case "test" =>
+ yield &test;
+ case "version" =>
+ yield &version;
+ };
+ task(subcmd.1);
};
- task(cmd.args);
};
diff --git a/cmd/hare/subcmds.ha b/cmd/hare/subcmds.ha
@@ -62,25 +62,7 @@ type goal = enum {
EXE,
};
-fn build(args: []str) void = {
- const help: []getopt::help = [
- "compiles the Hare program at <path>",
- ('c', "build object instead of executable"),
- ('v', "print executed commands"),
- ('D', "ident[:type]=value", "define a constant"),
- ('j', "jobs", "set parallelism for build"),
- ('L', "libdir", "add directory to linker library search path"),
- ('l', "name", "link with a system library"),
- ('N', "namespace", "override namespace for module"),
- ('o', "path", "set output file name"),
- ('t', "arch", "set target architecture"),
- ('T', "tags...", "set build tags"),
- ('X', "tags...", "unset build tags"),
- "<path>"
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn build(cmd: *getopt::command) void = {
let build_target = default_target();
let tags = module::tags_dup(build_target.tags);
defer module::tags_free(tags);
@@ -152,7 +134,7 @@ fn build(args: []str) void = {
if (len(cmd.args) == 0) os::getcwd()
else if (len(cmd.args) == 1) cmd.args[0]
else {
- getopt::printusage(os::stderr, args[0], help...);
+ getopt::printusage(os::stderr, "build", cmd.help...)!;
os::exit(1);
};
@@ -200,19 +182,11 @@ fn build(args: []str) void = {
match (plan_execute(&plan, verbose)) {
case void => void;
case !exec::exit_status =>
- fmt::fatalf("{} {}: build failed", os::args[0], os::args[1]);
+ fmt::fatalf("{} build: build failed", os::args[0]);
};
};
-fn cache(args: []str) void = {
- const help: []getopt::help = [
- "manages the build cache",
- ('c', "cleans the specified modules"),
- "modules...",
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn cache(cmd: *getopt::command) void = {
abort("cache subcommand not implemented yet."); // TODO
};
@@ -222,18 +196,7 @@ type deps_goal = enum {
TERM,
};
-fn deps(args: []str) void = {
- const help: []getopt::help = [
- "prints dependency information for a Hare program",
- ('d', "print dot syntax for use with graphviz"),
- ('M', "build-dir", "print rules for POSIX make"),
- ('T', "tags...", "set build tags"),
- ('X', "tags...", "unset build tags"),
- "<path|module>",
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn deps(cmd: *getopt::command) void = {
let build_target = default_target();
let tags = module::tags_dup(build_target.tags);
defer module::tags_free(tags);
@@ -271,7 +234,7 @@ fn deps(args: []str) void = {
if (len(cmd.args) == 0) os::getcwd()
else if (len(cmd.args) == 1) cmd.args[0]
else {
- getopt::printusage(os::stderr, args[0], help...);
+ getopt::printusage(os::stderr, "deps", cmd.help...)!;
os::exit(1);
};
@@ -342,15 +305,7 @@ fn deps(args: []str) void = {
};
};
-fn release(args: []str) void = {
- const help: []getopt::help = [
- "prepares a new release for a program or library",
- ('d', "enable dry-run mode; do not perform any changes"),
- "<major|minor|patch|x.y.z>",
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn release(cmd: *getopt::command) void = {
let dryrun = false;
for (let i = 0z; i < len(cmd.opts); i += 1) {
let opt = cmd.opts[i];
@@ -362,7 +317,7 @@ fn release(args: []str) void = {
};
if (len(cmd.args) == 0) {
- getopt::printusage(os::stderr, "release", help);
+ getopt::printusage(os::stderr, "release", cmd.help)!;
os::exit(1);
};
@@ -376,7 +331,7 @@ fn release(args: []str) void = {
case =>
yield match (parseversion(cmd.args[0])) {
case badversion =>
- getopt::printusage(os::stderr, "release", help);
+ getopt::printusage(os::stderr, "release", cmd.help)!;
os::exit(1);
case let ver: modversion =>
yield ver;
@@ -400,21 +355,7 @@ fn release(args: []str) void = {
};
};
-fn run(args: []str) void = {
- const help: []getopt::help = [
- "compiles and runs the Hare program at <path>",
- ('v', "print executed commands"),
- ('D', "ident[:type]=value", "define a constant"),
- ('j', "jobs", "set parallelism for build"),
- ('L', "libdir", "add directory to linker library search path"),
- ('l', "name", "link with a system library"),
- ('T', "tags...", "set build tags"),
- ('X', "tags...", "unset build tags"),
- "<path>", "<args...>",
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn run(cmd: *getopt::command) void = {
const build_target = default_target();
let tags = module::tags_dup(build_target.tags);
defer module::tags_free(tags);
@@ -503,7 +444,7 @@ fn run(args: []str) void = {
match (plan_execute(&plan, verbose)) {
case void => void;
case !exec::exit_status =>
- fmt::fatalf("{} {}: build failed", os::args[0], os::args[1]);
+ fmt::fatalf("{} run: build failed", os::args[0]);
};
const cmd = match (exec::cmd(output, runargs...)) {
case let err: exec::error =>
@@ -515,22 +456,7 @@ fn run(args: []str) void = {
exec::exec(&cmd);
};
-fn test(args: []str) void = {
- const help: []getopt::help = [
- "compiles and runs tests for Hare programs",
- ('v', "print executed commands"),
- ('D', "ident[:type]=value", "define a constant"),
- ('j', "jobs", "set parallelism for build"),
- ('L', "libdir", "add directory to linker library search path"),
- ('l', "name", "link with a system library"),
- ('o', "path", "set output file name"),
- ('T', "tags...", "set build tags"),
- ('X', "tags...", "unset build tags"),
- "[tests...]"
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn test(cmd: *getopt::command) void = {
const build_target = default_target();
let tags = module::tags_dup(build_target.tags);
append(tags, module::tag {
@@ -637,7 +563,7 @@ fn test(args: []str) void = {
match (plan_execute(&plan, verbose)) {
case void => void;
case !exec::exit_status =>
- fmt::fatalf("{} {}: build failed", os::args[0], os::args[1]);
+ fmt::fatalf("{} test: build failed", os::args[0]);
};
if (have_output) {
@@ -654,14 +580,7 @@ fn test(args: []str) void = {
exec::exec(&cmd);
};
-fn version(args: []str) void = {
- const help: []getopt::help = [
- "provides version information for the Hare environment",
- ('v', "print build parameters"),
- ];
- const cmd = getopt::parse(args, help...);
- defer getopt::finish(&cmd);
-
+fn version(cmd: *getopt::command) void = {
let verbose = false;
for (let i = 0z; i < len(cmd.opts); i += 1) {
// The only option is verbose
diff --git a/cmd/harec/main.ha b/cmd/harec/main.ha
@@ -47,7 +47,7 @@ export fn main() void = {
};
if (len(cmd.args) == 0) {
- getopt::printusage(os::stderr, os::args[0], usage);
+ getopt::printusage(os::stderr, os::args[0], usage)!;
os::exit(1);
};
diff --git a/getopt/getopts.ha b/getopt/getopts.ha
@@ -5,7 +5,6 @@
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2021 Jonathan Halmen <slowjo@halmen.xyz>
// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz>
-use encoding::utf8;
use fmt;
use io;
use os;
@@ -24,7 +23,9 @@ export type option = (flag, parameter);
// options specified and the list of non-option arguments.
export type command = struct {
opts: []option,
+ subcmd: (void | (str, *command)),
args: []str,
+ help: []help,
};
// Help text providing a short, one-line summary of the command; or providing
@@ -38,6 +39,9 @@ export type flag_help = (flag, str);
// is the first string and "help text" is the second string.
export type parameter_help = (flag, str, str);
+// Definition of a named subcommand.
+export type subcmd_help = (str, []help);
+
// Help text for a command or option. [[cmd_help]], [[flag_help]], and
// [[parameter_help]] compose such that the following []help:
//
@@ -60,14 +64,35 @@ export type parameter_help = (flag, str, str);
// -b: b help text
// -c <cflag>: c help text
// -d <dflag>: d help text
-export type help = (cmd_help | flag_help | parameter_help);
+export type help = (cmd_help | flag_help | parameter_help | subcmd_help);
+
+export type requires_arg = !rune;
+export type unknown_option = !rune;
+export type unknown_subcmd = !str;
+export type error = !(requires_arg | unknown_option | unknown_subcmd);
-// 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.
+// A wrapper for [[tryparse]] in which if an error occurs, details are printed
+// to [[os::stderr]] and [[os::exit]] is called with a nonzero exit status.
export fn parse(args: []str, help: help...) command = {
+ const cmd = args[0];
+ match (tryparse(args, help...)) {
+ case let c: command => return c;
+ case let r: requires_arg =>
+ fmt::errorfln("{}: option -{} requires an argument", cmd, r: rune)!;
+ case let r: unknown_option =>
+ fmt::errorfln("{}: unrecognized option: -{}", cmd, r: rune)!;
+ case let s: unknown_subcmd =>
+ fmt::errorfln("{}: unrecognized subcommand: {}", cmd, s: str)!;
+ printsubcmds(os::stderr, help)!;
+ };
+ printusage(os::stderr, args[0], help)!;
+ os::exit(1);
+};
+
+// Parses command line arguments and returns a [[command]], or an [[error]]
+// if an error occurs. The argument list must include the command name as
+// the first item; [[os::args]] fulfills this criteria.
+export fn tryparse(args: []str, help: help...) (command | ...error) = {
let opts: []option = [];
let i = 1z;
for (i < len(args); i += 1) :arg {
@@ -81,60 +106,71 @@ export fn parse(args: []str, help: help...) command = {
break;
};
- let d = utf8::decode(arg);
- assert(utf8::next(&d) as rune == '-');
- let next = utf8::next(&d);
- for (next is rune; next = utf8::next(&d)) :flag {
- const r = next as rune;
- for (let j = 0z; j < len(help); j += 1) :help {
- let p: parameter_help = match (help[j]) {
- case cmd_help =>
- continue :help;
- case let f: flag_help =>
- if (r == f.0) {
- append(opts, (r, ""));
- continue :flag;
- } else {
- continue :help;
- };
- case let p: parameter_help =>
- yield if (r == p.0) p else
- continue :help;
+ let iter = strings::iter(arg);
+ assert(strings::next(&iter) as rune == '-');
+ for (true) match (strings::next(&iter)) {
+ case void => break;
+ case let r: rune =>
+ let found = false;
+ for (let j = 0z; j < len(help); j += 1) match (help[j]) {
+ case let f: flag_help =>
+ if (r == f.0) {
+ append(opts, (r, ""));
+ found = true;
+ break;
};
- if (len(d.src) == d.offs) {
- if (i + 1 >= len(args)) {
- errmsg(args[0], "option requires an argument: ",
- r, help);
- os::exit(1);
+ case let p: parameter_help =>
+ if (r == p.0) {
+ let value = strings::iterstr(&iter);
+ if (len(value) == 0) {
+ if (i == len(args) - 1) {
+ free(opts);
+ return r: requires_arg;
+ };
+ i += 1;
+ append(opts, (r, args[i]));
+ } else {
+ append(opts, (r, value));
};
- i += 1;
- append(opts, (r, args[i]));
- } else {
- let s = strings::fromutf8(d.src[d.offs..])!;
- append(opts, (r, s));
+ continue :arg;
};
- continue :arg;
+ case =>
+ continue;
};
+ if (found) continue;
if (r =='h') {
- printhelp(os::stderr, args[0], help);
+ printhelp(os::stderr, args[0], help)!;
os::exit(0);
};
- errmsg(args[0], "unrecognized option: ", r, help);
- os::exit(1);
+ free(opts);
+ return r: unknown_option;
};
- match (next) {
- case void => void;
- case rune =>
- abort(); // Unreachable
- case (utf8::more | utf8::invalid) =>
- errmsg(args[0], "invalid UTF-8 in arguments", void,
- help);
- os::exit(1);
+ };
+ let subcmd: (void | (str, *command)) = void;
+ if (i < len(args)) {
+ let expects_subcmd = false;
+ for (let j = 0z; j < len(help); j += 1) match (help[j]) {
+ case let s: subcmd_help =>
+ expects_subcmd = true;
+ if (s.0 == args[i]) match (tryparse(args[i..], s.1...)) {
+ case let c: command =>
+ subcmd = (s.0, alloc(c));
+ case let e: error =>
+ free(opts);
+ return e;
+ };
+ case => continue;
+ };
+ if (expects_subcmd && subcmd is void) {
+ free(opts);
+ return args[i]: unknown_subcmd;
};
};
return command {
opts = opts,
- args = args[i..],
+ subcmd = subcmd,
+ args = if (subcmd is void) args[i..] else [],
+ help = help,
};
};
@@ -142,88 +178,117 @@ export fn parse(args: []str, help: help...) command = {
export fn finish(cmd: *command) void = {
if (cmd == null) return;
free(cmd.opts);
+ match (cmd.subcmd) {
+ case void => void;
+ case let s: (str, *command) =>
+ finish(s.1);
+ free(s.1);
+ };
};
-fn _printusage(out: io::handle, name: str, indent: bool, help: []help) size = {
- let z = fmt::fprint(out, "Usage:", name) as size;
+fn _printusage(
+ out: io::handle,
+ name: str,
+ indent: bool,
+ help: []help,
+) (size | io::error) = {
+ let z = fmt::fprint(out, "Usage:", name)?;
let started_flags = false;
for (let i = 0z; i < len(help); i += 1) if (help[i] is flag_help) {
if (!started_flags) {
- z += fmt::fprint(out, " [-") as size;
+ z += fmt::fprint(out, " [-")?;
started_flags = true;
};
const help = help[i] as flag_help;
- z += fmt::fprint(out, help.0: rune) as size;
+ z += fmt::fprint(out, help.0: rune)?;
};
if (started_flags) {
- z += fmt::fprint(out, "]") as size;
+ z += fmt::fprint(out, "]")?;
};
for (let i = 0z; i < len(help); i += 1) if (help[i] is parameter_help) {
const help = help[i] as parameter_help;
if (indent) {
- z += fmt::fprintf(out, "\n\t") as size;
+ z += fmt::fprintf(out, "\n\t")?;
};
- z += fmt::fprintf(out, " [-{} <{}>]", help.0: rune, help.1) as size;
+ z += fmt::fprintf(out, " [-{} <{}>]", help.0: rune, help.1)?;
};
let first_arg = true;
for (let i = 1z; i < len(help); i += 1) if (help[i] is cmd_help) {
if (first_arg) {
if (indent) {
- z += fmt::fprintf(out, "\n\t") as size;
+ z += fmt::fprintf(out, "\n\t")?;
};
first_arg = false;
};
- z += fmt::fprintf(out, " {}", help[i] as cmd_help: str) as size;
+ z += fmt::fprintf(out, " {}", help[i] as cmd_help: str)?;
};
- return z + fmt::fprint(out, "\n") as size;
+ return z + fmt::fprint(out, "\n")?;
};
// Prints command usage to the provided stream.
-export fn printusage(out: io::handle, name: str, help: []help) void = {
- let z = _printusage(io::empty, name, false, help);
- _printusage(out, name, if (z > 72) true else false, help);
+export fn printusage(
+ out: io::handle,
+ name: str,
+ help: []help
+) (void | io::error) = {
+ let z = _printusage(io::empty, name, false, help)?;
+ _printusage(out, name, if (z > 72) true else false, help)?;
};
// Prints command help to the provided stream.
-export fn printhelp(out: io::handle, name: str, help: []help) void = {
+export fn printhelp(
+ out: io::handle,
+ name: str,
+ help: []help
+) (void | io::error) = {
if (len(help) == 0) {
return;
};
if (help[0] is cmd_help) {
- fmt::fprintfln(out, "{}: {}\n", name, help[0] as cmd_help: str)!;
+ fmt::fprintfln(out, "{}: {}\n", name, help[0] as cmd_help: str)?;
};
- printusage(out, name, help);
+ printusage(out, name, help)?;
for (let i = 0z; i < len(help); i += 1) match (help[i]) {
- case cmd_help => void;
case (flag_help | parameter_help) =>
// Only print this if there are flags to show
- fmt::fprint(out, "\n")!;
+ fmt::fprint(out, "\n")?;
break;
+ case => void;
};
for (let i = 0z; i < len(help); i += 1) match (help[i]) {
- case cmd_help => void;
case let f: flag_help =>
- fmt::fprintfln(out, "-{}: {}", f.0: rune, f.1)!;
+ fmt::fprintfln(out, "-{}: {}", f.0: rune, f.1)?;
case let p: parameter_help =>
- fmt::fprintfln(out, "-{} <{}>: {}", p.0: rune, p.1, p.2)!;
+ fmt::fprintfln(out, "-{} <{}>: {}", p.0: rune, p.1, p.2)?;
+ case => void;
};
+
+ printsubcmds(out, help)?;
};
-fn errmsg(name: str, err: str, opt: (rune | void), help: []help) void = {
- fmt::errorfln("{}: {}{}", name, err, match (opt) {
- case let r: rune =>
- yield r;
- case void =>
- yield "";
- })!;
- printusage(os::stderr, name, help);
+fn printsubcmds(out: io::handle, help: []help) (void | io::error) = {
+ let first_subcmd = true;
+ for (let i = 0z; i < len(help); i += 1) match (help[i]) {
+ case let s: subcmd_help =>
+ // Only print this if there are subcommands to show
+ if (first_subcmd) {
+ fmt::fprintln(out, "\nSubcommands:")?;
+ first_subcmd = false;
+ };
+ if (len(s.1) == 0 || !(s.1[0] is cmd_help)) {
+ fmt::fprintfln(out, " {}", s.0)?;
+ } else {
+ fmt::fprintfln(out, " {}: {}", s.0, s.1[0] as cmd_help: str)?;
+ };
+ case => void;
+ };
};
@test fn parse() void = {