F3.2: std.cli minimal subcommand + flag parser over explicit []string
Extend std/cli.sx with a zero-heap argument parser that the caller drives
over a logical argv ([]string), separate from the F3.1 os_args accessor.
Grammar: <group> <command> [--flag VALUE | --bool]... [--json] [-- rest...]
- (group, command) dispatched against a caller-provided Command table;
no match -> error.UnknownCommand.
- value-taking vs boolean flags fixed by each command's FlagSpec list;
--json is a reserved global boolean surfaced as parsed.json.
- `--` or the first bare operand ends flag parsing; the remainder is
parsed.rest (operand views).
Heap discipline (heap-discipline.md): zero heap, zero copy. group/command/
flag values/rest are all VIEWS into args. Parsed is a by-value stack struct;
flag presence/values live in a fixed [16]FlagValue inline array indexed by
spec position (no per-flag allocation, no context.allocator). The flag-spec
list and command table are caller storage passed as views.
Failure surfacing (no silent skip): unknown command, unknown flag, a
value-flag missing its value, and an absent required flag each raise a
specific CliError variant; a caller-owned Diag records the offending token
(index + view) before each raise, since error tags carry no data.
examples/0717 drives the parser over explicit []string vectors: a valid
group/command/--flag/--bool/--json case (asserting parsed values + that
values are views into argv), subcommand dispatch, `--`/bare-operand
separators, and the five failure variants each asserted via destructure +
Diag. zig build && zig build test && run_examples.sh green (385 passed).
This commit is contained in:
137
examples/0717-modules-cli-parse.sx
Normal file
137
examples/0717-modules-cli-parse.sx
Normal file
@@ -0,0 +1,137 @@
|
||||
// CLI argument PARSER from `modules/std/cli.sx` (F3.2) — subcommand
|
||||
// dispatch + `--flag` parsing over an EXPLICIT logical argv (`[]string`).
|
||||
//
|
||||
// Every argv vector below is an explicit `[]string` literal (the caller's
|
||||
// logical args, program name already removed). The suite proves:
|
||||
//
|
||||
// 1. DISPATCH — `<group> <command>` selects the right command in the
|
||||
// caller's table; group/command are VIEWS into argv.
|
||||
// 2. FLAGS — `--out VALUE` (value-taking) binds a VIEW of the next
|
||||
// token; `--verbose` (boolean) records presence; the
|
||||
// reserved `--json` mode flag surfaces as `parsed.json`.
|
||||
// 3. SEPARATORS — `--` and the first bare operand both stop flag
|
||||
// parsing; the remainder is `parsed.rest` (operand VIEWS).
|
||||
// 4. HEAP — flag values / group / command / rest all point INSIDE
|
||||
// the input argv (zero copy); `Parsed` is a stack value.
|
||||
// 5. FAILURES — unknown command, unknown flag, missing required flag,
|
||||
// and a value-flag with no value each raise the specific
|
||||
// `CliError` variant on the error channel, and the
|
||||
// caller-owned `Diag` names the offending token.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/std/cli.sx";
|
||||
|
||||
report :: (label: string, ok: bool) {
|
||||
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
|
||||
}
|
||||
|
||||
// Half-open containment [lo, hi) — used to prove a view points into argv.
|
||||
in_range :: (x: s64, lo: s64, hi: s64) -> bool {
|
||||
return x >= lo and x < hi;
|
||||
}
|
||||
|
||||
// True when `parse(args, cmds)` raised exactly `want`. Destructure binds
|
||||
// the error tag without `try`, so a bad vector never aborts the example;
|
||||
// the failing token is captured in the caller-owned `Diag`.
|
||||
raises :: (args: []string, cmds: []Command, want: CliError) -> bool {
|
||||
d : Diag = .{};
|
||||
_, e := parse(args, cmds, @d);
|
||||
return e == want;
|
||||
}
|
||||
|
||||
main :: () -> ! {
|
||||
// ── Command table (caller storage; flag specs passed as views) ────
|
||||
publish_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "out", takes_value = true, required = true },
|
||||
FlagSpec.{ name = "verbose", takes_value = false, required = false },
|
||||
];
|
||||
status_flags : []FlagSpec = .[
|
||||
FlagSpec.{ name = "verbose", takes_value = false, required = false },
|
||||
];
|
||||
cmds : []Command = .[
|
||||
Command.{ group = "ci", command = "publish", flags = publish_flags },
|
||||
Command.{ group = "ci", command = "status", flags = status_flags },
|
||||
];
|
||||
|
||||
// ── 1. Valid: <group> <command> --flag v --bool --json ───────────
|
||||
d : Diag = .{};
|
||||
argv : []string = .["ci", "publish", "--out", "dist", "--verbose", "--json"];
|
||||
p := try parse(argv, cmds, @d);
|
||||
|
||||
report("dispatch-group", p.group == "ci");
|
||||
report("dispatch-command", p.command == "publish");
|
||||
report("dispatch-index", p.cmd_index == 0);
|
||||
report("flag-value", p.value_of("out") == "dist");
|
||||
report("flag-value-set", p.is_set("out"));
|
||||
report("bool-set", p.is_set("verbose"));
|
||||
report("json-set", p.json);
|
||||
report("no-rest", p.rest.len == 0);
|
||||
|
||||
// ── 2. Heap discipline: flag value is a VIEW into argv ────────────
|
||||
// "dist" is argv[3]; its bytes must lie inside that very element.
|
||||
src : s64 = xx argv[3].ptr;
|
||||
stop := src + argv[3].len;
|
||||
pview : s64 = xx p.value_of("out").ptr;
|
||||
report("value-is-view", in_range(pview, src, stop) or pview == src);
|
||||
// group/command are argv[0]/argv[1] verbatim (same pointer, no copy).
|
||||
g0 : s64 = xx argv[0].ptr;
|
||||
gp : s64 = xx p.group.ptr;
|
||||
report("group-is-view", gp == g0);
|
||||
|
||||
// ── 3. Dispatch to a different command in the table ──────────────
|
||||
s_argv : []string = .["ci", "status", "--verbose"];
|
||||
sp := try parse(s_argv, cmds, @d);
|
||||
report("dispatch-2nd", sp.command == "status" and sp.cmd_index == 1);
|
||||
report("2nd-bool", sp.is_set("verbose"));
|
||||
report("2nd-json-unset", !sp.json);
|
||||
|
||||
// ── 4. `--` separator: rest are operand views, flags stop there ──
|
||||
sep_argv : []string = .["ci", "publish", "--out", "dist", "--", "--raw", "x"];
|
||||
spv := try parse(sep_argv, cmds, @d);
|
||||
report("sep-value", spv.value_of("out") == "dist");
|
||||
report("sep-rest-len", spv.rest.len == 2);
|
||||
report("sep-rest-0", spv.rest.len == 2 and spv.rest[0] == "--raw");
|
||||
report("sep-rest-1", spv.rest.len == 2 and spv.rest[1] == "x");
|
||||
report("sep-no-bool", !spv.is_set("verbose"));
|
||||
|
||||
// ── 5. First bare operand also stops flag parsing ────────────────
|
||||
bare_argv : []string = .["ci", "publish", "--out", "dist", "extra", "tail"];
|
||||
bpv := try parse(bare_argv, cmds, @d);
|
||||
report("bare-rest-len", bpv.rest.len == 2);
|
||||
report("bare-rest-0", bpv.rest.len == 2 and bpv.rest[0] == "extra");
|
||||
|
||||
// ── 6. Value-flag accepts a single-dash value (not a long flag) ──
|
||||
dash_argv : []string = .["ci", "publish", "--out", "-5", "--verbose"];
|
||||
dpv := try parse(dash_argv, cmds, @d);
|
||||
report("dash-value", dpv.value_of("out") == "-5" and dpv.is_set("verbose"));
|
||||
|
||||
// ── 7. Failures: each surfaces the specific variant ──────────────
|
||||
a_unknown_cmd : []string = .["ci", "deploy", "--out", "x"];
|
||||
a_unknown_group : []string = .["zz", "publish", "--out", "x"];
|
||||
a_too_few : []string = .["ci"];
|
||||
a_unknown_flag : []string = .["ci", "publish", "--out", "x", "--nope"];
|
||||
a_missing_value : []string = .["ci", "publish", "--out"];
|
||||
a_value_eats : []string = .["ci", "publish", "--out", "--verbose"];
|
||||
a_missing_req : []string = .["ci", "publish", "--verbose"];
|
||||
report("err-unknown-cmd", raises(a_unknown_cmd, cmds, error.UnknownCommand));
|
||||
report("err-unknown-group", raises(a_unknown_group, cmds, error.UnknownCommand));
|
||||
report("err-too-few", raises(a_too_few, cmds, error.UnknownCommand));
|
||||
report("err-unknown-flag", raises(a_unknown_flag, cmds, error.UnknownFlag));
|
||||
report("err-missing-value", raises(a_missing_value, cmds, error.MissingValue));
|
||||
report("err-value-eats-flag", raises(a_value_eats, cmds, error.MissingValue));
|
||||
report("err-missing-req", raises(a_missing_req, cmds, error.MissingRequired));
|
||||
|
||||
// ── 8. Diag names the offending token on the error path ──────────
|
||||
de : Diag = .{};
|
||||
_, ue := parse(a_unknown_flag, cmds, @de);
|
||||
report("diag-flag-tag", ue == error.UnknownFlag);
|
||||
report("diag-flag-token", de.token == "--nope" and de.index == 4);
|
||||
|
||||
dm : Diag = .{};
|
||||
_, me := parse(a_missing_req, cmds, @dm);
|
||||
report("diag-req-tag", me == error.MissingRequired);
|
||||
report("diag-req-token", dm.token == "out");
|
||||
|
||||
print("=== DONE ===\n");
|
||||
return;
|
||||
}
|
||||
1
examples/expected/0717-modules-cli-parse.exit
Normal file
1
examples/expected/0717-modules-cli-parse.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
0
examples/expected/0717-modules-cli-parse.stderr
Normal file
0
examples/expected/0717-modules-cli-parse.stderr
Normal file
33
examples/expected/0717-modules-cli-parse.stdout
Normal file
33
examples/expected/0717-modules-cli-parse.stdout
Normal file
@@ -0,0 +1,33 @@
|
||||
dispatch-group: ok
|
||||
dispatch-command: ok
|
||||
dispatch-index: ok
|
||||
flag-value: ok
|
||||
flag-value-set: ok
|
||||
bool-set: ok
|
||||
json-set: ok
|
||||
no-rest: ok
|
||||
value-is-view: ok
|
||||
group-is-view: ok
|
||||
dispatch-2nd: ok
|
||||
2nd-bool: ok
|
||||
2nd-json-unset: ok
|
||||
sep-value: ok
|
||||
sep-rest-len: ok
|
||||
sep-rest-0: ok
|
||||
sep-rest-1: ok
|
||||
sep-no-bool: ok
|
||||
bare-rest-len: ok
|
||||
bare-rest-0: ok
|
||||
dash-value: ok
|
||||
err-unknown-cmd: ok
|
||||
err-unknown-group: ok
|
||||
err-too-few: ok
|
||||
err-unknown-flag: ok
|
||||
err-missing-value: ok
|
||||
err-value-eats-flag: ok
|
||||
err-missing-req: ok
|
||||
diag-flag-tag: ok
|
||||
diag-flag-token: ok
|
||||
diag-req-tag: ok
|
||||
diag-req-token: ok
|
||||
=== DONE ===
|
||||
Reference in New Issue
Block a user