Skip to content

Formatting

Zig uses format strings to turn values into text.

Zig uses format strings to turn values into text.

The simplest form is std.debug.print.

const std = @import("std");

pub fn main() void {
    std.debug.print("{s} {d}\n", .{ "zig", 16 });
}

The output is:

zig 16

The first argument is the format string.

"{s} {d}\n"

The second argument is a tuple of values.

.{ "zig", 16 }

Each placeholder in the format string consumes one value.

{s} prints string data.

{d} prints a decimal integer.

The format string is checked at compile time. If the string asks for a value in the wrong form, the compiler reports an error.

const std = @import("std");

pub fn main() void {
    std.debug.print("{d}\n", .{"hello"});
}

This is wrong. {d} expects a number.

Some common format specifiers are:

SpecifierMeaning
{}Default formatting
{any}Debug formatting
{s}String
{d}Decimal integer
{x}Lowercase hexadecimal
{X}Uppercase hexadecimal
{b}Binary
{c}Character

Example:

const std = @import("std");

pub fn main() void {
    const x: u8 = 255;

    std.debug.print("{}\n", .{x});
    std.debug.print("{d}\n", .{x});
    std.debug.print("{x}\n", .{x});
    std.debug.print("{X}\n", .{x});
    std.debug.print("{b}\n", .{x});
}

The output is:

255
255
ff
FF
11111111

{any} is useful for inspecting values.

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,
};

pub fn main() void {
    const p = Point{ .x = 10, .y = 20 };

    std.debug.print("{any}\n", .{p});
}

The output is similar to:

main.Point{ .x = 10, .y = 20 }

Formatting can also write to a buffer.

const std = @import("std");

pub fn main() !void {
    var buffer: [64]u8 = undefined;

    const text = try std.fmt.bufPrint(
        buffer[0..],
        "{s}-{d}",
        .{ "zig", 16 },
    );

    std.debug.print("{s}\n", .{text});
}

The output is:

zig-16

bufPrint writes into the supplied buffer.

It returns a slice containing the written bytes.

[]u8

If the buffer is too small, it returns an error.

Formatting can also allocate a new string.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const text = try std.fmt.allocPrint(
        allocator,
        "{s}-{d}",
        .{ "zig", 16 },
    );
    defer allocator.free(text);

    std.debug.print("{s}\n", .{text});
}

allocPrint allocates enough memory for the formatted result.

The caller owns the returned slice and must free it.

defer allocator.free(text);

Formatting is therefore explicit in the same way allocation is explicit.

For fixed memory, use bufPrint.

For allocated memory, use allocPrint.

For diagnostic output, use std.debug.print.

Formatting also supports width and padding.

const std = @import("std");

pub fn main() void {
    std.debug.print("{d: >5}\n", .{42});
    std.debug.print("{d:0>5}\n", .{42});
}

The output is:

   42
00042

The first line pads with spaces.

The second line pads with zeroes.

The width is five characters.

A useful rule is this: formatting does not hide ownership.

Printing writes somewhere.

Buffer formatting uses caller-provided memory.

Allocated formatting requires an allocator and returns memory the caller must free.

Exercise 14-17. Print the number 255 in decimal, hexadecimal, and binary.

Exercise 14-18. Format a string into a fixed buffer with std.fmt.bufPrint.

Exercise 14-19. Format a string with std.fmt.allocPrint and free it.

Exercise 14-20. Print several numbers in aligned columns.