a73x

ee4f6fb3

refactor: muxa's verb is the first bare word

a73x   2026-08-27 07:51

Commit message
refactor: muxa's verb is the first bare word

`parseArgs` special-cased argv[1] and ran a one-word "probe" parse so that
`muxa --help` and `muxa --version` were answered where a verb belongs —
a second call into the same table, kept in step by hand.

The verb is a positional instead: the first bare word sets `_verb` and any
later one is the verb's argument, so there is one parse and one table. A
line that names no verb (bare `muxa`, `muxa --vt`, `muxa bogus`) is the
usage error it always was, now from a post-check rather than a branch.

`muxa --vt capture` becomes legal, which is what one grammar means.

src/cli/muxa.zig
Old New
@@ -61,8 +61,7 @@ const Opts = struct {
61 /// is the wire's own default spelling and would fail a rule written 61 /// is the wire's own default spelling and would fail a rule written
62 /// for a name. `sessionName` is what the frames actually carry. 62 /// for a name. `sessionName` is what the frames actually carry.
63 session: ?proto.SessionName = null, 63 session: ?proto.SessionName = null,
64 /// Null until `parseArgs` reads argv[1]; a parse that returned an Opts 64 /// Null until `positional` meets a verb; a returned Opts has one.
65 /// has one.
66 _verb: ?Verb = null, 65 _verb: ?Verb = null,
67 _arg: ?[]const u8 = null, 66 _arg: ?[]const u8 = null,
68 67
@@ -74,8 +73,14 @@ const Opts = struct {
74 return if (o.session) |n| n.name else ""; 73 return if (o.session) |n| n.name else "";
75 } 74 }
76 75
77 /// The verb's argument. A second is a mistake: no verb here takes two. 76 /// The first bare word is the verb and the next is its argument. A word
77 /// that names no verb is refused where a verb belongs, and a second
78 /// argument is refused too: no verb here takes two.
78 pub fn positional(self: *Opts, word: []const u8) bool { 79 pub fn positional(self: *Opts, word: []const u8) bool {
80 if (self._verb == null) {
81 self._verb = std.meta.stringToEnum(Verb, word) orelse return false;
82 return true;
83 }
79 if (self._arg != null) return false; 84 if (self._arg != null) return false;
80 self._arg = word; 85 self._arg = word;
81 return true; 86 return true;
@@ -89,25 +94,14 @@ comptime {
89 const ParseError = error{ Usage, Help, Version }; 94 const ParseError = error{ Usage, Help, Version };
90 95
91 fn parseArgs(args: []const [:0]const u8) ParseError!Opts { 96 fn parseArgs(args: []const [:0]const u8) ParseError!Opts {
92 if (args.len < 2) return error.Usage; 97 var o: Opts = .{};
93 const verb = std.meta.stringToEnum(Verb, args[1]) orelse { 98 switch (cliflags.parse(Opts, &o, args[1..])) {
94 // The verb slot is also the only slot `muxa --help` has. Asked of
95 // the same table the verbs' flags go through, so the two spellings
96 // of asking cannot drift apart; anything else there is a bad verb.
97 var probe: Opts = .{};
98 return switch (cliflags.parse(Opts, &probe, args[1..2])) {
99 .help => error.Help,
100 .version => error.Version,
101 else => error.Usage,
102 };
103 };
104 var o: Opts = .{ ._verb = verb };
105 switch (cliflags.parse(Opts, &o, args[2..])) {
106 .ok => {}, 99 .ok => {},
107 .help => return error.Help, 100 .help => return error.Help,
108 .version => return error.Version, 101 .version => return error.Version,
109 .unknown_arg, .missing_value, .bad_value => return error.Usage, 102 .unknown_arg, .missing_value, .bad_value => return error.Usage,
110 } 103 }
104 if (o._verb == null) return error.Usage;
111 105
112 // Name ONE transport. A `--sock` silently ignored beside a `--quic` 106 // Name ONE transport. A `--sock` silently ignored beside a `--quic`
113 // would send an agent's frames somewhere other than the socket it 107 // would send an agent's frames somewhere other than the socket it
@@ -196,6 +190,25 @@ test "parseArgs verbs and flags" {
196 try std.testing.expectEqualStrings("make test", o2._arg.?); 190 try std.testing.expectEqualStrings("make test", o2._arg.?);
197 const a3 = [_][:0]const u8{ "muxa", "bogus" }; 191 const a3 = [_][:0]const u8{ "muxa", "bogus" };
198 try std.testing.expectError(error.Usage, parseArgs(&a3)); 192 try std.testing.expectError(error.Usage, parseArgs(&a3));
193
194 // The verb is a positional, so a flag may sit before it — one grammar,
195 // not a verb slot with rules of its own.
196 const early = [_][:0]const u8{ "muxa", "--vt", "capture" };
197 const oe = try parseArgs(&early);
198 try std.testing.expectEqual(Verb.capture, oe._verb.?);
199 try std.testing.expect(oe.vt);
200
201 // Only the FIRST bare word is read as a verb: a verb-shaped argument is
202 // the verb's argument, which is what `muxa send status` has to mean.
203 const shadow = [_][:0]const u8{ "muxa", "send", "status" };
204 const os = try parseArgs(&shadow);
205 try std.testing.expectEqual(Verb.send, os._verb.?);
206 try std.testing.expectEqualStrings("status", os._arg.?);
207
208 // A line whose every word is a flag names no verb, and is the same
209 // usage mistake a bare `muxa` is.
210 const verbless = [_][:0]const u8{ "muxa", "--vt" };
211 try std.testing.expectError(error.Usage, parseArgs(&verbless));
199 } 212 }
200 213
201 test "parseArgs: -- hands the rest to the verb, flags and all" { 214 test "parseArgs: -- hands the rest to the verb, flags and all" {
@@ -224,8 +237,8 @@ test "parseArgs: -- hands the rest to the verb, flags and all" {
224 } 237 }
225 238
226 test "muxa: --help and --version are answered wherever they can be typed" { 239 test "muxa: --help and --version are answered wherever they can be typed" {
227 // The verb slot is the only slot a bare `muxa --help` has, and the flag 240 // A bare `muxa --help` types it where a verb would go, and the same
228 // position is where anyone types it after finding a verb first. 241 // scan answers it after a verb: ONE table, so the two cannot drift.
229 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "--help" })); 242 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "--help" }));
230 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "-h" })); 243 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "-h" }));
231 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "status", "--help" })); 244 try std.testing.expectError(error.Help, parseArgs(&[_][:0]const u8{ "muxa", "status", "--help" }));