Port ref/hare/getopt/getopts.ha:202-314 (printusage / _printusage / containsh / printhelp): the two-pass io.empty width measurement + the >72-col indent rule. Table-driven tests. cstage-only (C-first); wwstage byte-id twin owed (#125). printsubcmds + parse() deferred (#127 / #126).
506 lines
15 KiB
Plaintext
506 lines
15 KiB
Plaintext
// 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
|
|
// flattened `&&` form is now legal — both cgen stages short-circuit
|
|
// per task #15 — but the nested-if shape was the original workaround
|
|
// and is preserved here as a regression marker.
|
|
|
|
package getopt;
|
|
|
|
import getopt;
|
|
import io;
|
|
import memio;
|
|
import 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;
|
|
};
|
|
};
|
|
|
|
// ---- printusage_cases: table of width-measure paths --------------------
|
|
//
|
|
// ref/hare/getopt/getopts.ha:202-265.
|
|
// Row 0: short line (≤72) — auto-[-h] + FLAG cluster, no wrap.
|
|
// Row 1: long line (>72) — PARAM slots each prefixed with \n\t.
|
|
//
|
|
// Help entries are packed into a flat [7]getopt.help array; hoff[i] /
|
|
// hcnt[i] mark each row's slice. Parallel [2]str arrays hold name and
|
|
// expected output per row.
|
|
@test fn printusage_cases() void = {
|
|
let helps: [7]getopt.help;
|
|
// Row 0: CMD + FLAG 'F' + FLAG 'a'
|
|
getopt.cmdhelp(&helps[0], "list files");
|
|
getopt.flaghelp(&helps[1], 'F': rune, "do F");
|
|
getopt.flaghelp(&helps[2], 'a': rune, "do a");
|
|
// Row 1: CMD + 3 PARAMs (no-indent measurement 77 > 72)
|
|
getopt.cmdhelp(&helps[3], "process files");
|
|
getopt.paramhelp(&helps[4], 'e': rune, "expression", "run expr");
|
|
getopt.paramhelp(&helps[5], 'o': rune, "output-file", "output");
|
|
getopt.paramhelp(&helps[6], 'i': rune, "input-path", "input");
|
|
|
|
let names: [2]str;
|
|
let wants: [2]str;
|
|
let hoff: [2]i32;
|
|
let hcnt: [2]i32;
|
|
|
|
names[0] = "ls";
|
|
wants[0] = "Usage: ls [-hFa]\n";
|
|
hoff[0] = 0; hcnt[0] = 3;
|
|
|
|
names[1] = "myprogram";
|
|
wants[1] = "Usage: myprogram [-h]\n\t [-e <expression>]\n\t [-o <output-file>]\n\t [-i <input-path>]\n";
|
|
hoff[1] = 3; hcnt[1] = 4;
|
|
|
|
let buf: [512]u8;
|
|
let i: i32 = 0;
|
|
for (i < 2) {
|
|
let ms: memio.stream = memio.fixed(buf[0:512]);
|
|
let s: io.stream = &ms.vt;
|
|
let h: io.handle = s: io.handle;
|
|
let hs: []getopt.help;
|
|
hs.ptr = &helps[hoff[i]];
|
|
hs.len = hcnt[i];
|
|
hs.cap = hcnt[i];
|
|
match (getopt.printusage(h, names[i], hs)) {
|
|
case void => {};
|
|
case let e: io.error => fail();
|
|
};
|
|
let got: str = memio.string(&ms);
|
|
if (!streq(got, wants[i])) { fail(); };
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
// ---- printhelp_cases: table of help-output paths ----------------------
|
|
//
|
|
// ref/hare/getopt/getopts.ha:281-314.
|
|
// Row 0: empty help slice → early return, no output.
|
|
// Row 1: full help (CMD+FLAG+PARAM+CMD) → summary + usage + option list.
|
|
//
|
|
// help[0..4] holds the full-case entries; the empty-case uses a 0-len
|
|
// slice from the same array (hcnt[0]=0).
|
|
@test fn printhelp_cases() void = {
|
|
let helps: [4]getopt.help;
|
|
getopt.cmdhelp(&helps[0], "concatenate files");
|
|
getopt.flaghelp(&helps[1], 'n': rune, "number lines");
|
|
getopt.paramhelp(&helps[2], 'o': rune, "output", "output file");
|
|
getopt.cmdhelp(&helps[3], "files...");
|
|
|
|
let names: [2]str;
|
|
let wants: [2]str;
|
|
let hcnt: [2]i32;
|
|
|
|
names[0] = "prog";
|
|
wants[0] = "";
|
|
hcnt[0] = 0;
|
|
|
|
names[1] = "cat";
|
|
wants[1] = "cat: concatenate files\n\nUsage: cat [-hn] [-o <output>] files...\n\n-h: print this help text\n-n: number lines\n-o <output>: output file\n";
|
|
hcnt[1] = 4;
|
|
|
|
let buf: [512]u8;
|
|
let i: i32 = 0;
|
|
for (i < 2) {
|
|
let ms: memio.stream = memio.fixed(buf[0:512]);
|
|
let s: io.stream = &ms.vt;
|
|
let h: io.handle = s: io.handle;
|
|
match (getopt.printhelp(h, names[i], helps[0:hcnt[i]])) {
|
|
case void => {};
|
|
case let e: io.error => fail();
|
|
};
|
|
let got: str = memio.string(&ms);
|
|
if (!streq(got, wants[i])) { fail(); };
|
|
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();
|
|
signalled = 9; printusage_cases();
|
|
signalled = 10; printhelp_cases();
|
|
return 0;
|
|
};
|