migrate version to new cli framework

This commit is contained in:
Emil Lerch 2026-05-18 15:47:06 -07:00
parent 722ddc0e98
commit ef21be8603
2 changed files with 91 additions and 57 deletions

View file

@ -12,31 +12,51 @@ const builtin = @import("builtin");
const zfin = @import("../root.zig"); const zfin = @import("../root.zig");
const version = @import("../version.zig"); const version = @import("../version.zig");
const cli = @import("common.zig"); const cli = @import("common.zig");
const framework = @import("framework.zig");
const Date = @import("../Date.zig"); const Date = @import("../Date.zig");
/// Run the version command. pub const ParsedArgs = struct {
/// verbose: bool = false,
/// `args` is the slice after `zfin version` (expects `--verbose`/`-v` or };
/// nothing). Unknown args produce an error on stderr.
pub fn run( pub const meta = struct {
io: std.Io, pub const name: []const u8 = "version";
config: zfin.Config, pub const group: framework.Group = .infra;
args: []const []const u8, pub const synopsis: []const u8 = "Show zfin version and build info";
out: *std.Io.Writer, pub const help: []const u8 =
) !void { \\Usage: zfin version [-v|--verbose]
var verbose = false; \\
for (args) |a| { \\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")) { if (std.mem.eql(u8, a, "--verbose") or std.mem.eql(u8, a, "-v")) {
verbose = true; parsed.verbose = true;
} else { } else {
try cli.stderrPrint(io, "Error: unknown argument to 'version': "); try cli.stderrPrint(ctx.io, "Error: unknown argument to 'version': ");
try cli.stderrPrint(io, a); try cli.stderrPrint(ctx.io, a);
try cli.stderrPrint(io, "\n"); try cli.stderrPrint(ctx.io, "\n");
return error.UnexpectedArg; 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 /// 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" { test "run: no args prints single-line banner" {
var buf: [1024]u8 = undefined; var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf); var w: std.Io.Writer = .fixed(&buf);
const cfg = stubConfig(null, "/tmp/test-cache"); 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(); const out = w.buffered();
// Banner shape: starts with "zfin ", contains " (built ", ends with newline. // 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 buf: [2048]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf); var w: std.Io.Writer = .fixed(&buf);
const cfg = stubConfig("/some/zfin/home", "/tmp/expected-cache-dir"); const cfg = stubConfig("/some/zfin/home", "/tmp/expected-cache-dir");
const args = [_][]const u8{"--verbose"}; var ctx = stubCtx(&w, cfg);
try run(std.testing.io, cfg, &args, &w); try run(&ctx, .{ .verbose = true });
const out = w.buffered(); const out = w.buffered();
// Banner still first line // 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); 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)'" { test "writeVersion: ZFIN_HOME=null renders '(unset)'" {
var buf: [2048]u8 = undefined; var buf: [2048]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf); var w: std.Io.Writer = .fixed(&buf);

View file

@ -26,6 +26,7 @@ const command_modules = .{
// Infrastructure // Infrastructure
.cache = @import("commands/cache.zig"), .cache = @import("commands/cache.zig"),
.version = @import("commands/version.zig"),
}; };
comptime { comptime {
@ -388,16 +389,6 @@ fn runCli(init: std.process.Init) !u8 {
var config = zfin.Config.fromEnv(io, allocator, init.environ_map); var config = zfin.Config.fromEnv(io, allocator, init.environ_map);
defer config.deinit(); 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); var svc = zfin.DataService.init(io, allocator, config);
defer svc.deinit(); defer svc.deinit();