lib: add getopt (Hare's tryparse + error helpers)
Mirrors Hare's getopt subject to current cgen gaps: flat `command`
fields instead of slices, struct-not-tuple `option`, error/help
constructors take out-pointers. The `parse` wrapper plus
printusage/printhelp/printsubcmds are deferred (need fmt.fprintf
with {}-interpolation, which lib/fmt doesn't expose yet). SUBCMD
machinery dropped entirely per Drew — accept-but-inert would have
been a silent-misuse hazard.
Graduates in one go when cgen tasks #4 #5 #6 #7 #9 #10 #11 #14
land. File header names the three surface sweeps that will follow.
Surfaces task #15 (w6c: && doesn't short-circuit, nil-arg deref
in test exposed it).
This commit is contained in:
404
lib/getopt/getopt.ww
Normal file
404
lib/getopt/getopt.ww
Normal file
@@ -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 <value>
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
402
lib/getopt/getopttest.ww
Normal file
402
lib/getopt/getopttest.ww
Normal file
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user