From 8b9537dbf5983a88f79c63390418e684fefc832e Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 18 May 2026 15:47:06 -0700 Subject: [PATCH] migrate version to new cli framework --- src/commands/version.zig | 137 +++++++++++++++++++++++++-------------- src/main.zig | 11 +--- 2 files changed, 91 insertions(+), 57 deletions(-) diff --git a/src/commands/version.zig b/src/commands/version.zig index 3429b5f..13595e0 100644 --- a/src/commands/version.zig +++ b/src/commands/version.zig @@ -12,31 +12,51 @@ const builtin = @import("builtin"); const zfin = @import("../root.zig"); const version = @import("../version.zig"); const cli = @import("common.zig"); +const framework = @import("framework.zig"); const Date = @import("../Date.zig"); -/// Run the version command. -/// -/// `args` is the slice after `zfin version` (expects `--verbose`/`-v` or -/// nothing). Unknown args produce an error on stderr. -pub fn run( - io: std.Io, - config: zfin.Config, - args: []const []const u8, - out: *std.Io.Writer, -) !void { - var verbose = false; - for (args) |a| { +pub const ParsedArgs = struct { + verbose: bool = false, +}; + +pub const meta = struct { + pub const name: []const u8 = "version"; + pub const group: framework.Group = .infra; + pub const synopsis: []const u8 = "Show zfin version and build info"; + pub const help: []const u8 = + \\Usage: zfin version [-v|--verbose] + \\ + \\Print zfin's version + build date. With `--verbose`/`-v`, also + \\prints the Zig compiler version, build mode, build target, + \\resolved ZFIN_HOME, and cache directory — useful for bug + \\reports. + \\ + ; +}; + +comptime { + framework.validateCommandModule(@This()); +} + +/// Parse `--verbose`/`-v`. Unknown args produce an error on stderr. +pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs { + var parsed: ParsedArgs = .{}; + for (cmd_args) |a| { if (std.mem.eql(u8, a, "--verbose") or std.mem.eql(u8, a, "-v")) { - verbose = true; + parsed.verbose = true; } else { - try cli.stderrPrint(io, "Error: unknown argument to 'version': "); - try cli.stderrPrint(io, a); - try cli.stderrPrint(io, "\n"); + try cli.stderrPrint(ctx.io, "Error: unknown argument to 'version': "); + try cli.stderrPrint(ctx.io, a); + try cli.stderrPrint(ctx.io, "\n"); return error.UnexpectedArg; } } + return parsed; +} - try writeVersion(out, config, verbose); +/// Run the version command. +pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void { + try writeVersion(ctx.out, ctx.config, parsed.verbose); } /// Render version info into `out`. Separated from `run` so tests can @@ -95,11 +115,61 @@ fn stubConfig(zfin_home: ?[]const u8, cache_dir: []const u8) zfin.Config { }; } +/// Build a minimal `RunCtx` for tests. Most fields are unused by the +/// version command's `run` path; we set what writeVersion reads. +fn stubCtx(out: *std.Io.Writer, cfg: zfin.Config) framework.RunCtx { + return .{ + .io = std.testing.io, + .allocator = std.testing.allocator, + .gpa = std.testing.allocator, + .environ_map = undefined, + .config = cfg, + .svc = null, + .globals = .{}, + .today = Date.fromYmd(2026, 5, 9), + .now_s = 0, + .color = false, + .out = out, + }; +} + +test "parseArgs: no args → verbose=false" { + var ctx: framework.RunCtx = undefined; + ctx.io = std.testing.io; + const args = [_][]const u8{}; + const parsed = try parseArgs(&ctx, &args); + try std.testing.expect(!parsed.verbose); +} + +test "parseArgs: --verbose sets verbose" { + var ctx: framework.RunCtx = undefined; + ctx.io = std.testing.io; + const args = [_][]const u8{"--verbose"}; + const parsed = try parseArgs(&ctx, &args); + try std.testing.expect(parsed.verbose); +} + +test "parseArgs: -v short form" { + var ctx: framework.RunCtx = undefined; + ctx.io = std.testing.io; + const args = [_][]const u8{"-v"}; + const parsed = try parseArgs(&ctx, &args); + try std.testing.expect(parsed.verbose); +} + +test "parseArgs: unknown flag errors" { + var ctx: framework.RunCtx = undefined; + ctx.io = std.testing.io; + const args = [_][]const u8{"--bogus"}; + try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args)); +} + test "run: no args prints single-line banner" { var buf: [1024]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const cfg = stubConfig(null, "/tmp/test-cache"); - try run(std.testing.io, cfg, &.{}, &w); + var ctx = stubCtx(&w, cfg); + try run(&ctx, .{}); const out = w.buffered(); // Banner shape: starts with "zfin ", contains " (built ", ends with newline. @@ -121,8 +191,8 @@ test "run: --verbose includes all diagnostic fields" { var buf: [2048]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const cfg = stubConfig("/some/zfin/home", "/tmp/expected-cache-dir"); - const args = [_][]const u8{"--verbose"}; - try run(std.testing.io, cfg, &args, &w); + var ctx = stubCtx(&w, cfg); + try run(&ctx, .{ .verbose = true }); const out = w.buffered(); // Banner still first line @@ -144,33 +214,6 @@ test "run: --verbose includes all diagnostic fields" { try std.testing.expect(std.mem.indexOf(u8, out, builtin.zig_version_string) != null); } -test "run: -v short form equivalent to --verbose" { - var buf_long: [2048]u8 = undefined; - var buf_short: [2048]u8 = undefined; - var w_long: std.Io.Writer = .fixed(&buf_long); - var w_short: std.Io.Writer = .fixed(&buf_short); - const cfg = stubConfig("/zhome", "/cache"); - - try run(std.testing.io, cfg, &[_][]const u8{"--verbose"}, &w_long); - try run(std.testing.io, cfg, &[_][]const u8{"-v"}, &w_short); - - try std.testing.expectEqualStrings(w_long.buffered(), w_short.buffered()); -} - -test "run: unknown flag returns UnexpectedArg and writes nothing to out" { - var buf: [1024]u8 = undefined; - var w: std.Io.Writer = .fixed(&buf); - const cfg = stubConfig(null, "/cache"); - const args = [_][]const u8{"--bogus"}; - - try std.testing.expectError(error.UnexpectedArg, run(std.testing.io, cfg, &args, &w)); - - // The error path returns before any writing to `out`. Stderr output - // (the "unknown argument" line) goes through cli.stderrPrint directly - // and is not observable via the fixed writer. - try std.testing.expectEqual(@as(usize, 0), w.buffered().len); -} - test "writeVersion: ZFIN_HOME=null renders '(unset)'" { var buf: [2048]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); diff --git a/src/main.zig b/src/main.zig index 3ad2187..d8a0955 100644 --- a/src/main.zig +++ b/src/main.zig @@ -26,6 +26,7 @@ const command_modules = .{ // Infrastructure .cache = @import("commands/cache.zig"), + .version = @import("commands/version.zig"), }; comptime { @@ -388,16 +389,6 @@ fn runCli(init: std.process.Init) !u8 { var config = zfin.Config.fromEnv(io, allocator, init.environ_map); defer config.deinit(); - // Version: doesn't need DataService; uses build_info + Config paths. - if (std.mem.eql(u8, command, "version")) { - commands.version.run(io, config, cmd_args, out) catch |err| switch (err) { - error.UnexpectedArg => return 1, - else => return err, - }; - try out.flush(); - return 0; - } - var svc = zfin.DataService.init(io, allocator, config); defer svc.deinit();