ee4f6fb3
refactor: muxa's verb is the first bare word
a73x 2026-08-27 07:51
Commit message
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" })); |