authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-21 11:01:51+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-21 11:01:51+02:00
log3c46da14db296beec186c3df4fe3669f6c99b715
tree8c34fd2046c87da2592b39454e833e0160900af0
parentbf4e2ab8d16e74374c4769f90e1f4042c0f968a1
parent9296ec14e9efe901b397cf8560f75cacf867f170

Merge pull request 'std.Io.Writer: string formatting enhancements' (#36596) from string-formatting into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36596

6 files changed, 162 insertions(+), 94 deletions(-)

doc/langref.html.in+17-36
...@@ -7274,46 +7274,27 @@ fn readU32Be() u32 {}...@@ -7274,46 +7274,27 @@ fn readU32Be() u32 {}
7274 </ul>7274 </ul>
7275 {#header_close#}7275 {#header_close#}
7276 {#header_close#}7276 {#header_close#}
7277
7277 {#header_open|Source Encoding#}7278 {#header_open|Source Encoding#}
7278 <p>Zig source code is encoded in UTF-8. An invalid UTF-8 byte sequence results in a compile error.</p>7279 <p>Zig source code is UTF-8 encoded. Invalid UTF-8 byte sequences are not allowed anywhere.</p>
7279 <p>Throughout all zig source code (including in comments), some code points are never allowed:</p>7280 <p>Some code points are never allowed, even in {#link|Comments#}:</p>
7280 <ul>7281 <ul>
7281 <li>Ascii control characters, except for U+000a (LF), U+000d (CR), and U+0009 (HT): U+0000 - U+0008, U+000b - U+000c, U+000e - U+0001f, U+007f.</li>7282 <li>ASCII control characters, except for U+000a (LF): U+0000...U+0009, U+000b...U+0001f, U+007f.</li>
7282 <li>Non-Ascii Unicode line endings: U+0085 (NEL), U+2028 (LS), U+2029 (PS).</li>7283 <li>Non-ASCII Unicode line endings: U+0085 (NEL), U+2028 (LS), U+2029 (PS).</li>
7284 <li>Byte order marks: U+FEFF (BOM).</li>
7283 </ul>7285 </ul>
7284 <p>7286 <p>
7285 LF (byte value 0x0a, code point U+000a, {#syntax#}'\n'{#endsyntax#}) is the line terminator in Zig source code.7287 LF (byte value 0x0a, code point U+000a, {#syntax#}'\n'{#endsyntax#}) is
7286 This byte value terminates every line of zig source code except the last line of the file.7288 the line terminator in Zig source code. This byte value terminates every
7287 It is recommended that non-empty source files end with an empty line, which means the last byte would be 0x0a (LF).7289 line of Zig source code, including last line of the file.
7288 </p>7290 </p>
7289 <p>7291 <p>These conservative rules mean that third party tools reading
7290 Each LF may be immediately preceded by a single CR (byte value 0x0d, code point U+000d, {#syntax#}'\r'{#endsyntax#})7292 already-validated Zig source code may make simplifying assumptions, such
7291 to form a Windows style line ending, but this is discouraged. Note that in multiline strings, CRLF sequences will7293 as naively separating lines based on {#syntax#}'\n'{#endsyntax#}.
7292 be encoded as LF when compiled into a zig program.7294 However, tooling such as <kbd>zig fmt</kbd> provides convenience
7293 A CR in any other context is not allowed.7295 functionality to convert invalid source encodings to valid source
7294 </p>7296 encodings, for instance by stripping byte order marks and carriage
7295 <p>7297 returns.</p>
7296 HT hard tabs (byte value 0x09, code point U+0009, {#syntax#}'\t'{#endsyntax#}) are interchangeable with
7297 SP spaces (byte value 0x20, code point U+0020, {#syntax#}' '{#endsyntax#}) as a token separator,
7298 but use of hard tabs is discouraged. See {#link|Grammar#}.
7299 </p>
7300 <p>
7301 For compatibility with other tools, the compiler ignores a UTF-8-encoded byte order mark (U+FEFF)
7302 if it is the first Unicode code point in the source text. A byte order mark is not allowed anywhere else in the source.
7303 </p>
7304 <p>
7305 Note that running <kbd>zig fmt</kbd> on a source file will implement all recommendations mentioned here.
7306 </p>
7307 <p>
7308 Note that a tool reading Zig source code can make assumptions if the source code is assumed to be correct Zig code.
7309 For example, when identifying the ends of lines, a tool can use a naive search such as <code>/\n/</code>,
7310 or an <a href="https://msdn.microsoft.com/en-us/library/dd409797.aspx">advanced</a>
7311 search such as <code>/\r\n?|[\n\u0085\u2028\u2029]/</code>, and in either case line endings will be correctly identified.
7312 For another example, when identifying the whitespace before the first token on a line,
7313 a tool can either use a naive search such as <code>/[ \t]/</code>,
7314 or an <a href="https://tc39.es/ecma262/#sec-characterclassescape">advanced</a> search such as <code>/\s/</code>,
7315 and in either case whitespace will be correctly identified.
7316 </p>
7317 {#header_close#}7298 {#header_close#}
73187299
7319 {#header_open|Keyword Reference#}7300 {#header_open|Keyword Reference#}
lib/compiler/Maker.zig+26-20
...@@ -292,9 +292,7 @@ pub fn main(init: process.Init.Minimal) !void {...@@ -292,9 +292,7 @@ pub fn main(init: process.Init.Minimal) !void {
292 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));292 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
293 configure_argv.appendAssumeCapacity(arg);293 configure_argv.appendAssumeCapacity(arg);
294 } else if (mem.eql(u8, arg, "--color")) {294 } else if (mem.eql(u8, arg, "--color")) {
295 const next_arg = nextArgOrFatal(args, &arg_i);295 color = nextEnumArg(args, &arg_i, Color);
296 color = stringToEnum(Color, next_arg) orelse
297 fatalWithHint("expected [auto|on|off] found {q}", .{next_arg});
298296
299 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));297 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
300 configure_argv.appendAssumeCapacity(try arena.print("--color={t}", .{color}));298 configure_argv.appendAssumeCapacity(try arena.print("--color={t}", .{color}));
...@@ -401,25 +399,11 @@ pub fn main(init: process.Init.Minimal) !void {...@@ -401,25 +399,11 @@ pub fn main(init: process.Init.Minimal) !void {
401 } else if (mem.eql(u8, arg, "--libc")) {399 } else if (mem.eql(u8, arg, "--libc")) {
402 graph.libc_file = nextArgOrFatal(args, &arg_i);400 graph.libc_file = nextArgOrFatal(args, &arg_i);
403 } else if (mem.eql(u8, arg, "--error-style")) {401 } else if (mem.eql(u8, arg, "--error-style")) {
404 const next_arg = nextArg(args, &arg_i) orelse402 error_style = nextEnumArg(args, &arg_i, ErrorStyle);
405 fatalWithHint("expected style after {q}", .{arg});
406 error_style = stringToEnum(ErrorStyle, next_arg) orelse {
407 fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg });
408 };
409 } else if (mem.eql(u8, arg, "--multiline-errors")) {403 } else if (mem.eql(u8, arg, "--multiline-errors")) {
410 const next_arg = nextArg(args, &arg_i) orelse404 multiline_errors = nextEnumArg(args, &arg_i, MultilineErrors);
411 fatalWithHint("expected style after {q}", .{arg});
412 multiline_errors = stringToEnum(MultilineErrors, next_arg) orelse {
413 fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg });
414 };
415 } else if (mem.eql(u8, arg, "--summary")) {405 } else if (mem.eql(u8, arg, "--summary")) {
416 const next_arg = nextArg(args, &arg_i) orelse406 summary = nextEnumArg(args, &arg_i, Summary);
417 fatalWithHint("expected [all|new|failures|line|none] after {q}", .{arg});
418 summary = stringToEnum(Summary, next_arg) orelse {
419 fatalWithHint("expected [all|new|failures|line|none] after {q}, found {q}", .{
420 arg, next_arg,
421 });
422 };
423 } else if (mem.cutPrefix(u8, arg, "--seed=")) |rest| {407 } else if (mem.cutPrefix(u8, arg, "--seed=")) |rest| {
424 graph.random_seed = parseRandomSeed(rest);408 graph.random_seed = parseRandomSeed(rest);
425 } else if (mem.eql(u8, arg, "--build-id")) {409 } else if (mem.eql(u8, arg, "--build-id")) {
...@@ -4053,3 +4037,25 @@ fn confPathDepToCachePath(...@@ -4053,3 +4037,25 @@ fn confPathDepToCachePath(
4053 .install_include => @panic("TODO"),4037 .install_include => @panic("TODO"),
4054 };4038 };
4055}4039}
4040
4041fn fatalEnumHint(comptime E: type, arg: []const u8, param: ?[]const u8) noreturn {
4042 var buf: [100]u8 = undefined;
4043 var w: Io.Writer = .fixed(&buf);
4044 for (@typeInfo(E).@"enum".field_names) |field_name| {
4045 w.writeAll(field_name) catch unreachable;
4046 w.writeByte('|') catch unreachable;
4047 }
4048 const buffered = w.buffered();
4049 const enum_options_text = buffered[0 .. buffered.len - 1];
4050 if (param) |p| {
4051 fatalWithHint("expected [{s}] after {q}; found {q}", .{ enum_options_text, arg, p });
4052 } else {
4053 fatalWithHint("expected [{s}] after {q}", .{ enum_options_text, arg });
4054 }
4055}
4056
4057fn nextEnumArg(args: []const []const u8, i: *usize, comptime E: type) E {
4058 const arg = args[i.* - 1];
4059 const next_arg = nextArg(args, i) orelse fatalEnumHint(E, arg, null);
4060 return stringToEnum(E, next_arg) orelse fatalEnumHint(E, arg, next_arg);
4061}
lib/std/Io/Writer.zig+42-20
...@@ -584,36 +584,41 @@ pub fn writeAll(w: *Writer, bytes: []const u8) Error!void {...@@ -584,36 +584,41 @@ pub fn writeAll(w: *Writer, bytes: []const u8) Error!void {
584/// required, otherwise the digit following ':' is interpreted as **width**.584/// required, otherwise the digit following ':' is interpreted as **width**.
585///585///
586/// **specifier** supports:586/// **specifier** supports:
587/// - `x` and `X`: numeric value in hexadecimal notation, or string in hexadecimal bytes587/// - "x" and "X": numeric value in hexadecimal notation, or string in hexadecimal bytes
588/// - `s`:588/// - "s":
589/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination589/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination
590/// - for slices of u8, print the entire slice as a string without zero-termination590/// - for slices of u8, print the entire slice as a string without zero-termination
591/// - `t`:591/// - "t":
592/// - for enums and tagged unions: prints the tag name592/// - for enums and tagged unions: prints the tag name
593/// - for error sets: prints the error name593/// - for error sets: prints the error name
594/// - `b64`: string as standard base64594/// - "b64": string as standard base64
595/// - `e`: floating point value in scientific notation595/// - "e": floating point value in scientific notation
596/// - `d`: numeric value in decimal notation596/// - "d": numeric value in decimal notation
597/// - `b`: integer value in binary notation597/// - "b": integer value in binary notation
598/// - `o`: integer value in octal notation598/// - "o": integer value in octal notation
599/// - `c`: integer as an ASCII character. Integer type must have 8 bits at max.599/// - "c": integer as an ASCII character. Integer type must have 8 bits at max.
600/// - `u`: integer as an UTF-8 sequence. Integer type must have 21 bits at max.600/// - "u": integer as an UTF-8 sequence. Integer type must have 21 bits at max.
601/// - `B`: bytes in SI units (decimal)601/// - "B": bytes in SI units (decimal)
602/// - `Bi`: bytes in IEC units (binary)602/// - "Bi": bytes in IEC units (binary)
603/// - `?`: optional value as either the unwrapped value, or `null`; may be603/// - "?": optional value as either the unwrapped value, or `null`; may be
604/// followed by a format specifier for the underlying value.604/// followed by a format specifier for the underlying value.
605/// - `!`: error union value as either the unwrapped value, or the formatted605/// - "!": error union value as either the unwrapped value, or the formatted
606/// error value; may be followed by a format specifier for the underlying606/// error value; may be followed by a format specifier for the underlying
607/// value.607/// value.
608/// - `*`: the address of the value instead of the value itself.608/// - "*": the address of the value instead of the value itself.
609/// - `any`: a value of any type using its default format.609/// - "any": a value of any type using its default format.
610/// - `f`: delegates to the `format` method of the type, passing `*Writer` and610/// - "f": delegates to the `format` method of the type, passing `*Writer` and
611/// expecting `Error!void` returned.611/// expecting `Error!void` returned.
612///612/// - "q": prints as a double-quote escaped string. Inside the double-quoted
613/// A user type may be a struct, vector, union or enum type.613/// string, everything is passed through unmodified, except for the following
614/// transformations:
615/// - escaped: '\n', '\r', '\t', '\\', '"'
616/// - hex-encoded: ASCII control characters
617/// - "qf": delegates to the `format` method of the type, while double-quote
618/// escaping.
614///619///
615/// Literal curly braces can be escaped in the format string via doubling, e.g.620/// Literal curly braces can be escaped in the format string via doubling, e.g.
616/// `{{` or `}}`.621/// "{{" or "}}".
617pub fn print(w: *Writer, comptime fmt: []const u8, args: anytype) Error!void {622pub fn print(w: *Writer, comptime fmt: []const u8, args: anytype) Error!void {
618 const ArgsType = @TypeOf(args);623 const ArgsType = @TypeOf(args);
619 const args_type_info = @typeInfo(ArgsType);624 const args_type_info = @typeInfo(ArgsType);
...@@ -1231,6 +1236,18 @@ pub fn printValue(...@@ -1231,6 +1236,18 @@ pub fn printValue(
1231 },1236 },
1232 else => {},1237 else => {},
1233 },1238 },
1239 'q' => switch (fmt[1]) {
1240 'f' => {
1241 try w.writeByte('"');
1242 var buffer: [64]u8 = undefined;
1243 var escaping_writer: std.zig.StringEscapeWriter = .init(w, &buffer);
1244 try value.format(&escaping_writer.writer);
1245 try escaping_writer.writer.flush();
1246 try w.writeByte('"');
1247 return;
1248 },
1249 else => {},
1250 },
1234 else => {},1251 else => {},
1235 },1252 },
1236 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) {1253 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) {
...@@ -2143,6 +2160,11 @@ test "{q} format string" {...@@ -2143,6 +2160,11 @@ test "{q} format string" {
2143 try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data});2160 try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data});
2144}2161}
21452162
2163test "{qf} format string" {
2164 const data: []const u8 = "😎";
2165 try testing.expectFmt("hello \"@\\\"😎\\\"\" world", "hello {qf} world", .{std.zig.fmtId(data)});
2166}
2167
2146fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {2168fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {
2147 var buffer: [100]u8 = undefined;2169 var buffer: [100]u8 = undefined;
2148 var w: Writer = .fixed(&buffer);2170 var w: Writer = .fixed(&buffer);
lib/std/zig.zig+69-7
...@@ -538,21 +538,83 @@ test fmtChar {...@@ -538,21 +538,83 @@ test fmtChar {
538}538}
539539
540/// Print the string as escaped contents of a double quoted string.540/// Print the string as escaped contents of a double quoted string.
541///
542/// The following transformations are made:
543/// * escaped: '\n', '\r', '\t', '\\', '"'
544/// * hex-encoded: ascii control characters
545///
546/// Everything else is passed through unmodified.
541pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void {547pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void {
548 _ = try stringEscapeCounting(bytes, w);
549}
550
551pub fn stringEscapeCounting(bytes: []const u8, w: *Writer) Writer.Error!usize {
552 var n: usize = 0;
542 for (bytes) |byte| switch (byte) {553 for (bytes) |byte| switch (byte) {
543 '\n' => try w.writeAll("\\n"),554 '\t' => {
544 '\r' => try w.writeAll("\\r"),555 try w.writeAll("\\t");
545 '\t' => try w.writeAll("\\t"),556 n += 2;
546 '\\' => try w.writeAll("\\\\"),557 },
547 '"' => try w.writeAll("\\\""),558 '\n' => {
548 ' ', '!', '#'...'[', ']'...'~' => try w.writeByte(byte),559 try w.writeAll("\\n");
549 else => {560 n += 2;
561 },
562 '\r' => {
563 try w.writeAll("\\r");
564 n += 2;
565 },
566 '\\' => {
567 try w.writeAll("\\\\");
568 n += 2;
569 },
570 '"' => {
571 try w.writeAll("\\\"");
572 n += 2;
573 },
574 0...8, 11, 12, 14...0x1f, 0x7f => {
550 try w.writeAll("\\x");575 try w.writeAll("\\x");
551 try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });576 try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });
577 n += 4;
578 },
579 else => {
580 try w.writeByte(byte);
581 n += 1;
552 },582 },
553 };583 };
584 return n;
554}585}
555586
587pub const StringEscapeWriter = struct {
588 out: *Writer,
589 writer: Writer,
590
591 pub fn init(out: *Writer, buffer: []u8) @This() {
592 return .{
593 .out = out,
594 .writer = .{
595 .vtable = &.{ .drain = @This().drain },
596 .buffer = buffer,
597 },
598 };
599 }
600
601 fn drain(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
602 const sew: *StringEscapeWriter = @alignCast(@fieldParentPtr("writer", w));
603 const out = sew.out;
604 _ = try stringEscapeCounting(w.buffered(), out);
605 w.end = 0;
606 var n: usize = 0;
607 for (data[0 .. data.len - 1]) |bytes| {
608 n += try stringEscapeCounting(bytes, out);
609 }
610 const pattern = data[data.len - 1];
611 for (0..splat) |_| {
612 n += try stringEscapeCounting(pattern, out);
613 }
614 return n;
615 }
616};
617
556/// Print as escaped contents of a single-quoted string.618/// Print as escaped contents of a single-quoted string.
557pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void {619pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void {
558 switch (codepoint) {620 switch (codepoint) {
lib/std/zig/parser_generated_oracle.zig+5-5
...@@ -3073,7 +3073,7 @@ const Parser = struct {...@@ -3073,7 +3073,7 @@ const Parser = struct {
3073 return blk_0: {3073 return blk_0: {
3074 const pos_0 = p.i;3074 const pos_0 = p.i;
3075 if (blk_1: {3075 if (blk_1: {
3076 if (std.mem.startsWith(u8, p.source[p.i..], "\xef\xbb\xbf")) {3076 if (std.mem.startsWith(u8, p.source[p.i..], "")) {
3077 p.i += 3;3077 p.i += 3;
3078 break :blk_1 true;3078 break :blk_1 true;
3079 }3079 }
...@@ -3145,7 +3145,7 @@ const Parser = struct {...@@ -3145,7 +3145,7 @@ const Parser = struct {
3145 return blk_0: {3145 return blk_0: {
3146 const pos_0 = p.i;3146 const pos_0 = p.i;
3147 if (blk_1: {3147 if (blk_1: {
3148 if (std.mem.startsWith(u8, p.source[p.i..], "\xf4")) {3148 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
3149 p.i += 1;3149 p.i += 1;
3150 break :blk_1 true;3150 break :blk_1 true;
3151 }3151 }
...@@ -3201,7 +3201,7 @@ const Parser = struct {...@@ -3201,7 +3201,7 @@ const Parser = struct {
3201 return blk_0: {3201 return blk_0: {
3202 const pos_0 = p.i;3202 const pos_0 = p.i;
3203 if (blk_1: {3203 if (blk_1: {
3204 if (std.mem.startsWith(u8, p.source[p.i..], "\xf0")) {3204 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
3205 p.i += 1;3205 p.i += 1;
3206 break :blk_1 true;3206 break :blk_1 true;
3207 }3207 }
...@@ -3257,7 +3257,7 @@ const Parser = struct {...@@ -3257,7 +3257,7 @@ const Parser = struct {
3257 return blk_0: {3257 return blk_0: {
3258 const pos_0 = p.i;3258 const pos_0 = p.i;
3259 if (blk_1: {3259 if (blk_1: {
3260 if (std.mem.startsWith(u8, p.source[p.i..], "\xed")) {3260 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
3261 p.i += 1;3261 p.i += 1;
3262 break :blk_1 true;3262 break :blk_1 true;
3263 }3263 }
...@@ -3313,7 +3313,7 @@ const Parser = struct {...@@ -3313,7 +3313,7 @@ const Parser = struct {
3313 return blk_0: {3313 return blk_0: {
3314 const pos_0 = p.i;3314 const pos_0 = p.i;
3315 if (blk_1: {3315 if (blk_1: {
3316 if (std.mem.startsWith(u8, p.source[p.i..], "\xe0")) {3316 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
3317 p.i += 1;3317 p.i += 1;
3318 break :blk_1 true;3318 break :blk_1 true;
3319 }3319 }
tools/gen_parser_oracle.zig+3-6
...@@ -267,17 +267,14 @@ const Generator = struct {...@@ -267,17 +267,14 @@ const Generator = struct {
267 const bytes = g.p.strings.items[literal.off..][0..literal.len];267 const bytes = g.p.strings.items[literal.off..][0..literal.len];
268 try g.w.print(268 try g.w.print(
269 \\blk_{d}: {{269 \\blk_{d}: {{
270 \\if (std.mem.startsWith(u8, p.source[p.i..], "270 \\if (std.mem.startsWith(u8, p.source[p.i..], {q})) {{
271 , .{suffix});271 \\
272 try std.zig.stringEscape(bytes, g.w);
273 try g.w.print(
274 \\")) {{
275 \\p.i += {d};272 \\p.i += {d};
276 \\ break :blk_{d} true;273 \\ break :blk_{d} true;
277 \\}}274 \\}}
278 \\break :blk_{d} false;275 \\break :blk_{d} false;
279 \\}}276 \\}}
280 , .{ bytes.len, suffix, suffix });277 , .{ suffix, bytes, bytes.len, suffix, suffix });
281 },278 },
282 .class => |ranges| {279 .class => |ranges| {
283 try g.w.writeAll("(p.i < p.source.len and switch (p.source[p.i]) {");280 try g.w.writeAll("(p.i < p.source.len and switch (p.source[p.i]) {");