diff --git a/lib/std/fmt.zig b/lib/std/fmt.zig index a3a97020bf..5b673eec1b 100644 --- a/lib/std/fmt.zig +++ b/lib/std/fmt.zig @@ -65,6 +65,8 @@ fn peekIsAlign(comptime fmt: []const u8) bool { /// - format the non-numeric value as a string of bytes in hexadecimal notation ("binary dump") in either lower case or upper case /// - output numeric value in hexadecimal notation /// - `s`: print a pointer-to-many as a c-string, use zero-termination +/// - `z`: escape the string with @"" syntax if it is not a valid Zig identifier. +/// - `Z`: print the string escaping non-printable characters using Zig escape sequences. /// - `B` and `Bi`: output a memory size in either metric (1000) or power-of-two (1024) based notation. works for both float and integer values. /// - `e` and `E`: if printing a string, escape non-printable characters /// - `e`: output floating point value in scientific notation @@ -543,7 +545,14 @@ pub fn formatIntValue( } else { @compileError("Cannot print integer that is larger than 8 bits as a ascii"); } - } else if (comptime std.mem.eql(u8, fmt, "b")) { + } else if (comptime std.mem.eql(u8, fmt, "Z")) { + if (@typeInfo(@TypeOf(int_value)).Int.bits <= 8) { + const c: u8 = int_value; + return formatZigEscapes(@as(*const [1]u8, &c), options, writer); + } else { + @compileError("Cannot escape character with more than 8 bits"); + } + }else if (comptime std.mem.eql(u8, fmt, "b")) { radix = 2; uppercase = false; } else if (comptime std.mem.eql(u8, fmt, "x")) { @@ -612,6 +621,10 @@ pub fn formatText( } } return; + } else if (comptime std.mem.eql(u8, fmt, "z")) { + return formatZigIdentifier(bytes, options, writer); + } else if (comptime std.mem.eql(u8, fmt, "Z")) { + return formatZigEscapes(bytes, options, writer); } else { @compileError("Unknown format string: '" ++ fmt ++ "'"); } @@ -652,9 +665,62 @@ pub fn formatBuf( } } -// Print a float in scientific notation to the specified precision. Null uses full precision. -// It should be the case that every full precision, printed value can be re-parsed back to the -// same type unambiguously. +/// Print the string as a Zig identifier escaping it with @"" syntax if needed. +pub fn formatZigIdentifier( + bytes: []const u8, + options: FormatOptions, + writer: anytype, +) !void { + if (isValidZigIdentifier(bytes)) { + return writer.writeAll(bytes); + } + try writer.writeAll("@\""); + try formatZigEscapes(bytes, options, writer); + try writer.writeByte('"'); +} + +fn isValidZigIdentifier(bytes: []const u8) bool { + for (bytes) |c, i| { + switch (c) { + '_', 'a'...'z', 'A'...'Z' => {}, + '0'...'9' => if (i == 0) return false, + else => return false, + } + } + return std.zig.Token.getKeyword(bytes) == null; +} + +pub fn formatZigEscapes( + bytes: []const u8, + options: FormatOptions, + writer: anytype, +) !void { + for (bytes) |c| { + const s: []const u8 = switch (c) { + '\"' => "\\\"", + '\'' => "\\'", + '\\' => "\\\\", + '\n' => "\\n", + '\r' => "\\r", + '\t' => "\\t", + // Handle the remaining escapes Zig doesn't support by turning them + // into their respective hex representation + else => if (std.ascii.isCntrl(c)) { + try writer.writeAll("\\x"); + try formatInt(c, 16, false, .{ .width = 2, .fill = '0' }, writer); + continue; + } else { + try writer.writeByte(c); + continue; + }, + }; + try writer.writeAll(s); + } +} + +/// Print a float in scientific notation to the specified precision. Null uses full precision. +/// It should be the case that every full precision, printed value can be re-parsed back to the +/// same type unambiguously. pub fn formatFloatScientific( value: anytype, options: FormatOptions, @@ -746,8 +812,8 @@ pub fn formatFloatScientific( } } -// Print a float of the format x.yyyyy where the number of y is specified by the precision argument. -// By default floats are printed at full precision (no rounding). +/// Print a float of the format x.yyyyy where the number of y is specified by the precision argument. +/// By default floats are printed at full precision (no rounding). pub fn formatFloatDecimal( value: anytype, options: FormatOptions, @@ -1136,7 +1202,7 @@ pub fn bufPrintZ(buf: []u8, comptime fmt: []const u8, args: anytype) BufPrintErr return result[0 .. result.len - 1 :0]; } -// Count the characters needed for format. Useful for preallocating memory +/// Count the characters needed for format. Useful for preallocating memory pub fn count(comptime fmt: []const u8, args: anytype) u64 { var counting_writer = std.io.countingWriter(std.io.null_writer); format(counting_writer.writer(), fmt, args) catch |err| switch (err) {}; @@ -1334,6 +1400,14 @@ test "escape non-printable" { try testFmt("ab\\xFFc", "{E}", .{"ab\xffc"}); } +test "escape invalid identifiers" { + try testFmt("@\"while\"", "{z}", .{"while"}); + try testFmt("hello", "{z}", .{"hello"}); + try testFmt("@\"11\\\"23\"", "{z}", .{"11\"23"}); + try testFmt("@\"11\\x0f23\"", "{z}", .{"11\x0F23"}); + try testFmt("\\x0f", "{Z}", .{0x0f}); +} + test "pointer" { { const value = @intToPtr(*align(1) i32, 0xdeadbeef);