a8c489d9
refactor: muxweb parses to an Opts or an error, not a three-armed union
a73x 2026-08-27 08:06
Commit message
src/cli/webhub_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -64,28 +64,21 @@ const Parsed = struct { | |||
| 64 | } | 64 | } |
| 65 | }; | 65 | }; |
| 66 | 66 | ||
| 67 | const ParseResult = union(enum) { | ||
| 68 | serve: Parsed, | ||
| 69 | version, | ||
| 70 | help, | ||
| 71 | }; | ||
| 72 | |||
| 73 | comptime { | 67 | comptime { |
| 74 | cliflags.assertDocumented(Parsed, usage, &.{}); | 68 | cliflags.assertDocumented(Parsed, usage, &.{}); |
| 75 | } | 69 | } |
| 76 | 70 | ||
| 77 | /// A usage mistake is an ERROR, not a third result: it is the one outcome | 71 | /// Only a serving line hands anything back, so everything else is an |
| 78 | /// with nothing to hand back, and saying so lets the single errdefer own | 72 | /// error: the single errdefer then owns the tile list on every path that |
| 79 | /// the tile list on every refusing path. As a value it wanted a | 73 | /// does not serve. As union arms they wanted a `deinit` beside each |
| 80 | /// `tiles.deinit` beside every refusing return, each one a chance to | 74 | /// refusing return, every one of them a chance to forget. |
| 81 | /// forget. | 75 | const ParseError = error{ Usage, Help, Version } || std.mem.Allocator.Error; |
| 82 | const ParseError = error{Usage} || std.mem.Allocator.Error; | ||
| 83 | 76 | ||
| 84 | fn parseArgs( | 77 | fn parseArgs( |
| 85 | alloc: std.mem.Allocator, | 78 | alloc: std.mem.Allocator, |
| 86 | args: []const [:0]const u8, | 79 | args: []const [:0]const u8, |
| 87 | env_key: ?[]const u8, | 80 | env_key: ?[]const u8, |
| 88 | ) ParseError!ParseResult { | 81 | ) ParseError!Parsed { |
| 89 | var p = Parsed{ ._argv = .{ .alloc = alloc } }; | 82 | var p = Parsed{ ._argv = .{ .alloc = alloc } }; |
| 90 | errdefer p.deinit(); | 83 | errdefer p.deinit(); |
| 91 | 84 | ||
| @@ -102,13 +95,8 @@ fn parseArgs( | |||
| 102 | switch (outcome) { | 95 | switch (outcome) { |
| 103 | .ok => {}, | 96 | .ok => {}, |
| 104 | .unknown_arg, .missing_value, .bad_value => return error.Usage, | 97 | .unknown_arg, .missing_value, .bad_value => return error.Usage, |
| 105 | // The two non-error early returns, so the two that still free for | 98 | .help => return error.Help, |
| 106 | // themselves: errdefer does not run on the way out with a result | 99 | .version => return error.Version, |
| 107 | // in hand. | ||
| 108 | .help, .version => { | ||
| 109 | p.deinit(); | ||
| 110 | return if (outcome == .help) .help else .version; | ||
| 111 | }, | ||
| 112 | } | 100 | } |
| 113 | // Port 0 asks the kernel to choose, and the hub prints the port it was | 101 | // Port 0 asks the kernel to choose, and the hub prints the port it was |
| 114 | // asked for as the door to open — a door nobody could find. Refused | 102 | // asked for as the door to open — a door nobody could find. Refused |
| @@ -120,7 +108,7 @@ fn parseArgs( | |||
| 120 | // last run persisted. main decides what an empty argv means; the parse | 108 | // last run persisted. main decides what an empty argv means; the parse |
| 121 | // only reports what was on the line. | 109 | // only reports what was on the line. |
| 122 | p.key = xdg.pickKey(p.key, env_key); | 110 | p.key = xdg.pickKey(p.key, env_key); |
| 123 | return .{ .serve = p }; | 111 | return p; |
| 124 | } | 112 | } |
| 125 | 113 | ||
| 126 | pub fn main() !u8 { | 114 | pub fn main() !u8 { |
| @@ -132,18 +120,15 @@ pub fn main() !u8 { | |||
| 132 | const args = try std.process.argsAlloc(alloc); | 120 | const args = try std.process.argsAlloc(alloc); |
| 133 | defer std.process.argsFree(alloc, args); | 121 | defer std.process.argsFree(alloc, args); |
| 134 | 122 | ||
| 135 | const result = parseArgs(alloc, args, std.posix.getenv(xdg.key_env)) catch |err| switch (err) { | 123 | var parsed = parseArgs(alloc, args, std.posix.getenv(xdg.key_env)) catch |err| switch (err) { |
| 136 | error.Usage => { | 124 | error.Usage => { |
| 137 | std.debug.print("{s}", .{usage}); | 125 | std.debug.print("{s}", .{usage}); |
| 138 | return 2; | 126 | return 2; |
| 139 | }, | 127 | }, |
| 128 | error.Help => return cliflags.help(usage), | ||
| 129 | error.Version => return cliflags.version("muxweb", build_options.version), | ||
| 140 | else => |e| return e, | 130 | else => |e| return e, |
| 141 | }; | 131 | }; |
| 142 | var parsed = switch (result) { | ||
| 143 | .help => return cliflags.help(usage), | ||
| 144 | .version => return cliflags.version("muxweb", build_options.version), | ||
| 145 | .serve => |p| p, | ||
| 146 | }; | ||
| 147 | defer parsed.deinit(); | 132 | defer parsed.deinit(); |
| 148 | 133 | ||
| 149 | // An arena, because every string built here lives exactly as long as | 134 | // An arena, because every string built here lives exactly as long as |
| @@ -256,7 +241,7 @@ test "parse: three spellings become three tiles in argv order, port and key bind | |||
| 256 | const args = [_][:0]const u8{ | 241 | const args = [_][:0]const u8{ |
| 257 | "muxweb", "box1", "--sock", "/tmp/a.sock", "quic://h:4433", "--key", "/k", "--port", "8000", | 242 | "muxweb", "box1", "--sock", "/tmp/a.sock", "quic://h:4433", "--key", "/k", "--port", "8000", |
| 258 | }; | 243 | }; |
| 259 | var r = (try parseArgs(alloc, &args, null)).serve; | 244 | var r = try parseArgs(alloc, &args, null); |
| 260 | defer r.deinit(); | 245 | defer r.deinit(); |
| 261 | try std.testing.expectEqual(@as(usize, 3), r._argv.tiles.items.len); | 246 | try std.testing.expectEqual(@as(usize, 3), r._argv.tiles.items.len); |
| 262 | try std.testing.expectEqualStrings("box1", r._argv.tiles.items[0]); | 247 | try std.testing.expectEqualStrings("box1", r._argv.tiles.items[0]); |
| @@ -272,7 +257,7 @@ test "parse: a quoted '--sock PATH#SESSION' is the same tile as the two-argument | |||
| 272 | const alloc = std.testing.allocator; | 257 | const alloc = std.testing.allocator; |
| 273 | // The wall file's own spelling, pasted straight onto the command line: | 258 | // The wall file's own spelling, pasted straight onto the command line: |
| 274 | // muxweb used to refuse it while `mux wall` required it. | 259 | // muxweb used to refuse it while `mux wall` required it. |
| 275 | var r = (try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--sock /tmp/a.sock#b" }, null)).serve; | 260 | var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--sock /tmp/a.sock#b" }, null); |
| 276 | defer r.deinit(); | 261 | defer r.deinit(); |
| 277 | try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len); | 262 | try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len); |
| 278 | try std.testing.expectEqualStrings("--sock /tmp/a.sock#b", r._argv.tiles.items[0]); | 263 | try std.testing.expectEqualStrings("--sock /tmp/a.sock#b", r._argv.tiles.items[0]); |
| @@ -283,7 +268,7 @@ test "parse: zero targets, bad flags, and flag-beats-env" { | |||
| 283 | // No targets is an empty argv wall, not a refusal: restore-from-file | 268 | // No targets is an empty argv wall, not a refusal: restore-from-file |
| 284 | // semantics live in main, which is the only place that can read a file. | 269 | // semantics live in main, which is the only place that can read a file. |
| 285 | { | 270 | { |
| 286 | var r = (try parseArgs(alloc, &[_][:0]const u8{"muxweb"}, null)).serve; | 271 | var r = try parseArgs(alloc, &[_][:0]const u8{"muxweb"}, null); |
| 287 | defer r.deinit(); | 272 | defer r.deinit(); |
| 288 | try std.testing.expectEqual(@as(usize, 0), r._argv.tiles.items.len); | 273 | try std.testing.expectEqual(@as(usize, 0), r._argv.tiles.items.len); |
| 289 | } | 274 | } |
| @@ -310,24 +295,24 @@ test "parse: zero targets, bad flags, and flag-beats-env" { | |||
| 310 | // ...and an ordinary port still binds, so the refusal is the zero and | 295 | // ...and an ordinary port still binds, so the refusal is the zero and |
| 311 | // not the flag. | 296 | // not the flag. |
| 312 | { | 297 | { |
| 313 | var r = (try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--port", "1" }, null)).serve; | 298 | var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--port", "1" }, null); |
| 314 | defer r.deinit(); | 299 | defer r.deinit(); |
| 315 | try std.testing.expectEqual(@as(u16, 1), r.port); | 300 | try std.testing.expectEqual(@as(u16, 1), r.port); |
| 316 | } | 301 | } |
| 317 | // Env fills in when --key is absent; --key wins when both are set. | 302 | // Env fills in when --key is absent; --key wins when both are set. |
| 318 | { | 303 | { |
| 319 | var r = (try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h" }, "/env-key")).serve; | 304 | var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h" }, "/env-key"); |
| 320 | defer r.deinit(); | 305 | defer r.deinit(); |
| 321 | try std.testing.expectEqualStrings("/env-key", r.key.?); | 306 | try std.testing.expectEqualStrings("/env-key", r.key.?); |
| 322 | } | 307 | } |
| 323 | { | 308 | { |
| 324 | var r = (try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--key", "/flag-key" }, "/env-key")).serve; | 309 | var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--key", "/flag-key" }, "/env-key"); |
| 325 | defer r.deinit(); | 310 | defer r.deinit(); |
| 326 | try std.testing.expectEqualStrings("/flag-key", r.key.?); | 311 | try std.testing.expectEqualStrings("/flag-key", r.key.?); |
| 327 | } | 312 | } |
| 328 | // Empty either way means unset. | 313 | // Empty either way means unset. |
| 329 | { | 314 | { |
| 330 | var r = (try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h" }, "")).serve; | 315 | var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h" }, ""); |
| 331 | defer r.deinit(); | 316 | defer r.deinit(); |
| 332 | try std.testing.expectEqual(@as(?[]const u8, null), r.key); | 317 | try std.testing.expectEqual(@as(?[]const u8, null), r.key); |
| 333 | } | 318 | } |
| @@ -338,7 +323,7 @@ test "tiles: the spelling reaches the wall verbatim, `#NAME` and all" { | |||
| 338 | const args = [_][:0]const u8{ | 323 | const args = [_][:0]const u8{ |
| 339 | "muxweb", "host#b", "quic://h:1#b", "--sock", "/tmp/x#b", "plainhost", "a#b#c", | 324 | "muxweb", "host#b", "quic://h:1#b", "--sock", "/tmp/x#b", "plainhost", "a#b#c", |
| 340 | }; | 325 | }; |
| 341 | var r = (try parseArgs(alloc, &args, null)).serve; | 326 | var r = try parseArgs(alloc, &args, null); |
| 342 | defer r.deinit(); | 327 | defer r.deinit(); |
| 343 | try std.testing.expectEqual(@as(usize, 5), r._argv.tiles.items.len); | 328 | try std.testing.expectEqual(@as(usize, 5), r._argv.tiles.items.len); |
| 344 | 329 | ||
| @@ -385,12 +370,12 @@ test "tiles: a bad session name after # is still a usage error at parse" { | |||
| 385 | 370 | ||
| 386 | test "help is an answer, not a refusal, and -- fences the tiles from the flags" { | 371 | test "help is an answer, not a refusal, and -- fences the tiles from the flags" { |
| 387 | const alloc = std.testing.allocator; | 372 | const alloc = std.testing.allocator; |
| 388 | try std.testing.expect(try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--help" }, null) == .help); | 373 | try std.testing.expectError(error.Help, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--help" }, null)); |
| 389 | try std.testing.expect(try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "-h" }, null) == .help); | 374 | try std.testing.expectError(error.Help, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "-h" }, null)); |
| 390 | 375 | ||
| 391 | // Past `--` a word is a tile whatever it is spelled like: the escape a | 376 | // Past `--` a word is a tile whatever it is spelled like: the escape a |
| 392 | // host whose name reads as a flag would otherwise have none of. | 377 | // host whose name reads as a flag would otherwise have none of. |
| 393 | var r = (try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--", "host" }, null)).serve; | 378 | var r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "--", "host" }, null); |
| 394 | defer r.deinit(); | 379 | defer r.deinit(); |
| 395 | try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len); | 380 | try std.testing.expectEqual(@as(usize, 1), r._argv.tiles.items.len); |
| 396 | try std.testing.expectEqualStrings("host", r._argv.tiles.items[0]); | 381 | try std.testing.expectEqualStrings("host", r._argv.tiles.items[0]); |
| @@ -398,8 +383,7 @@ test "help is an answer, not a refusal, and -- fences the tiles from the flags" | |||
| 398 | 383 | ||
| 399 | test "version short-circuits everything else on the line" { | 384 | test "version short-circuits everything else on the line" { |
| 400 | const alloc = std.testing.allocator; | 385 | const alloc = std.testing.allocator; |
| 401 | const r = try parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--version", "--bogus" }, null); | 386 | try std.testing.expectError(error.Version, parseArgs(alloc, &[_][:0]const u8{ "muxweb", "h", "--version", "--bogus" }, null)); |
| 402 | try std.testing.expect(r == .version); | ||
| 403 | } | 387 | } |
| 404 | 388 | ||
| 405 | // Forces semantic analysis of every pub decl under `zig build test`, so an | 389 | // Forces semantic analysis of every pub decl under `zig build test`, so an |