Hi everyone, I tried to create a small command-line application which reads a ZON config file. My config file (named config.zon
) looks like this:
.{
.a = 42,
}
And then my program (named usesconfig.zig
) looks like this:
const std = @import("std");
const Config = struct {
a: i32 = 1,
b: i32 = 2,
};
pub fn main() !void {
// TODO: use default allocator when available
const allocator = std.heap.smp_allocator;
const stdout = std.io.getStdErr().writer();
const stderr = std.io.getStdErr().writer();
// Any way to write this easier?
// See also https://github.com/ziglang/zig/issues/2792
const config: Config = loadconf: {
const args: [][:0]u8 = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 2) {
// Should I use std.debug.print instead of stderr?
// It's easier, but this is not debugging.
try stderr.print(
"Expecting config file name as single argument.\n",
.{},
);
return std.process.exit(1);
}
const cfg_filename = args[1];
// This seems straight forward:
var cfg_string = std.fs.cwd().readFileAlloc(
allocator,
cfg_filename,
1024 * 1024,
) catch |err| {
try stderr.print(
"Could not read config file \"{s}\": {s}\n",
.{ cfg_filename, @errorName(err) },
);
return std.process.exit(1);
};
defer allocator.free(cfg_string);
// But std.zon.parse.fromSlice requires a NULL-terminated slice.
// So there is some overhead.
// Is this the right way to add a zero byte? Feels verbose.
cfg_string = try allocator.realloc(cfg_string, cfg_string.len + 1);
cfg_string[cfg_string.len - 1] = 0;
const cfg_string_term = cfg_string[0 .. cfg_string.len - 1 :0];
var diagnostics = std.zon.parse.Diagnostics{};
defer diagnostics.deinit(allocator);
break :loadconf std.zon.parse.fromSlice(
Config,
allocator,
cfg_string_term,
&diagnostics,
.{},
) catch {
try stderr.print("Could not parse config file \"{s}\":\n{any}", .{
cfg_filename,
diagnostics,
});
return std.process.exit(1);
};
};
defer std.zon.parse.free(allocator, config);
try stdout.print("Configuration is {any}\n", .{config});
}
I can run with zig run usesconfig.zig -- config.zon
, and then I get the following output:
Configuration is usesconfig.Config{ .a = 42, .b = 2 }
My questions in this context are:
- Is it a good or bad idea to use ZON for configuring a program?
- Is my code idiomatic? (Ignoring the choice of allocator)
- When reporting errors, should I use
std.debug.print
orstd.io.getStdErr
? Is reporting error messages considered “debugging” in that matter? - Is it a good idea to read a configuration file all at once by using
std.fs.Dir.readFileAlloc
, or are there better alternatives? - Why does
std.zon.parse.fromSlize
require a null-terminated slice? Any easier way to go from having a filename of my configuration file to the parsed struct without manual reallocation to add the NULL byte?
Your help and opinions are appreciated.
Edit: I forgot defer diagnostics.deinit(allocator);
which I added in the code above.
Edit #2: Apparently my code requires the current master branch as the 0.14.1 release has a different signature for std.zon.parse.fromSlice
. The version I use is 0.15.0-dev.847+850655f06
.
Edit #3: I also forgot this (and added it above), even if it seems to be a no-op with the given Config
type:
};
+ defer std.zon.parse.free(allocator, config);
try stdout.print("Configuration is {any}\n", .{config});