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).
This commit is contained in:
2026-06-07 06:19:13 +09:00
parent f3750ae3ce
commit abd97e6c57
2 changed files with 340 additions and 8 deletions

View File

@@ -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: <name>"
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 <name>].
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;
};