authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 12:07:21-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:36-07:00
logea151030bc4a1366ef3ca0bd1f59d4867a215762
tree0af291d969bff89de2825221c52d65fa0b5abfc6
parent65c96c403589607f882468891c4e2c329a28fde9

std.Io.Writer: implement {q} formatter

This is intended to replace the common pattern: print("invalid foobar: '{s}': {t}", ...) The idea is to not invent imaginary syntax. If the string contained single quotes for example, this would be a nonsensical error message. On the other hand with the new pattern: print("invalid foobar: {q}: {t}", ...) It's both easier on the eyes at the print site, and also it will allow the user to copy paste a properly escaped string, should the quoted text contain any odd characters, including invisible ones like null bytes.

1 files changed, 16 insertions(+), 14 deletions(-)

lib/std/Io/Writer.zig+16-14
...@@ -1172,23 +1172,12 @@ pub fn printValue(...@@ -1172,23 +1172,12 @@ pub fn printValue(
1172 },1172 },
1173 else => invalidFmtError(fmt, value),1173 else => invalidFmtError(fmt, value),
1174 },1174 },
1175 // TODO make this print double quotes and quote-escape the string
1176 // according to zig string syntax rules
1177 'q' => switch (@typeInfo(T)) {1175 'q' => switch (@typeInfo(T)) {
1178 .pointer => |info| switch (info.size) {1176 .pointer => |info| switch (info.size) {
1179 .one, .slice => {1177 .one, .slice => return printStringEscaped(w, value),
1180 const slice: []const u8 = value;1178 .many, .c => return printStringEscaped(w, std.mem.span(value)),
1181 return w.alignBufferOptions(slice, options);
1182 },
1183 .many, .c => {
1184 const slice: [:0]const u8 = std.mem.span(value);
1185 return w.alignBufferOptions(slice, options);
1186 },
1187 },
1188 .array => {
1189 const slice: []const u8 = &value;
1190 return w.alignBufferOptions(slice, options);
1191 },1179 },
1180 .array => return printStringEscaped(w, &value),
1192 else => invalidFmtError(fmt, value),1181 else => invalidFmtError(fmt, value),
1193 },1182 },
1194 'B' => switch (@typeInfo(T)) {1183 'B' => switch (@typeInfo(T)) {
...@@ -1467,6 +1456,14 @@ fn printEnumNonexhaustive(w: *Writer, value: anytype) Error!void {...@@ -1467,6 +1456,14 @@ fn printEnumNonexhaustive(w: *Writer, value: anytype) Error!void {
1467 try w.writeByte(')');1456 try w.writeByte(')');
1468}1457}
14691458
1459/// Prints a double quote, then escapes a string according to Zig string
1460/// literal rules, then a double quote.
1461pub fn printStringEscaped(w: *Writer, bytes: []const u8) Error!void {
1462 try w.writeByte('"');
1463 try std.zig.stringEscape(bytes, w);
1464 try w.writeByte('"');
1465}
1466
1470pub fn printVector(1467pub fn printVector(
1471 w: *Writer,1468 w: *Writer,
1472 comptime fmt: []const u8,1469 comptime fmt: []const u8,
...@@ -2121,6 +2118,11 @@ test "printFloat with comptime_float" {...@@ -2121,6 +2118,11 @@ test "printFloat with comptime_float" {
2121 try testing.expectFmt("1", "{}", .{1.0});2118 try testing.expectFmt("1", "{}", .{1.0});
2122}2119}
21232120
2121test "{q} format string" {
2122 const data: []const u8 = "i\tlike\"cheese\x00\x05cheese";
2123 try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data});
2124}
2125
2124fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {2126fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {
2125 var buffer: [100]u8 = undefined;2127 var buffer: [100]u8 = undefined;
2126 var w: Writer = .fixed(&buffer);2128 var w: Writer = .fixed(&buffer);