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:
@@ -43,14 +43,18 @@
|
|||||||
// (`(str, *command)` payload + `alloc(command)?`) and a real
|
// (`(str, *command)` payload + `alloc(command)?`) and a real
|
||||||
// consumer to drive the test surface. Add when the first
|
// consumer to drive the test surface. Add when the first
|
||||||
// consumer is queued.
|
// consumer is queued.
|
||||||
// * [[parse]], [[printusage]], [[printhelp]], [[printsubcmds]]
|
// * [[printusage]] and [[printhelp]] are now shipped. They use
|
||||||
// aren't shipped. The print family needs fmt.fprintf-with-{}-
|
// fmt.fprintf + fmt.fprint with [[io.empty]] for the two-pass
|
||||||
// interpolation that lib/fmt doesn't expose (lib/fmt is print-
|
// width measurement. [[io.empty]] diverges from Hare's
|
||||||
// string-only); [[parse]] is a thin wrapper that bundles
|
// `const empty: *stream` — it is a function call (#118 blocks
|
||||||
// printusage + os.exit on top of [[tryparse]], so it goes too.
|
// static vtable const-init). [[printsubcmds]] is not shipped:
|
||||||
// Callers handle errors themselves; [[strerror]] formats a one-
|
// it exists solely to enumerate subcmd_help entries, and ww has
|
||||||
// line summary into a module-level buffer (same shape as
|
// no SUBCMD helpkind (see subcommand note above).
|
||||||
// strconv.*tos).
|
// * [[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
|
// GRADUATE-IN-ONE-GO WARNING (lib/CLAUDE.md policy). Callers MUST
|
||||||
// NOT bake the current cgen-forced field names into themselves —
|
// NOT bake the current cgen-forced field names into themselves —
|
||||||
@@ -94,6 +98,8 @@
|
|||||||
package getopt;
|
package getopt;
|
||||||
|
|
||||||
import encoding.utf8;
|
import encoding.utf8;
|
||||||
|
import fmt;
|
||||||
|
import io;
|
||||||
import os;
|
import os;
|
||||||
import strings;
|
import strings;
|
||||||
|
|
||||||
@@ -394,3 +400,228 @@ export fn strerror(err: *error) str = {
|
|||||||
r.len = off;
|
r.len = off;
|
||||||
return r;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
package getopt;
|
package getopt;
|
||||||
|
|
||||||
import getopt;
|
import getopt;
|
||||||
|
import io;
|
||||||
|
import memio;
|
||||||
import strings;
|
import strings;
|
||||||
|
|
||||||
// Direct exit(2) binding rather than `use os;` — os exports
|
// 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 <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 = {
|
export fn main() i32 = {
|
||||||
signalled = 1; flagcluster();
|
signalled = 1; flagcluster();
|
||||||
signalled = 2; paramflag();
|
signalled = 2; paramflag();
|
||||||
@@ -400,5 +499,7 @@ export fn main() i32 = {
|
|||||||
signalled = 6; errortable();
|
signalled = 6; errortable();
|
||||||
signalled = 7; strerrortext();
|
signalled = 7; strerrortext();
|
||||||
signalled = 8; nooptionsbare();
|
signalled = 8; nooptionsbare();
|
||||||
|
signalled = 9; printusage_cases();
|
||||||
|
signalled = 10; printhelp_cases();
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user