From abd97e6c57e525c3f56e90692f0201cfe592d4d7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 7 Jun 2026 06:19:13 +0900 Subject: [PATCH] lib/getopt: printusage + printhelp 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). --- lib/getopt/getopt.ww | 247 +++++++++++++++++++++++++++++++++++++-- lib/getopt/getopttest.ww | 101 ++++++++++++++++ 2 files changed, 340 insertions(+), 8 deletions(-) diff --git a/lib/getopt/getopt.ww b/lib/getopt/getopt.ww index 4d4be5ff..336b2bfa 100644 --- a/lib/getopt/getopt.ww +++ b/lib/getopt/getopt.ww @@ -43,14 +43,18 @@ // (`(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). +// * [[printusage]] and [[printhelp]] are now shipped. They use +// fmt.fprintf + fmt.fprint with [[io.empty]] for the two-pass +// width measurement. [[io.empty]] diverges from Hare's +// `const empty: *stream` — it is a function call (#118 blocks +// static vtable const-init). [[printsubcmds]] is not shipped: +// it exists solely to enumerate subcmd_help entries, and ww has +// no SUBCMD helpkind (see subcommand note above). +// * [[parse]] is still deferred: it calls os.exit with +// `os.status.FAILURE` which doesn't exist (os.exit takes bare +// i32; task #126). +// * [[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 — @@ -94,6 +98,8 @@ package getopt; import encoding.utf8; +import fmt; +import io; import os; import strings; @@ -394,3 +400,228 @@ export fn strerror(err: *error) str = { r.len = off; return r; }; + +// ---- help output -------------------------------------------------------- +// +// ref/hare/getopt/getopts.ha:202-335. +// +// Divergences from Hare (all forced): +// * [[printusage]] / [[printhelp]] take `io.handle` directly; +// Hare's signatures match. +// * [[io.empty]] is a function call, not a `const *stream` (#118). +// * [[containsh]] iterates the flat `help` struct (not a tagged +// union) and checks `kind == FLAG|PARAM && flag == 'h'`. +// * [[printsubcmds]] is not shipped — ww has no SUBCMD helpkind +// (see file header). The `printhelp` call site that would invoke +// it is dropped. +// * Error returns from fmt.fprint/fprintf are propagated via explicit +// match rather than Hare's `?` shorthand (ww has no `?` yet). +// +// containsh — true if `hs` declares an explicit `-h` flag or param. +// Mirrors ref/hare/getopt/getopts.ha:267-279 adapted to the flat +// [[help]] struct. +fn containsh(hs: []help) bool = { + let i: i32 = 0; + for (i < hs.len) { + let p: *help = &hs[i]; + if ((p.kind == helpkind.FLAG || p.kind == helpkind.PARAM) && + p.flag == 'h': rune) { + return true; + }; + i += 1; + }; + return false; +}; + +// _printusage — inner two-pass renderer for [[printusage]]. +// Mirrors ref/hare/getopt/getopts.ha:213-265. +// Returns total bytes written for the width-measurement pass. +fn _printusage( + out: io.handle, + name: str, + indent: bool, + hascmdh: bool, + help: []help, +) (size | io.error) = { + let z: size = 0; + + // "Usage: " + match (fmt.fprint(out, "Usage:", name)) { + case let n: size => z += n; + case let e: io.error => return e; + }; + + // Optional auto-[-h] + flag cluster [-Xabc]. + let startedflags: bool = false; + if (!hascmdh) { + match (fmt.fprint(out, " [-h")) { + case let n: size => { z += n; startedflags = true; }; + case let e: io.error => return e; + }; + }; + let i: i32 = 0; + for (i < help.len) { + let p: *help = &help[i]; + if (p.kind == helpkind.FLAG) { + if (!startedflags) { + match (fmt.fprint(out, " [-")) { + case let n: size => { z += n; startedflags = true; }; + case let e: io.error => return e; + }; + }; + match (fmt.fprint(out, p.flag)) { + case let n: size => z += n; + case let e: io.error => return e; + }; + }; + i += 1; + }; + if (startedflags) { + match (fmt.fprint(out, "]")) { + case let n: size => z += n; + case let e: io.error => return e; + }; + }; + + // Parameter slots [-X ]. + i = 0; + for (i < help.len) { + let p: *help = &help[i]; + if (p.kind == helpkind.PARAM) { + if (indent) { + match (fmt.fprint(out, "\n\t")) { + case let n: size => z += n; + case let e: io.error => return e; + }; + }; + match (fmt.fprintf(out, " [-{} <{}>]", p.flag, p.name)) { + case let n: size => z += n; + case let e: io.error => return e; + }; + }; + i += 1; + }; + + // Positional CMD labels (indices > 0 only; index 0 is the command + // summary). Mirrors ref/hare/getopt/getopts.ha:253-264. + let firstarg: bool = true; + i = 1; + for (i < help.len) { + let p: *help = &help[i]; + if (p.kind == helpkind.CMD) { + if (firstarg) { + if (indent) { + match (fmt.fprint(out, "\n\t")) { + case let n: size => z += n; + case let e: io.error => return e; + }; + }; + firstarg = false; + }; + match (fmt.fprintf(out, " {}", p.text)) { + case let n: size => z += n; + case let e: io.error => return e; + }; + }; + i += 1; + }; + + match (fmt.fprint(out, "\n")) { + case let n: size => z += n; + case let e: io.error => return e; + }; + return z; +}; + +// printusage — print a one-line usage summary to `out`. +// Mirrors ref/hare/getopt/getopts.ha:202-211. +export fn printusage( + out: io.handle, + name: str, + help: []help, +) (void | io.error) = { + let hascmdh: bool = containsh(help); + let z: size = 0; + // Measure with io.empty to decide whether to indent long lines. + // Diverges from Hare's `_printusage(io::empty, ...)`: ww's + // io.empty() is a function (not a const *stream) due to #118. + let esink: io.handle = io.empty(): io.handle; + match (_printusage(esink, name, false, hascmdh, help)) { + case let n: size => z = n; + case let e: io.error => return e; + }; + match (_printusage(out, name, z > 72, hascmdh, help)) { + case let n: size => {}; + case let e: io.error => return e; + }; + return void; +}; + +// printhelp — print full help text (summary + usage + option list) to +// `out`. Mirrors ref/hare/getopt/getopts.ha:281-314. +// Omits the printsubcmds call at the tail (ww has no SUBCMD helpkind). +export fn printhelp( + out: io.handle, + name: str, + help: []help, +) (void | io.error) = { + if (help.len == 0) { + return void; + }; + + // Print "name: summary\n\n" if help[0] is a CMD entry. + let p0: *help = &help[0]; + if (p0.kind == helpkind.CMD) { + match (fmt.fprintfln(out, "{}: {}\n", name, p0.text)) { + case let n: size => {}; + case let e: io.error => return e; + }; + }; + + // Usage line (two-pass for 72-char indent). + let hascmdh: bool = containsh(help); + let z: size = 0; + let esink: io.handle = io.empty(): io.handle; + match (_printusage(esink, name, false, hascmdh, help)) { + case let n: size => z = n; + case let e: io.error => return e; + }; + match (_printusage(out, name, z > 72, hascmdh, help)) { + case let n: size => {}; + case let e: io.error => return e; + }; + + // Blank line between usage and option list. + match (fmt.fprint(out, "\n")) { + case let n: size => {}; + case let e: io.error => return e; + }; + + // Auto -h line if not declared. + if (!hascmdh) { + match (fmt.fprintln(out, "-h: print this help text")) { + case let n: size => {}; + case let e: io.error => return e; + }; + }; + + // Per-option lines. + let i: i32 = 0; + for (i < help.len) { + let p: *help = &help[i]; + if (p.kind == helpkind.FLAG) { + match (fmt.fprintfln(out, "-{}: {}", p.flag, p.text)) { + case let n: size => {}; + case let e: io.error => return e; + }; + }; + if (p.kind == helpkind.PARAM) { + match (fmt.fprintfln(out, "-{} <{}>: {}", p.flag, p.name, p.text)) { + case let n: size => {}; + case let e: io.error => return e; + }; + }; + i += 1; + }; + return void; +}; diff --git a/lib/getopt/getopttest.ww b/lib/getopt/getopttest.ww index 2969932f..0c7ed5d4 100644 --- a/lib/getopt/getopttest.ww +++ b/lib/getopt/getopttest.ww @@ -22,6 +22,8 @@ package getopt; import getopt; +import io; +import memio; import strings; // Direct exit(2) binding rather than `use os;` — os exports @@ -391,6 +393,103 @@ fn streq(a: str, b: str) bool = { }; }; +// ---- 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 ]\n\t [-o ]\n\t [-i ]\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 ] files...\n\n-h: print this help text\n-n: number lines\n-o : 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(); @@ -400,5 +499,7 @@ export fn main() i32 = { signalled = 6; errortable(); signalled = 7; strerrortext(); signalled = 8; nooptionsbare(); + signalled = 9; printusage_cases(); + signalled = 10; printhelp_cases(); return 0; };