hare

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

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:
Mcmd/hare/main.ha | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcmd/hare/subcmds.ha | 109+++++++++++--------------------------------------------------------------------
Mcmd/harec/main.ha | 2+-
Mgetopt/getopts.ha | 221+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
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 = {