For fun I’m noodling on the idea of an interactive Zig format specifier “tester”, in a similar vein to this interactive regex tester. Imagine two text input boxes:
Format string: "A number: {d:.1}"
Format args: 10.11
And an output box showing in case of success:
Output: "A number: 10.1" (The formatted string)
And in the case of failure/invalid format strings/args:
Output: Some specific error based on the compilation failure message, bonus points if I can point to what exactly in the format string/args is wrong
Due to the compile time nature of Zig formatting specifiers, I’ve come up with the following toy program:
build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const code_gen_mod = b.createModule(.{
.root_source_file = b.path("src/generate_format_args.zig"),
.target = target,
.optimize = optimize,
});
const code_gen_exe = b.addExecutable(.{
.name = "code_gen",
.root_module = code_gen_mod,
});
const code_gen_cmd = b.addRunArtifact(code_gen_exe);
const format_args = b.option([]const u8, "format_args", "Contents of format args") orelse "";
code_gen_cmd.addArg(format_args);
const generated_zig = code_gen_cmd.captureStdOut();
const options = b.addOptions();
const format_string = b.option([]const u8, "format_string", "format string for program") orelse @panic("Must supply a format string");
options.addOption([]const u8, "format_string", format_string);
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "fmt_tester",
.root_module = exe_mod,
});
exe_mod.addOptions("options", options);
exe_mod.addAnonymousImport("generated_args", .{ .root_source_file = generated_zig });
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_unit_tests = b.addTest(.{
.root_module = exe_mod,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
}
main.zig:
const std = @import("std");
const generated_args = @import("generated_args");
const format_string = @import("options").format_string;
pub fn main() !void {
const msg = try std.fmt.allocPrint(std.heap.page_allocator, format_string, generated_args.args);
defer std.heap.page_allocator.free(msg);
std.debug.print("{s}\r\n", .{msg});
}
generate_format_args.zig:
const std = @import("std");
pub fn main() !void {
var args = try std.process.argsWithAllocator(std.heap.page_allocator);
defer args.deinit();
_ = args.next() orelse unreachable;
const format_args: []const u8 = args.next() orelse "";
const zig_file_content = "pub const args = .{{{s}}};";
const stdout = std.io.getStdOut().writer();
stdout.print(zig_file_content, .{format_args}) catch |err| errorExit("Error with stdout: {any}", .{err});
return std.process.cleanExit();
}
fn errorExit(comptime format: []const u8, args: anytype) noreturn {
std.io.getStdErr().writer().print(format, args) catch unreachable;
std.process.exit(1);
}
It behaves like so:
Valid args:
zig build -Dformat_string="{d:.1}, {d:.2}, {d:.1}" -Dformat_args="10, 11.0, 22.33" run
10, 11.00, 22.3
Invalid args:
zig build -Dformat_string="{d:.1}, {d:.2}, {d:.1}" -Dformat_args="10, 11.0" run
...
/home/hayden/.zvm/0.14.1/lib/std/fmt.zig:191:13: error: too few arguments
@compileError("too few arguments");
However, now it’s time to write the code that will do the following:
- Take both inputs (format string + args)
- Build Zig program
- If success:
- Run program and get output
- If compile fail:
- Parse failure message to do something useful with it
Is there a “standard” way to drive the Zig build system from Zig code itself? Or am I better off using a higher level scripting language like Python for this purpose?