From 7e012b4f7c08e234b7e13cd7875a3a496dd80956 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 20 Aug 2026 16:23:10 -0700 Subject: [PATCH] std.Io.Writer: introduce {qf} calls format function and then double quote escapes it --- lib/std/Io/Writer.zig | 17 +++++++++++ lib/std/zig.zig | 68 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 7a6f4468964c4b5f8c8a1a6ff86fcd6d7a63a6b4..e7c54d9a52549577aafd34c34a27f648e86e490f 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -1231,6 +1231,18 @@ pub fn printValue( }, else => {}, }, + 'q' => switch (fmt[1]) { + 'f' => { + try w.writeByte('"'); + var buffer: [64]u8 = undefined; + var escaping_writer: std.zig.StringEscapeWriter = .init(w, &buffer); + try value.format(&escaping_writer.writer); + try escaping_writer.writer.flush(); + try w.writeByte('"'); + return; + }, + else => {}, + }, else => {}, }, 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) { @@ -2143,6 +2155,11 @@ test "{q} format string" { try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data}); } +test "{qf} format string" { + const data: []const u8 = "😎"; + try testing.expectFmt("hello \"@\\\"😎\\\"\" world", "hello {qf} world", .{std.zig.fmtId(data)}); +} + fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void { var buffer: [100]u8 = undefined; var w: Writer = .fixed(&buffer); diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 6f060d0550047ec569ccb14ef12330f7db45ffc6..e18a6414c6e9ba130640e061307c68b16ff02456 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -545,20 +545,76 @@ test fmtChar { /// /// Everything else is passed through unmodified. pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void { + _ = try stringEscapeCounting(bytes, w); +} + +pub fn stringEscapeCounting(bytes: []const u8, w: *Writer) Writer.Error!usize { + var n: usize = 0; for (bytes) |byte| switch (byte) { - '\t' => try w.writeAll("\\t"), - '\n' => try w.writeAll("\\n"), - '\r' => try w.writeAll("\\r"), - '\\' => try w.writeAll("\\\\"), - '"' => try w.writeAll("\\\""), + '\t' => { + try w.writeAll("\\t"); + n += 2; + }, + '\n' => { + try w.writeAll("\\n"); + n += 2; + }, + '\r' => { + try w.writeAll("\\r"); + n += 2; + }, + '\\' => { + try w.writeAll("\\\\"); + n += 2; + }, + '"' => { + try w.writeAll("\\\""); + n += 2; + }, 0...8, 11, 12, 14...0x1f, 0x7f => { try w.writeAll("\\x"); try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' }); + n += 4; + }, + else => { + try w.writeByte(byte); + n += 1; }, - else => try w.writeByte(byte), }; + return n; } +pub const StringEscapeWriter = struct { + out: *Writer, + writer: Writer, + + pub fn init(out: *Writer, buffer: []u8) @This() { + return .{ + .out = out, + .writer = .{ + .vtable = &.{ .drain = @This().drain }, + .buffer = buffer, + }, + }; + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const sew: *StringEscapeWriter = @alignCast(@fieldParentPtr("writer", w)); + const out = sew.out; + _ = try stringEscapeCounting(w.buffered(), out); + w.end = 0; + var n: usize = 0; + for (data[0 .. data.len - 1]) |bytes| { + n += try stringEscapeCounting(bytes, out); + } + const pattern = data[data.len - 1]; + for (0..splat) |_| { + n += try stringEscapeCounting(pattern, out); + } + return n; + } +}; + /// Print as escaped contents of a single-quoted string. pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void { switch (codepoint) { -- 2.54.0