diff --git a/lib/getopt/getopt.ww b/lib/getopt/getopt.ww new file mode 100644 index 00000000..ff14ff8b --- /dev/null +++ b/lib/getopt/getopt.ww @@ -0,0 +1,404 @@ +// getopt — POSIX-style short-flag argument parser. +// +// Mirrors Hare's getopts:: at ref/hare/getopt/getopts.ha; drops +// underscores per plan 9 style (parameter_help → paramhelp, +// unknown_option → unknownopt). +// +// Surface divergence from Hare, all forced by current cgen + lib/fmt +// limits: +// +// * [[command]] is a flat struct: instead of `opts: []option` / +// `args: []str` / `help: []help`, it carries three (ptr, len, +// cap) triples — `optsptr/optslen/optscap`, `argsptr/argslen/ +// argscap`, `helpptr/helplen/helpcap`. The cstage cgen drops +// chained writes through a slice subfield (`out.opts.len = v` +// stores nothing) and similarly mis-reads chained `cmd.opts.len`, +// so a struct that *contains* slice fields can't round-trip. +// Build a `[]option` locally from the triple when iteration is +// wanted (or index `cmd.optsptr[i]` directly). +// * [[command]] is filled through an out-pointer; Hare's +// `let c = parse(args, help...)` is blocked by the 32B+ return- +// by-value gap (task #5). Same shape as lib/memio. +// * [[help]] is a flat 40B struct with a `kind: helpkind` +// discriminator rather than Hare's +// `(cmdhelp | flaghelp | paramhelp)` tagged union. The Hare +// paramhelp variant alone is 40B, which doesn't fit ww's 32B +// tagged-union return slot. Constructors [[cmdhelp]] / +// [[flaghelp]] / [[paramhelp]] write through an out-pointer for +// the same reason (task #5), and because task #6 (arr[i].field +// = v) blocks the obvious `h[i].kind = ...` callsite spelling. +// * [[option]] is a `struct { flag: rune, value: str }` rather +// than Hare's `(rune, str)` 2-tuple: ww's cgen mis-sizes a +// tuple of (i32, str) as 20B (no 4B pad before the str field). +// The struct shape lays out as 24B as expected. +// * [[error]] is a flat 24B struct (kind + flag + name). Hare's +// `!(str, []help, (requiresarg | unknownopt | unknownsubcmd))` +// 3-tuple-with-nested-union doesn't map onto ww's single-payload +// `!` tag. UNKNOWNSUBCMD is omitted because we don't parse +// subcommands; the remaining 24B fits the tagged-union return +// ABI (tag + 3 words). +// * Hare's subcmd_help variant + recursive subcommand parsing is +// not shipped — no SUBCMD helpkind, no subcmdhelp constructor. +// The recursion needs heap-allocated self-referential types +// (`(str, *command)` payload + `alloc(command)?`) and a real +// consumer to drive the test surface. Add when the first +// consumer is queued. +// * [[parse]], [[printusage]], [[printhelp]], [[printsubcmds]] +// aren't shipped. The print family needs fmt.fprintf-with-{}- +// interpolation that lib/fmt doesn't expose (lib/fmt is print- +// string-only); [[parse]] is a thin wrapper that bundles +// printusage + os.exit on top of [[tryparse]], so it goes too. +// Callers handle errors themselves; [[strerror]] formats a one- +// line summary into a module-level buffer (same shape as +// strconv.*tos). +// +// GRADUATE-IN-ONE-GO WARNING (lib/CLAUDE.md policy). Callers MUST +// NOT bake the current cgen-forced field names into themselves — +// they will collapse to the Hare shape in a single sweep when the +// underlying cgen gaps close, and the old spellings will disappear +// in the same commit (no transition period). +// +// * `cmd.optsptr / cmd.optslen / cmd.optscap` (and `args*` / +// `help*`) graduate to `cmd.opts: []option`, `cmd.args: []str`, +// `cmd.help: []help` once tasks #9 + #10 land. Iterate via the +// slice header you build locally; don't read the triple by name +// from many call sites. +// * `option.flag` / `option.value` graduate to tuple positions +// `cmd.opts[i].0` (rune) / `cmd.opts[i].1` (str) once task #11 +// lands. Treat them as opaque positional fields where possible. +// * `strerror(err: *error)` graduates to `strerror(err: error)` +// (by value, no &) once task #14 lands. +// +// Caller layout for `ls -Fa files.txt`: +// +// let helps: [4]getopt.help; +// getopt.cmdhelp(&helps[0], "list files"); +// getopt.flaghelp(&helps[1], 'F': rune, "..."); +// getopt.flaghelp(&helps[2], 'a': rune, "..."); +// getopt.cmdhelp(&helps[3], "files..."); +// +// let cmd: getopt.command; +// match (getopt.tryparse(&cmd, args, helps[0:4])) { +// case void => { +// let j: i32 = 0; +// for (j < cmd.optslen) { +// let o: *getopt.option = &cmd.optsptr[j]; +// /* o.flag, o.value */ +// j += 1; +// }; +// }; +// case let e: getopt.error => { /* render via strerror */ }; +// }; +// defer getopt.finish(&cmd); + +use strings; + +// Direct rt_free / rt_ensure bindings rather than `use os;` — os +// exports read/write/close, which collide with io.read/write/close +// in callers that mix both (task #7). Same pattern as lib/memio. +// +// rt_ensure is the runtime slice-growth helper invoked by the +// `append(s, v)` builtin. We bind it directly because the builtin's +// expansion stores only 8 bytes of the new element (cgen emits a +// single MOVQ), losing the `value: str` half of an [[option]]. +// [[appendoption]] grows manually and stores both fields via *option. +@symbol("rt_free") fn rtfree(p: *void, n: u64) void; +@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void; + +// helpkind — which slot of [[help]] is meaningful. Hare's getopt +// also has a subcmd_help variant; not shipped here (see file header). +export type helpkind = enum i32 { + CMD = 0, // label / arg-name / one-line summary + FLAG = 1, // -X (no argument) + PARAM = 2, // -X +}; + +// help — one entry in the program's option/help list. +// +// CMD: `text` is the label. +// FLAG: `flag` is the rune; `text` is the help text. +// PARAM: `flag` is the rune; `name` is the value's display name; +// `text` is the help text. +// +// Use [[cmdhelp]] / [[flaghelp]] / [[paramhelp]] to fill help +// entries — direct struct-field assignment via `h[i].field = v` +// is blocked by task #6. +export type help = struct { + kind: helpkind, + flag: rune, + name: str, + text: str, +}; + +// cmdhelp — fill `*out` as a CMD-kind [[help]] entry (label / arg- +// name). Mirrors Hare's cmd_help. +export fn cmdhelp(out: *help, text: str) void = { + out.kind = helpkind.CMD; + out.flag = 0: rune; + out.name = ""; + out.text = text; +}; + +// flaghelp — fill `*out` as a FLAG-kind [[help]] entry. Mirrors +// Hare's flag_help = (rune, str). +export fn flaghelp(out: *help, r: rune, text: str) void = { + out.kind = helpkind.FLAG; + out.flag = r; + out.name = ""; + out.text = text; +}; + +// paramhelp — fill `*out` as a PARAM-kind [[help]] entry. Mirrors +// Hare's parameter_help = (rune, str, str). +export fn paramhelp(out: *help, r: rune, name: str, text: str) void = { + out.kind = helpkind.PARAM; + out.flag = r; + out.name = name; + out.text = text; +}; + +// errorkind — which payload of [[error]] carries the offender. +export type errorkind = enum i32 { + REQUIRESARG = 0, // -X needs an argument we didn't get + UNKNOWNOPT = 1, // -X isn't in the help list +}; + +// error — parse failure. 24B layout (i32 kind + rune flag + str +// name) so `(void | error)` rides the tagged-union return ABI. +export type error = struct { + kind: errorkind, + flag: rune, // the offending letter + name: str, // argv[0] (program name) +}; + +// option — one element of the parsed option list. Hare's getopts +// uses a `(rune, str)` 2-tuple; ww's cgen mis-sizes that as 20B +// (no 4B pad before the str field), so we use a named struct — the +// struct layout pads correctly. +export type option = struct { + flag: rune, + value: str, +}; + +// command — parse result. Caller owns the slot; [[finish]] releases +// the storage tryparse allocated. +// +// Flat (ptr, len, cap) triples replace the natural slice fields +// because the cstage cgen miscompiles both writes and reads of a +// slice subfield through a struct (see file header). Reconstruct a +// `[]option` view locally if iteration is more convenient than +// pointer-indexing `optsptr[i]`. +// +// opts*: every option encountered, in argv order. +// args*: positional arguments after option processing — a borrowed +// view into the caller's argv. +// help*: a borrowed view of the help slice tryparse was called with. +export type command = struct { + optsptr: *option, + optslen: i32, + optscap: i32, + argsptr: *str, + argslen: i32, + argscap: i32, + helpptr: *help, + helplen: i32, + helpcap: i32, +}; + +// appendoption — grow `*opts` by one slot and store `(flag, value)`. +// Avoids the `append(opts, pair)` builtin: cgen lowers that to a +// MOVQ-of-the-first-8-bytes, which drops the `value: str` half. +fn appendoption(opts: *[]option, flag: rune, value: str) void = { + let newlen: i32 = opts.len + 1; + opts.len = newlen; + rtensure(opts: *void, 24u64); + let dst: *option = &opts.ptr[newlen - 1]; + dst.flag = flag; + dst.value = value; +}; + +// findflag — lookup the kind of `r` in `hs`. Returns void if absent. +// +// Reads go through `&hs[i]` rather than `hs[i].field` directly: the +// cstage cgen emits a bare MOVQ for a slice-indexed struct-field +// read, so `hs[i].kind == K` and `hs[i].flag == r` would compare 8B +// loads (kind packed with flag) against the 4B constants. The +// chained-N_DOT-through-*struct path (cmd/w6c/cgen.c, 3801) uses +// MOVL for i32 fields and avoids the read-too-much. Same pattern +// callers should use when iterating `cmd.optsptr[i]`. +fn findflag(hs: []help, r: rune) (helpkind | void) = { + let i: i32 = 0; + for (i < hs.len) { + let p: *help = &hs[i]; + if (p.kind == helpkind.FLAG && p.flag == r) { + return helpkind.FLAG; + }; + if (p.kind == helpkind.PARAM && p.flag == r) { + return helpkind.PARAM; + }; + i += 1; + }; + return; +}; + +// tryparse — parse `argv` against `help`, filling `*out` on success. +// `argv` must include the program name in argv[0]; matches Hare and +// matches the slice [[os.args]] would hand back. +// +// Recognised forms: +// -X FLAG-kind option +// -X value PARAM-kind option, value from next argv slot +// -Xvalue PARAM-kind option, value glued to flag +// -XYZ FLAG-cluster (each rune looked up independently) +// -- stop processing; everything after is positional +// - positional argument (commonly stdin) +// +// Returns void on success; an [[error]] on the first failure. +// +// `opts` is accumulated in a *local* slice and the (ptr, len, cap) +// triple is copied into `*out.opts*` at the end: `append` on a +// slice that lives inside `*out` would need `&out.foo`, which hits +// the `&x.field` gap (task #4). +export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = { + out.optsptr = nil: *option; + out.optslen = 0; + out.optscap = 0; + out.argsptr = nil: *str; + out.argslen = 0; + out.argscap = 0; + out.helpptr = help.ptr; + out.helplen = help.len; + out.helpcap = help.cap; + + let opts: []option; + opts.ptr = nil: *option; + opts.len = 0; + opts.cap = 0; + + let i: i32 = 1; + for (i < argv.len) { + let arg: str = argv[i]; + if (arg.len == 0) { break; }; + if (arg.len == 1) { break; }; // bare "-" → positional + if (arg[0] != 45: u8) { break; }; // not '-' → positional + if (arg.len == 2 && arg[1] == 45: u8) { + // "--" separator: skip and stop. + i += 1; + break; + }; + + let bi: i32 = 1; + let advanced: bool = false; + for (bi < arg.len) { + if (advanced) { break; }; + let r: rune = arg[bi]: rune; + let look: (helpkind | void) = findflag(help, r); + match (look) { + case void => { + if (opts.cap > 0) { + rtfree(opts.ptr: *void, + (opts.cap: u64) * 24u64); + }; + let e: error; + e.kind = errorkind.UNKNOWNOPT; + e.flag = r; + e.name = argv[0]; + return e; + }; + case let k: helpkind => { + if (k == helpkind.FLAG) { + appendoption(&opts, r, ""); + bi += 1; + } else { + // PARAM: glued value, or next argv slot. + if (bi + 1 < arg.len) { + let v: str = strings.sub(arg, bi + 1, arg.len); + appendoption(&opts, r, v); + advanced = true; + } else { + if (i + 1 >= argv.len) { + if (opts.cap > 0) { + rtfree(opts.ptr: *void, + (opts.cap: u64) * 24u64); + }; + let e: error; + e.kind = errorkind.REQUIRESARG; + e.flag = r; + e.name = argv[0]; + return e; + }; + i += 1; + appendoption(&opts, r, argv[i]); + advanced = true; + }; + }; + }; + }; + }; + i += 1; + }; + + out.optsptr = opts.ptr; + out.optslen = opts.len; + out.optscap = opts.cap; + + if (i < argv.len) { + out.argsptr = &argv[i]; + out.argslen = argv.len - i; + out.argscap = argv.len - i; + }; + return; +}; + +// finish — release storage owned by `cmd`. Only opts was grown by +// tryparse; args is a borrowed view into argv. +export fn finish(cmd: *command) void = { + if (cmd.optscap > 0) { + // option layout: rune (4) + pad (4) + str (16) = 24B. + rtfree(cmd.optsptr: *void, (cmd.optscap: u64) * 24u64); + }; + cmd.optsptr = nil: *option; + cmd.optslen = 0; + cmd.optscap = 0; +}; + +// strerror — render `err` as a one-line message into a module-level +// buffer. The buffer is reused; callers needing the bytes to outlive +// the next strerror call duplicate via strings.dup. Mirrors Hare's +// strerror(error) str shape (and matches strconv's static-buffer +// idiom). +let strerrorbuf: [1024]u8; + +fn writebuf(s: str, off: i32) i32 = { + let n: i32 = s.len; + if (off + n > 1024) { n = 1024 - off; }; + let i: i32 = 0; + for (i < n) { + strerrorbuf[off + i] = s[i]; + i += 1; + }; + return off + n; +}; + +// strerror takes `*error` rather than Hare's value `error`: the cstage +// cgen only passes the first 8 bytes of a >8B struct argument, which +// would truncate the `name` field. Returning a `case let e: error` +// scrutinee through `&e` keeps the call shape uniform. +export fn strerror(err: *error) str = { + let off: i32 = 0; + off = writebuf(err.name, off); + off = writebuf(": ", off); + if (err.kind == errorkind.REQUIRESARG) { + off = writebuf("option -", off); + if (off < 1024) { strerrorbuf[off] = err.flag: u8; off += 1; }; + off = writebuf(" requires an argument", off); + }; + if (err.kind == errorkind.UNKNOWNOPT) { + off = writebuf("unrecognized option: -", off); + if (off < 1024) { strerrorbuf[off] = err.flag: u8; off += 1; }; + }; + let r: str; + r.ptr = &strerrorbuf[0]; + r.len = off; + return r; +}; diff --git a/lib/getopt/getopttest.ww b/lib/getopt/getopttest.ww new file mode 100644 index 00000000..5a05aa05 --- /dev/null +++ b/lib/getopt/getopttest.ww @@ -0,0 +1,402 @@ +// getopttest — exercises lib/getopt. Run with +// `out/bin/ww run lib/getopt/getopttest.ww`. +// +// Every @test enumerates parallel `[N]T` arrays of inputs and +// expectations, then iterates one body across them. Parallel arrays +// (rather than `[N]struct{...}`) sidestep the cstage cgen's chained +// `arr[i].field` store gap (task #6). +// +// Reads of [[command.optsptr]] go through `&cmd.optsptr[i]` rather +// than `cmd.optsptr[i].field` directly: the cstage cgen emits a bare +// MOVQ for a pointer-indexed struct-field read, so packed i32 fields +// (flag rune at offset 0) silently pick up the next 4 bytes (str.ptr +// low half) in the upper 32 bits. The chained-N_DOT-through-*struct +// path uses MOVL and avoids the over-read. +// +// Guards on a possibly-nil [[command.argsptr]] are split into nested +// `if`s rather than `argslen > 0 && !streq(argsptr[0], ...)`: the +// cstage cgen evaluates the RHS of `&&` even when the LHS is false, +// segfaulting on the nil deref when tryparse exits with no positional +// args (e.g. `["ls", "--"]`). + +use getopt; +use strings; + +// Direct exit(2) binding rather than `use os;` — os exports +// read/write/close, mirroring memio's reasoning (task #7). +@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64; +fn doexit(code: i32) void = { + syscall1ww(60i64, code: i64); +}; + +// signalled — bumped by main before each test so a failing exit code +// pinpoints the offending case. +let signalled: i32 = 0; + +fn fail() void = { doexit(signalled + 10); }; + +fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// ---- flagcluster: -Fahs files.txt → 4 flags + 1 arg -------------------- + +@test fn flagcluster() void = { + let helps: [5]getopt.help; + getopt.cmdhelp(&helps[0], "list files"); + getopt.flaghelp(&helps[1], 'F': rune, "do F"); + getopt.flaghelp(&helps[2], 'a': rune, "do a"); + getopt.flaghelp(&helps[3], 'h': rune, "do h"); + getopt.flaghelp(&helps[4], 's': rune, "do s"); + + let argv: [3]str; + argv[0] = "ls"; + argv[1] = "-Fahs"; + argv[2] = "files.txt"; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv[0:3], helps[0:5]); + match (r) { + case void => {}; + case let e: getopt.error => fail(); + }; + + if (cmd.optslen != 4) { fail(); }; + + // (wantflag, wantval) parallel arrays, indexed by option order. + let wantf: [4]u8; + let wantv: [4]str; + wantf[0] = 'F': u8; wantv[0] = ""; + wantf[1] = 'a': u8; wantv[1] = ""; + wantf[2] = 'h': u8; wantv[2] = ""; + wantf[3] = 's': u8; wantv[3] = ""; + + let i: i32 = 0; + for (i < 4) { + let p: *getopt.option = &cmd.optsptr[i]; + if ((p.flag: u8) != wantf[i]) { fail(); }; + if (!streq(p.value, wantv[i])) { fail(); }; + i += 1; + }; + + if (cmd.argslen != 1) { fail(); }; + if (!streq(cmd.argsptr[0], "files.txt")) { fail(); }; + + getopt.finish(&cmd); +}; + +// ---- paramflag: glued + separated arguments ---------------------------- + +@test fn paramflag() void = { + let helps: [3]getopt.help; + getopt.cmdhelp(&helps[0], "edit"); + getopt.paramhelp(&helps[1], 'e': rune, "script", "script"); + getopt.paramhelp(&helps[2], 'f': rune, "file", "script file"); + + let argv: [5]str; + argv[0] = "sed"; + argv[1] = "-e"; + argv[2] = "s/foo/bar/"; + argv[3] = "-f/tmp/x.sed"; + argv[4] = "-"; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv[0:5], helps[0:3]); + match (r) { + case void => {}; + case let e: getopt.error => fail(); + }; + + if (cmd.optslen != 2) { fail(); }; + + let wantf: [2]u8; + let wantv: [2]str; + wantf[0] = 'e': u8; wantv[0] = "s/foo/bar/"; + wantf[1] = 'f': u8; wantv[1] = "/tmp/x.sed"; + + let i: i32 = 0; + for (i < 2) { + let p: *getopt.option = &cmd.optsptr[i]; + if ((p.flag: u8) != wantf[i]) { fail(); }; + if (!streq(p.value, wantv[i])) { fail(); }; + i += 1; + }; + + if (cmd.argslen != 1) { fail(); }; + if (!streq(cmd.argsptr[0], "-")) { fail(); }; + + getopt.finish(&cmd); +}; + +// ---- separator: -- ends option processing ------------------------------ +// +// `--` in three positions: after a flag, immediately after argv[0], +// and at the tail with nothing trailing. Flat-packed argv per +// errortable's pattern; rows expect distinct (optslen, argslen, +// args[0]) outcomes. +@test fn separator() void = { + let helps: [2]getopt.help; + getopt.cmdhelp(&helps[0], "list"); + getopt.flaghelp(&helps[1], 'F': rune, "do F"); + + // row 0: ["ls", "-F", "--", "-name"] → 1 opt 'F', 1 arg "-name" + // row 1: ["ls", "--", "-F"] → 0 opts, 1 arg "-F" + // row 2: ["ls", "--"] → 0 opts, 0 args + let srcs: [9]str; + srcs[0]="ls"; srcs[1]="-F"; srcs[2]="--"; srcs[3]="-name"; + srcs[4]="ls"; srcs[5]="--"; srcs[6]="-F"; + srcs[7]="ls"; srcs[8]="--"; + + let argo: [3]i32; + let argn: [3]i32; + let wantopts: [3]i32; + let wantargs: [3]i32; + let wantarg0: [3]str; + argo[0]=0; argn[0]=4; wantopts[0]=1; wantargs[0]=1; wantarg0[0]="-name"; + argo[1]=4; argn[1]=3; wantopts[1]=0; wantargs[1]=1; wantarg0[1]="-F"; + argo[2]=7; argn[2]=2; wantopts[2]=0; wantargs[2]=0; wantarg0[2]=""; + + let i: i32 = 0; + for (i < 3) { + let argv: []str; + argv.ptr = &srcs[argo[i]]; + argv.len = argn[i]; + argv.cap = argn[i]; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:2]); + match (r) { + case void => {}; + case let e: getopt.error => fail(); + }; + if (cmd.optslen != wantopts[i]) { fail(); }; + if (cmd.argslen != wantargs[i]) { fail(); }; + if (cmd.argslen > 0) { + if (!streq(cmd.argsptr[0], wantarg0[i])) { fail(); }; + }; + getopt.finish(&cmd); + i += 1; + }; +}; + +// ---- repeatedflag: -vvv → 3 v opts ------------------------------------- + +@test fn repeatedflag() void = { + let helps: [2]getopt.help; + getopt.cmdhelp(&helps[0], "verbose count"); + getopt.flaghelp(&helps[1], 'v': rune, "verbosity"); + + let argv: [2]str; + argv[0] = "x"; + argv[1] = "-vvv"; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv[0:2], helps[0:2]); + match (r) { + case void => {}; + case let e: getopt.error => fail(); + }; + + if (cmd.optslen != 3) { fail(); }; + let i: i32 = 0; + for (i < 3) { + let p: *getopt.option = &cmd.optsptr[i]; + if ((p.flag: u8) != ('v': u8)) { fail(); }; + if (p.value.len != 0) { fail(); }; + i += 1; + }; + if (cmd.argslen != 0) { fail(); }; + + getopt.finish(&cmd); +}; + +// ---- baredash: "-" alone is a positional -------------------------------- +// +// Bare `-` in three positions: trailing after a flag, leading (which +// halts option scanning), and followed by what looks like a flag +// (also halts — first non-flag wins). +@test fn baredash() void = { + let helps: [2]getopt.help; + getopt.cmdhelp(&helps[0], "cat"); + getopt.flaghelp(&helps[1], 'v': rune, "verbose"); + + // row 0: ["cat", "-v", "-"] → 1 opt 'v', 1 arg "-" + // row 1: ["cat", "-"] → 0 opts, 1 arg "-" + // row 2: ["cat", "-", "-v"] → 0 opts, 2 args "-","-v" + let srcs: [8]str; + srcs[0]="cat"; srcs[1]="-v"; srcs[2]="-"; + srcs[3]="cat"; srcs[4]="-"; + srcs[5]="cat"; srcs[6]="-"; srcs[7]="-v"; + + let argo: [3]i32; + let argn: [3]i32; + let wantopts: [3]i32; + let wantargs: [3]i32; + argo[0]=0; argn[0]=3; wantopts[0]=1; wantargs[0]=1; + argo[1]=3; argn[1]=2; wantopts[1]=0; wantargs[1]=1; + argo[2]=5; argn[2]=3; wantopts[2]=0; wantargs[2]=2; + + let i: i32 = 0; + for (i < 3) { + let argv: []str; + argv.ptr = &srcs[argo[i]]; + argv.len = argn[i]; + argv.cap = argn[i]; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:2]); + match (r) { + case void => {}; + case let e: getopt.error => fail(); + }; + if (cmd.optslen != wantopts[i]) { fail(); }; + if (cmd.argslen != wantargs[i]) { fail(); }; + // First positional is always the bare "-" in these rows. + if (cmd.argslen > 0) { + if (!streq(cmd.argsptr[0], "-")) { fail(); }; + }; + getopt.finish(&cmd); + i += 1; + }; +}; + +// ---- errortable: unknownopt + requiresarg variants --------------------- +// +// Parallel arrays per row: argv flat-packed into `srcs`, with `argo` +// the offset and `argn` the count for each row. +@test fn errortable() void = { + let helps: [3]getopt.help; + getopt.cmdhelp(&helps[0], "prog"); + getopt.flaghelp(&helps[1], 'v': rune, "verbose"); + getopt.paramhelp(&helps[2], 'e': rune, "expr", "expression"); + + // Flat-packed argv for 3 cases: + // row 0: ["prog", "-x"] → UNKNOWNOPT 'x' + // row 1: ["prog", "-e"] → REQUIRESARG 'e' + // row 2: ["prog", "-v", "-e"] → REQUIRESARG 'e' (cluster ends mid-param) + let srcs: [7]str; + srcs[0] = "prog"; srcs[1] = "-x"; + srcs[2] = "prog"; srcs[3] = "-e"; + srcs[4] = "prog"; srcs[5] = "-v"; srcs[6] = "-e"; + + let argo: [3]i32; + let argn: [3]i32; + let wantk: [3]i32; + let wantf: [3]u8; + argo[0] = 0; argn[0] = 2; wantk[0] = 1; wantf[0] = 'x': u8; // UNKNOWNOPT=1 + argo[1] = 2; argn[1] = 2; wantk[1] = 0; wantf[1] = 'e': u8; // REQUIRESARG=0 + argo[2] = 4; argn[2] = 3; wantk[2] = 0; wantf[2] = 'e': u8; + + let i: i32 = 0; + for (i < 3) { + let argv: []str; + argv.ptr = &srcs[argo[i]]; + argv.len = argn[i]; + argv.cap = argn[i]; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:3]); + match (r) { + case void => fail(); + case let e: getopt.error => { + if ((e.kind: i32) != wantk[i]) { fail(); }; + if ((e.flag: u8) != wantf[i]) { fail(); }; + if (!streq(e.name, "prog")) { fail(); }; + }; + }; + getopt.finish(&cmd); + i += 1; + }; +}; + +// ---- strerrortext: render both error kinds ----------------------------- +// +// Parallel rows over (kind, flag, name) → expected message. `kind` +// rides as i32 because the errorkind enum doesn't yet index an +// array element directly. +@test fn strerrortext() void = { + let kinds: [2]i32; + let flags: [2]u8; + let names: [2]str; + let wants: [2]str; + kinds[0]=1; flags[0]='x': u8; names[0]="prog"; wants[0]="prog: unrecognized option: -x"; + kinds[1]=0; flags[1]='e': u8; names[1]="sed"; wants[1]="sed: option -e requires an argument"; + + let i: i32 = 0; + for (i < 2) { + let e: getopt.error; + if (kinds[i] == 0) { e.kind = getopt.errorkind.REQUIRESARG; }; + if (kinds[i] == 1) { e.kind = getopt.errorkind.UNKNOWNOPT; }; + e.flag = flags[i]: rune; + e.name = names[i]; + let s: str = getopt.strerror(&e); + if (!streq(s, wants[i])) { fail(); }; + i += 1; + }; +}; + +// ---- nooptionsbare: argv with only program name + positionals ---------- +// +// Positional-only argv at three lengths: two positionals, one +// positional, and the program-name-only edge. +@test fn nooptionsbare() void = { + let helps: [2]getopt.help; + getopt.cmdhelp(&helps[0], "echo"); + getopt.flaghelp(&helps[1], 'n': rune, "no newline"); + + // row 0: ["echo", "hello", "world"] → 2 args, args[0]="hello" + // row 1: ["echo", "a"] → 1 arg, args[0]="a" + // row 2: ["echo"] → 0 args + let srcs: [6]str; + srcs[0]="echo"; srcs[1]="hello"; srcs[2]="world"; + srcs[3]="echo"; srcs[4]="a"; + srcs[5]="echo"; + + let argo: [3]i32; + let argn: [3]i32; + let wantargs: [3]i32; + let wantarg0: [3]str; + argo[0]=0; argn[0]=3; wantargs[0]=2; wantarg0[0]="hello"; + argo[1]=3; argn[1]=2; wantargs[1]=1; wantarg0[1]="a"; + argo[2]=5; argn[2]=1; wantargs[2]=0; wantarg0[2]=""; + + let i: i32 = 0; + for (i < 3) { + let argv: []str; + argv.ptr = &srcs[argo[i]]; + argv.len = argn[i]; + argv.cap = argn[i]; + + let cmd: getopt.command; + let r: (void | getopt.error) = getopt.tryparse(&cmd, argv, helps[0:2]); + match (r) { + case void => {}; + case let e: getopt.error => fail(); + }; + if (cmd.optslen != 0) { fail(); }; + if (cmd.argslen != wantargs[i]) { fail(); }; + if (cmd.argslen > 0) { + if (!streq(cmd.argsptr[0], wantarg0[i])) { fail(); }; + }; + getopt.finish(&cmd); + i += 1; + }; +}; + +export fn main() i32 = { + signalled = 1; flagcluster(); + signalled = 2; paramflag(); + signalled = 3; separator(); + signalled = 4; repeatedflag(); + signalled = 5; baredash(); + signalled = 6; errortable(); + signalled = 7; strerrortext(); + signalled = 8; nooptionsbare(); + return 0; +};