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 {}
72747274 </ul>
72757275 {#header_close#}
72767276 {#header_close#}
7277
72777278 {#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>Throughout all zig source code (including in comments), some code points are never allowed:</p>
7279 <p>Zig source code is UTF-8 encoded. Invalid UTF-8 byte sequences are not allowed anywhere.</p>
7280 <p>Some code points are never allowed, even in {#link|Comments#}:</p>
72807281 <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>Non-Ascii Unicode line endings: U+0085 (NEL), U+2028 (LS), U+2029 (PS).</li>
7282 <li>ASCII control characters, except for U+000a (LF): U+0000...U+0009, U+000b...U+0001f, U+007f.</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>
72837285 </ul>
72847286 <p>
7285 LF (byte value 0x0a, code point U+000a, {#syntax#}'\n'{#endsyntax#}) is the line terminator in Zig source code.
7286 This byte value terminates every line of zig source code except the last line of the file.
7287 It is recommended that non-empty source files end with an empty line, which means the last byte would be 0x0a (LF).
7288 </p>
7289 <p>
7290 Each LF may be immediately preceded by a single CR (byte value 0x0d, code point U+000d, {#syntax#}'\r'{#endsyntax#})
7291 to form a Windows style line ending, but this is discouraged. Note that in multiline strings, CRLF sequences will
7292 be encoded as LF when compiled into a zig program.
7293 A CR in any other context is not allowed.
7294 </p>
7295 <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>
7287 LF (byte value 0x0a, code point U+000a, {#syntax#}'\n'{#endsyntax#}) is
7288 the line terminator in Zig source code. This byte value terminates every
7289 line of Zig source code, including last line of the file.
7290 </p>
7291 <p>These conservative rules mean that third party tools reading
7292 already-validated Zig source code may make simplifying assumptions, such
7293 as naively separating lines based on {#syntax#}'\n'{#endsyntax#}.
7294 However, tooling such as <kbd>zig fmt</kbd> provides convenience
7295 functionality to convert invalid source encodings to valid source
7296 encodings, for instance by stripping byte order marks and carriage
7297 returns.</p>
73177298 {#header_close#}
73187299
73197300 {#header_open|Keyword Reference#}
lib/compiler/Maker.zig+26-20
......@@ -292,9 +292,7 @@ pub fn main(init: process.Init.Minimal) !void {
292292 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
293293 configure_argv.appendAssumeCapacity(arg);
294294 } else if (mem.eql(u8, arg, "--color")) {
295 const next_arg = nextArgOrFatal(args, &arg_i);
296 color = stringToEnum(Color, next_arg) orelse
297 fatalWithHint("expected [auto|on|off] found {q}", .{next_arg});
295 color = nextEnumArg(args, &arg_i, Color);
298296
299297 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
300298 configure_argv.appendAssumeCapacity(try arena.print("--color={t}", .{color}));
......@@ -401,25 +399,11 @@ pub fn main(init: process.Init.Minimal) !void {
401399 } else if (mem.eql(u8, arg, "--libc")) {
402400 graph.libc_file = nextArgOrFatal(args, &arg_i);
403401 } else if (mem.eql(u8, arg, "--error-style")) {
404 const next_arg = nextArg(args, &arg_i) orelse
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 };
402 error_style = nextEnumArg(args, &arg_i, ErrorStyle);
409403 } else if (mem.eql(u8, arg, "--multiline-errors")) {
410 const next_arg = nextArg(args, &arg_i) orelse
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 };
404 multiline_errors = nextEnumArg(args, &arg_i, MultilineErrors);
415405 } else if (mem.eql(u8, arg, "--summary")) {
416 const next_arg = nextArg(args, &arg_i) orelse
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 };
406 summary = nextEnumArg(args, &arg_i, Summary);
423407 } else if (mem.cutPrefix(u8, arg, "--seed=")) |rest| {
424408 graph.random_seed = parseRandomSeed(rest);
425409 } else if (mem.eql(u8, arg, "--build-id")) {
......@@ -4053,3 +4037,25 @@ fn confPathDepToCachePath(
40534037 .install_include => @panic("TODO"),
40544038 };
40554039}
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 {
584584/// required, otherwise the digit following ':' is interpreted as **width**.
585585///
586586/// **specifier** supports:
587/// - `x` and `X`: numeric value in hexadecimal notation, or string in hexadecimal bytes
588/// - `s`:
587/// - "x" and "X": numeric value in hexadecimal notation, or string in hexadecimal bytes
588/// - "s":
589589/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination
590590/// - for slices of u8, print the entire slice as a string without zero-termination
591/// - `t`:
591/// - "t":
592592/// - for enums and tagged unions: prints the tag name
593593/// - for error sets: prints the error name
594/// - `b64`: string as standard base64
595/// - `e`: floating point value in scientific notation
596/// - `d`: numeric value in decimal notation
597/// - `b`: integer value in binary notation
598/// - `o`: integer value in octal notation
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.
601/// - `B`: bytes in SI units (decimal)
602/// - `Bi`: bytes in IEC units (binary)
603/// - `?`: optional value as either the unwrapped value, or `null`; may be
594/// - "b64": string as standard base64
595/// - "e": floating point value in scientific notation
596/// - "d": numeric value in decimal notation
597/// - "b": integer value in binary notation
598/// - "o": integer value in octal notation
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.
601/// - "B": bytes in SI units (decimal)
602/// - "Bi": bytes in IEC units (binary)
603/// - "?": optional value as either the unwrapped value, or `null`; may be
604604/// followed by a format specifier for the underlying value.
605/// - `!`: error union value as either the unwrapped value, or the formatted
605/// - "!": error union value as either the unwrapped value, or the formatted
606606/// error value; may be followed by a format specifier for the underlying
607607/// value.
608/// - `*`: the address of the value instead of the value itself.
609/// - `any`: a value of any type using its default format.
610/// - `f`: delegates to the `format` method of the type, passing `*Writer` and
608/// - "*": the address of the value instead of the value itself.
609/// - "any": a value of any type using its default format.
610/// - "f": delegates to the `format` method of the type, passing `*Writer` and
611611/// expecting `Error!void` returned.
612///
613/// A user type may be a struct, vector, union or enum type.
612/// - "q": prints as a double-quote escaped string. Inside the double-quoted
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.
614619///
615620/// Literal curly braces can be escaped in the format string via doubling, e.g.
616/// `{{` or `}}`.
621/// "{{" or "}}".
617622pub fn print(w: *Writer, comptime fmt: []const u8, args: anytype) Error!void {
618623 const ArgsType = @TypeOf(args);
619624 const args_type_info = @typeInfo(ArgsType);
......@@ -1231,6 +1236,18 @@ pub fn printValue(
12311236 },
12321237 else => {},
12331238 },
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 },
12341251 else => {},
12351252 },
12361253 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) {
......@@ -2143,6 +2160,11 @@ test "{q} format string" {
21432160 try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data});
21442161}
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
21462168fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {
21472169 var buffer: [100]u8 = undefined;
21482170 var w: Writer = .fixed(&buffer);
lib/std/zig.zig+69-7
......@@ -538,21 +538,83 @@ test fmtChar {
538538}
539539
540540/// 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.
541547pub 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;
542553 for (bytes) |byte| switch (byte) {
543 '\n' => try w.writeAll("\\n"),
544 '\r' => try w.writeAll("\\r"),
545 '\t' => try w.writeAll("\\t"),
546 '\\' => try w.writeAll("\\\\"),
547 '"' => try w.writeAll("\\\""),
548 ' ', '!', '#'...'[', ']'...'~' => try w.writeByte(byte),
549 else => {
554 '\t' => {
555 try w.writeAll("\\t");
556 n += 2;
557 },
558 '\n' => {
559 try w.writeAll("\\n");
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 => {
550575 try w.writeAll("\\x");
551576 try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });
577 n += 4;
578 },
579 else => {
580 try w.writeByte(byte);
581 n += 1;
552582 },
553583 };
584 return n;
554585}
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
556618/// Print as escaped contents of a single-quoted string.
557619pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void {
558620 switch (codepoint) {
lib/std/zig/parser_generated_oracle.zig+5-5
......@@ -3073,7 +3073,7 @@ const Parser = struct {
30733073 return blk_0: {
30743074 const pos_0 = p.i;
30753075 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..], "")) {
30773077 p.i += 3;
30783078 break :blk_1 true;
30793079 }
......@@ -3145,7 +3145,7 @@ const Parser = struct {
31453145 return blk_0: {
31463146 const pos_0 = p.i;
31473147 if (blk_1: {
3148 if (std.mem.startsWith(u8, p.source[p.i..], "\xf4")) {
3148 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
31493149 p.i += 1;
31503150 break :blk_1 true;
31513151 }
......@@ -3201,7 +3201,7 @@ const Parser = struct {
32013201 return blk_0: {
32023202 const pos_0 = p.i;
32033203 if (blk_1: {
3204 if (std.mem.startsWith(u8, p.source[p.i..], "\xf0")) {
3204 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
32053205 p.i += 1;
32063206 break :blk_1 true;
32073207 }
......@@ -3257,7 +3257,7 @@ const Parser = struct {
32573257 return blk_0: {
32583258 const pos_0 = p.i;
32593259 if (blk_1: {
3260 if (std.mem.startsWith(u8, p.source[p.i..], "\xed")) {
3260 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
32613261 p.i += 1;
32623262 break :blk_1 true;
32633263 }
......@@ -3313,7 +3313,7 @@ const Parser = struct {
33133313 return blk_0: {
33143314 const pos_0 = p.i;
33153315 if (blk_1: {
3316 if (std.mem.startsWith(u8, p.source[p.i..], "\xe0")) {
3316 if (std.mem.startsWith(u8, p.source[p.i..], "�")) {
33173317 p.i += 1;
33183318 break :blk_1 true;
33193319 }
tools/gen_parser_oracle.zig+3-6
......@@ -267,17 +267,14 @@ const Generator = struct {
267267 const bytes = g.p.strings.items[literal.off..][0..literal.len];
268268 try g.w.print(
269269 \\blk_{d}: {{
270 \\if (std.mem.startsWith(u8, p.source[p.i..], "
271 , .{suffix});
272 try std.zig.stringEscape(bytes, g.w);
273 try g.w.print(
274 \\")) {{
270 \\if (std.mem.startsWith(u8, p.source[p.i..], {q})) {{
271 \\
275272 \\p.i += {d};
276273 \\ break :blk_{d} true;
277274 \\}}
278275 \\break :blk_{d} false;
279276 \\}}
280 , .{ bytes.len, suffix, suffix });
277 , .{ suffix, bytes, bytes.len, suffix, suffix });
281278 },
282279 .class => |ranges| {
283280 try g.w.writeAll("(p.i < p.source.len and switch (p.source[p.i]) {");