feat(pretty_print): handle union and pointer (including slices)
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 37s
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 37s
This commit is contained in:
110
README.md
110
README.md
@@ -1,20 +1,36 @@
|
||||
# zlog
|
||||
|
||||
Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pretty print function for easy overwriting of user defined types (`struct`, `enum`, `union` and `vector`).
|
||||
Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pretty print function for easy overwriting of user defined types.
|
||||
|
||||
> [!CAUTION]
|
||||
> Only builds using the zig master version are tested to work.
|
||||
|
||||
## Usage
|
||||
|
||||
The following snippet shows an example usage of `zlog` to change the default log format and add pretty printing to both user defined types (`Options` and `Struct`):
|
||||
The following snippet shows an example usage of `zlog` to change the default log format and add pretty printing to the user defined types:
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
const zlog = @import("zlog");
|
||||
const Union = union {
|
||||
int: i32,
|
||||
boolean: bool,
|
||||
|
||||
// use this to overwrite the default log format of `std.log`
|
||||
pub const std_options = zlog.std_options;
|
||||
// copy and paste this function into your user defined types to enable pretty printing for these types
|
||||
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
try zlog.pretty_format(value, fmt, options, writer);
|
||||
}
|
||||
};
|
||||
|
||||
const TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
|
||||
// copy and paste this function into your user defined types to enable pretty printing for these types
|
||||
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
try zlog.pretty_format(value, fmt, options, writer);
|
||||
}
|
||||
};
|
||||
|
||||
const Options = enum {
|
||||
a,
|
||||
@@ -32,7 +48,8 @@ const Struct = struct {
|
||||
b: bool = true,
|
||||
c: [5]u16 = .{ 1, 2, 3, 4, 5 },
|
||||
d: []const u8 = "string",
|
||||
e: Options = Options.b,
|
||||
e: Options = .b,
|
||||
f: TaggedUnion = .{ .one = -5 },
|
||||
|
||||
// same function as above...
|
||||
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
@@ -43,15 +60,20 @@ const Struct = struct {
|
||||
pub fn main() void {
|
||||
// initialize zlog with the scope of `main`
|
||||
const log = std.log.scoped(.main);
|
||||
// NOTE: the scope of `default` will result in no scoping being printed in
|
||||
// the resulting output (default behavior of `std.log.defaultLog`)
|
||||
// NOTE the scope of `default` will result in no scoping being printed in
|
||||
// the resulting output (default behavior of `std.log.defaultLog`)
|
||||
|
||||
// some variables to log
|
||||
const int_var = 42;
|
||||
const array_var = [_]i32{ 1, 2, 3, 4 };
|
||||
const string_var = "This is a test";
|
||||
const option_var = Options.a;
|
||||
const option_var: Options = .a;
|
||||
const struct_var: Struct = .{};
|
||||
const tagged_union: TaggedUnion = .{
|
||||
.three = "Three",
|
||||
};
|
||||
const void_tagged_union: TaggedUnion = .nothing;
|
||||
const uniun: Union = .{ .boolean = true };
|
||||
|
||||
// NOTE: Depending on the optimization target some of these log messages
|
||||
// will not show, which is inline with the behavior of `std.log`.
|
||||
@@ -59,32 +81,62 @@ pub fn main() void {
|
||||
log.info("Info message {any}", .{array_var});
|
||||
log.info("Info message \"{s}\"", .{string_var});
|
||||
log.warn("Warning message {any}", .{option_var});
|
||||
log.warn("Warning message {any}", .{void_tagged_union});
|
||||
log.warn("Warning message {any}", .{uniun});
|
||||
log.err("Error message {any}", .{struct_var});
|
||||
log.err("Error message {any}", .{tagged_union});
|
||||
}
|
||||
|
||||
pub const std_options = zlog.std_options;
|
||||
|
||||
const std = @import("std");
|
||||
const zlog = @import("zlog");
|
||||
```
|
||||
|
||||
This will result in the following output:
|
||||
|
||||
```
|
||||
[[1m2025-02-24 13:00:08[0m] [[38;5;12mdebug[0m]([2mmain[0m): Debug message 42
|
||||
[[1m2025-02-24 13:00:08[0m] [[38;5;10minfo[0m]([2mmain[0m): Info message { 1, 2, 3, 4 }
|
||||
[[1m2025-02-24 13:00:08[0m] [[38;5;10minfo[0m]([2mmain[0m): Info message "This is a test"
|
||||
[[1m2025-02-24 13:00:08[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.Options = enum {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
} = a
|
||||
[[1m2025-02-24 13:00:08[0m] [[38;5;9merror[0m]([2mmain[0m): Error message main.Struct = struct {
|
||||
.a = 42,
|
||||
.b = true,
|
||||
.c = []u16: { 1, 2, 3, 4, 5 },
|
||||
.d = { 115, 116, 114, 105, 110, 103 },
|
||||
.e = main.Options = enum {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
} = b,
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;12mdebug[0m]([2mmain[0m): Debug message 42
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;10minfo[0m]([2mmain[0m): Info message { 1, 2, 3, 4 }
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;10minfo[0m]([2mmain[0m): Info message "This is a test"
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.Options = enum {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
} = .a
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
} = .{ .nothing = void }
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;11mwarning[0m]([2mmain[0m): Warning message main.Union = union {
|
||||
int: i32,
|
||||
boolean: bool,
|
||||
} = @7fffd1c71048
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;9merror[0m]([2mmain[0m): Error message main.Struct = struct {
|
||||
.a = 42,
|
||||
.b = true,
|
||||
.c = []u16: { 1, 2, 3, 4, 5 },
|
||||
.d = "string",
|
||||
.e = main.Options = enum {
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
} = .b,
|
||||
.f = main.TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
} = .{ .one = -5 },
|
||||
}
|
||||
[[1m2025-07-17 19:41:43[0m] [[38;5;9merror[0m]([2mmain[0m): Error message main.TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
} = .{ .three = "Three" }
|
||||
```
|
||||
|
||||
## Customization
|
||||
@@ -112,4 +164,4 @@ The following list shows some tips on how to use logging more effectively. These
|
||||
break :port try fmt.parseInt(u16, buf[0 .. len -| 1], 10);
|
||||
};
|
||||
```
|
||||
- When looking through the output of the log (i.e. written to disk by `program 2> log`) the `log` file contains control code characters (*ansi*) for the colored outputs. You can still see them correctly with the following command `less -rf log`
|
||||
- When looking through the output of the log (i.e. written to disk by `program 2> log` when using the _stderr_ build option) the `log` file contains control code characters (*ansi*) for the colored outputs. You can still see them correctly with the following command `less -R log`.
|
||||
|
||||
45
src/main.zig
45
src/main.zig
@@ -1,7 +1,24 @@
|
||||
const std = @import("std");
|
||||
const zlog = @import("zlog");
|
||||
const Union = union {
|
||||
int: i32,
|
||||
boolean: bool,
|
||||
|
||||
pub const std_options = zlog.std_options;
|
||||
// copy and paste this function into your user defined types to enable pretty printing for these types
|
||||
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
try zlog.pretty_format(value, fmt, options, writer);
|
||||
}
|
||||
};
|
||||
|
||||
const TaggedUnion = union(enum) {
|
||||
one: i16,
|
||||
two: u32,
|
||||
three: []const u8,
|
||||
nothing,
|
||||
|
||||
// copy and paste this function into your user defined types to enable pretty printing for these types
|
||||
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
try zlog.pretty_format(value, fmt, options, writer);
|
||||
}
|
||||
};
|
||||
|
||||
const Options = enum {
|
||||
a,
|
||||
@@ -19,7 +36,8 @@ const Struct = struct {
|
||||
b: bool = true,
|
||||
c: [5]u16 = .{ 1, 2, 3, 4, 5 },
|
||||
d: []const u8 = "string",
|
||||
e: Options = Options.b,
|
||||
e: Options = .b,
|
||||
f: TaggedUnion = .{ .one = -5 },
|
||||
|
||||
// same function as above...
|
||||
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
@@ -30,15 +48,20 @@ const Struct = struct {
|
||||
pub fn main() void {
|
||||
// initialize zlog with the scope of `main`
|
||||
const log = std.log.scoped(.main);
|
||||
// NOTE: the scope of `default` will result in no scoping being printed in
|
||||
// the resulting output (default behavior of `std.log.defaultLog`)
|
||||
// NOTE the scope of `default` will result in no scoping being printed in
|
||||
// the resulting output (default behavior of `std.log.defaultLog`)
|
||||
|
||||
// some variables to log
|
||||
const int_var = 42;
|
||||
const array_var = [_]i32{ 1, 2, 3, 4 };
|
||||
const string_var = "This is a test";
|
||||
const option_var = Options.a;
|
||||
const option_var: Options = .a;
|
||||
const struct_var: Struct = .{};
|
||||
const tagged_union: TaggedUnion = .{
|
||||
.three = "Three",
|
||||
};
|
||||
const void_tagged_union: TaggedUnion = .nothing;
|
||||
const uniun: Union = .{ .boolean = true };
|
||||
|
||||
// NOTE: Depending on the optimization target some of these log messages
|
||||
// will not show, which is inline with the behavior of `std.log`.
|
||||
@@ -46,5 +69,13 @@ pub fn main() void {
|
||||
log.info("Info message {any}", .{array_var});
|
||||
log.info("Info message \"{s}\"", .{string_var});
|
||||
log.warn("Warning message {any}", .{option_var});
|
||||
log.warn("Warning message {any}", .{void_tagged_union});
|
||||
log.warn("Warning message {any}", .{uniun});
|
||||
log.err("Error message {any}", .{struct_var});
|
||||
log.err("Error message {any}", .{tagged_union});
|
||||
}
|
||||
|
||||
pub const std_options = zlog.std_options;
|
||||
|
||||
const std = @import("std");
|
||||
const zlog = @import("zlog");
|
||||
|
||||
157
src/zlog.zig
157
src/zlog.zig
@@ -1,23 +1,14 @@
|
||||
const build_options = @import("build_options");
|
||||
const std = @import("std");
|
||||
const ztime = if (build_options.timestamp) @import("ztime") else null;
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.logFn = logFn,
|
||||
};
|
||||
|
||||
// zlog defaultLog function replacement, which adjusts the surrounding contents of every std.log message.
|
||||
/// *zlog* defaultLog function replacement, which adjusts the surrounding contents of every `std.log` message.
|
||||
fn logFn(
|
||||
comptime message_level: std.log.Level,
|
||||
comptime message_level: log.Level,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
// TODO: provide build time configuration to allow tweaking corresponding output
|
||||
// TODO provide build time configuration to allow tweaking corresponding output
|
||||
// - change output file for writing messages to (default `stderr`)
|
||||
// write into own file for each level?
|
||||
// - write into own file for each level?
|
||||
|
||||
// TODO: make the level text colored according to the level!
|
||||
const prefix = if (scope == .default) ": " else "(\x1b[2m" ++ @tagName(scope) ++ "\x1b[0m): ";
|
||||
const level_txt = switch (comptime message_level) {
|
||||
.err => "[\x1b[38;5;9merror\x1b[0m]",
|
||||
@@ -25,10 +16,10 @@ fn logFn(
|
||||
.info => "[\x1b[38;5;10minfo\x1b[0m]",
|
||||
.debug => "[\x1b[38;5;12mdebug\x1b[0m]",
|
||||
};
|
||||
const fmt = level_txt ++ prefix ++ format ++ "\n";
|
||||
const complete_format = level_txt ++ prefix ++ format ++ "\n";
|
||||
if (comptime build_options.file.len != 0) {
|
||||
// TODO: handle errors accordingly (i.e. panic?)
|
||||
// NOTE: with zig 0.13.0 there is currently no way to open files to append (except to use libc or talk directly to posix, which this lib should not have to do)
|
||||
// TODO handle errors accordingly (i.e. panic?)
|
||||
// NOTE with zig 0.13.0 there is currently no way to open files to append (except to use libc or talk directly to posix, which this lib should not have to do)
|
||||
const file = std.fs.openFileAbsolute(build_options.file, .{
|
||||
.mode = .read_write,
|
||||
}) catch std.fs.createFileAbsolute(build_options.file, .{
|
||||
@@ -36,7 +27,7 @@ fn logFn(
|
||||
}) catch @panic("Failed to open and/or create configured log file");
|
||||
defer file.close();
|
||||
|
||||
var buffer = std.io.bufferedWriter(file.writer());
|
||||
var buffer = io.bufferedWriter(file.writer());
|
||||
defer buffer.flush() catch {};
|
||||
|
||||
const writer = buffer.writer();
|
||||
@@ -44,21 +35,21 @@ fn logFn(
|
||||
}
|
||||
|
||||
if (comptime build_options.stderr) {
|
||||
var buffer = std.io.bufferedWriter(std.io.getStdErr().writer());
|
||||
var buffer = io.bufferedWriter(io.getStdErr().writer());
|
||||
defer buffer.flush() catch {};
|
||||
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
const writer = buffer.writer();
|
||||
log_writing(writer, fmt, args);
|
||||
log_writing(writer, complete_format, args);
|
||||
}
|
||||
}
|
||||
|
||||
inline fn log_writing(writer: anytype, comptime fmt: []const u8, args: anytype) void {
|
||||
inline fn log_writing(writer: anytype, comptime format: []const u8, args: anytype) void {
|
||||
nosuspend {
|
||||
if (build_options.timestamp) log_timestamp(writer);
|
||||
writer.print(fmt, args) catch return;
|
||||
writer.print(format, args) catch return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +57,14 @@ inline fn log_timestamp(writer: anytype) void {
|
||||
writer.print("[\x1b[1m{any}\x1b[0m] ", .{ztime.DateTime.now()}) catch return;
|
||||
}
|
||||
|
||||
pub fn pretty_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
try inner_format(object, fmt, options, writer, 0);
|
||||
pub fn pretty_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype) !void {
|
||||
try inner_format(object, format, options, writer, 0);
|
||||
}
|
||||
|
||||
fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, comptime depth: u8) !void {
|
||||
// TODO: here `std.meta` might be useful
|
||||
fn inner_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype, comptime depth: u8) !void {
|
||||
const Object = @TypeOf(object);
|
||||
const object_info = @typeInfo(Object);
|
||||
|
||||
switch (object_info) {
|
||||
.@"struct" => |s| {
|
||||
try writer.writeAll(@typeName(Object));
|
||||
@@ -84,8 +75,8 @@ fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.Form
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll(" = ");
|
||||
// TODO check corresponding type and try to adapt fmt accordingly!
|
||||
// TODO: pass along the already parsed formatting options too
|
||||
try inner_format(@field(object, field.name), fmt, options, writer, depth + 1);
|
||||
// TODO pass along the already parsed formatting options too
|
||||
try inner_format(@field(object, field.name), format, options, writer, depth + 1);
|
||||
try writer.writeAll(",\n");
|
||||
}
|
||||
try writer.writeAll("\t" ** depth);
|
||||
@@ -100,27 +91,109 @@ fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.Form
|
||||
try writer.writeAll(",\n");
|
||||
}
|
||||
try writer.writeAll("\t" ** depth);
|
||||
try writer.writeAll("} = ");
|
||||
try writer.writeAll("} = .");
|
||||
try writer.writeAll(@tagName(object));
|
||||
},
|
||||
// TODO: implement prett_printing for other user defined types (`union` and `vector`)
|
||||
// TODO: recognize []const u8 types and print them as strings
|
||||
.array => |a| {
|
||||
if (a.child == @TypeOf([:0]const u8)) {
|
||||
try std.fmt.format(writer, "\"{s}\"", .{object});
|
||||
.@"union" => |u| {
|
||||
try writer.writeAll(@typeName(Object));
|
||||
try writer.writeAll(" = union");
|
||||
if (u.tag_type) |Tag| {
|
||||
_ = Tag;
|
||||
try writer.writeAll("(enum)");
|
||||
}
|
||||
try writer.writeAll(" {\n");
|
||||
inline for (u.fields) |field| {
|
||||
try writer.writeAll("\t" ** (depth + 1));
|
||||
try writer.writeAll(field.name);
|
||||
if (@typeInfo(field.type) != .void) {
|
||||
try writer.writeAll(": ");
|
||||
try writer.writeAll(@typeName(field.type));
|
||||
}
|
||||
try writer.writeAll(",\n");
|
||||
}
|
||||
try writer.writeAll("\t" ** depth);
|
||||
try writer.writeAll("} = ");
|
||||
if (u.tag_type) |Tag| {
|
||||
try writer.writeAll(".{ .");
|
||||
try writer.writeAll(@tagName(object));
|
||||
try writer.writeAll(" = ");
|
||||
inline for (u.fields) |u_field| if (object == @field(Tag, u_field.name)) {
|
||||
try inner_format(@field(object, u_field.name), format, options, writer, depth + 1);
|
||||
};
|
||||
try writer.writeAll(" }");
|
||||
} else {
|
||||
try std.fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object });
|
||||
// NOTE the value of a union (untagged) is displayed like this also in the standard library,
|
||||
// not sure if you can reflect the used variant (and its value)
|
||||
try fmt.format(writer, "@{x}", .{@intFromPtr(&object)});
|
||||
}
|
||||
},
|
||||
.vector => |v| {
|
||||
if (v.child == @TypeOf([:0]const u8)) {
|
||||
try std.fmt.format(writer, "\"{s}\"", .{object});
|
||||
} else {
|
||||
try std.fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object });
|
||||
}
|
||||
.pointer => |p| switch (p.size) {
|
||||
.slice => switch (@typeInfo(p.child)) {
|
||||
.int => |num| {
|
||||
if (num.signedness != .unsigned) {
|
||||
try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object });
|
||||
} else {
|
||||
switch (num.bits) {
|
||||
8 => try fmt.format(writer, "\"{s}\"", .{object}),
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }),
|
||||
}
|
||||
}
|
||||
},
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }),
|
||||
},
|
||||
.c => switch (@typeInfo(p.child)) {
|
||||
.int => |num| {
|
||||
if (num.signedness != .unsigned) {
|
||||
try fmt.format(writer, "[*c]{s}: {any}", .{ @typeName(p.child), object });
|
||||
} else {
|
||||
switch (num.bits) {
|
||||
8 => try fmt.format(writer, "\"{s}\"", .{object}),
|
||||
else => try fmt.format(writer, "[*c]{s}: {any}", .{ @typeName(p.child), object }),
|
||||
}
|
||||
}
|
||||
},
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }),
|
||||
},
|
||||
.many => try fmt.format(writer, "[*]{s}: {any}", .{ @typeName(p.child), object }),
|
||||
.one => try fmt.format(writer, "[1]{s}: {any}", .{ @typeName(p.child), object }),
|
||||
},
|
||||
else => {
|
||||
try std.fmt.format(writer, "{any}", .{object});
|
||||
.array => |a| switch (@typeInfo(a.child)) {
|
||||
.int => |num| {
|
||||
if (num.signedness != .unsigned) {
|
||||
try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object });
|
||||
} else {
|
||||
switch (num.bits) {
|
||||
8 => try fmt.format(writer, "\"{s}\"", .{object}),
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }),
|
||||
}
|
||||
}
|
||||
},
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }),
|
||||
},
|
||||
.vector => |v| switch (@typeInfo(v.child)) {
|
||||
.int => |num| {
|
||||
if (num.signedness != .unsigned) {
|
||||
try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object });
|
||||
} else {
|
||||
switch (num.bits) {
|
||||
8 => try fmt.format(writer, "\"{s}\"", .{object}),
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }),
|
||||
}
|
||||
}
|
||||
},
|
||||
else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }),
|
||||
},
|
||||
else => try fmt.format(writer, "{any}", .{object}),
|
||||
}
|
||||
}
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.logFn = logFn,
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const io = std.io;
|
||||
const log = std.log;
|
||||
const fmt = std.fmt;
|
||||
const build_options = @import("build_options");
|
||||
const ztime = if (build_options.timestamp) @import("ztime") else null;
|
||||
|
||||
Reference in New Issue
Block a user