3.2 · Formatting Mastery: fmt, write!, format_args!
Domain 3 — Strings and Text Processing Duration: ~15 minutes Library components:
std::fmt,std::fmt::NumBuffer, all formatting traits,std::fmt::Formatter,std::fmt::Arguments,write!,writeln!,format!,format_args!
Introduction
The Rust formatting system is more than println!. The std::fmt module defines a family of traits, and a format specifier selects each one:
DisplayandDebugBinary,Octal,LowerHex, andUpperHexLowerExpandUpperExpPointer
The Formatter struct gives access to all the options that the caller supplied (width, alignment, precision, fill, sign). When you know these traits and options, you can write custom types that behave exactly like built-in types.
This tutorial explains:
- How to implement
DisplayandDebugfor custom types. - How to use the
Formatteroptions: width, alignment, fill, precision, sign, zero-padding. - The numeric format traits and the pointer format trait.
format_args!for formatting without allocation.{integer}::format_intoandstd::fmt::NumBufferfor decimal integers without allocation.write!andwriteln!forfmt::Writetargets and forio::Writetargets.- The debug builder helpers (
DebugStruct,DebugTuple,DebugList,DebugMap,DebugSet).
Display and Debug
Display is for output to the end user. Debug is for developers. A Debug implementation should always give a representation that is similar to syntactically valid Rust.
use std::fmt;
struct Color { r: u8, g: u8, b: u8 }
// Display: the text for the end user, a CSS-style hex color such as #FF0000.
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// {:02X} writes one u8 as 2 uppercase hex digits, with a leading zero if necessary.
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
}
}
// Debug: the text for developers, with the type name and the name of each field.
impl fmt::Debug for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Color") // starts the output with the type name
.field("r", &self.r) // adds `r: <Debug text of self.r>`
.field("g", &self.g)
.field("b", &self.b)
.finish() // completes the output and returns fmt::Result
}
}
{} calls Display. {:?} calls Debug. {:#?} enables pretty-printing, and the debug builder helpers do the pretty-printing automatically.
An implementation of Display also gives the type a to_string() method at no cost. The blanket impl<T: fmt::Display> ToString for T supplies the method.
03_05_display_debug_traits.rs also formats a Point type and a Meters type. It prints:
Display: #FF0000
Display: #008080
Debug: Color { r: 255, g: 0, b: 0 }
Debug pretty: Color {
r: 0,
g: 128,
b: 128,
}
Point Display: (3, -1.5)
Point Debug: Point { x: 3.0, y: -1.5 }
Meters Display: 12.35m
Meters Debug: Meters(12.345678)
formatted: The color is #FF0000 and the point is (3, -1.5)
All assertions passed.
Formatter Options
The format string {:fill<align><sign><#><0><width>.<precision>type} maps to fields on the Formatter struct.
Figure: Format Specifier Syntax
| Specifier | Meaning | Example |
|---|---|---|
< > ^ | Left, right, or center alignment | {:<10} |
fill | Fill char, written before the alignment character | {:*>10} |
+ | Always print the sign | {:+} |
0 | Zero-padding | {:08} |
width | Minimum field width | {:10} |
.prec | Decimal precision, or string truncation | {:.2} / {:.4} |
width$ | Width from an argument | {:>width$} |
// Alignment in a field that is 10 characters wide
format!("{:<10}", "left") // "left "
format!("{:>10}", "right") // " right"
format!("{:^10}", "center") // " center "
// Fill character: it goes before the alignment character
format!("{:*<10}", "fill") // "fill******"
format!("{:0>6}", 42) // "000042"
// Precision
format!("{:.2}", 3.14159) // "3.14": 2 digits after the decimal point
format!("{:.4}", "hello") // "hell": for a string, the precision is the maximum length
// Sign
format!("{:+}", 42_i32) // "+42": a positive number also gets its sign
// Combination: fill `*`, right alignment, sign, width 10
format!("{:*>+10}", 42_i32) // "*******+42"
// Width at run time: `width$` reads the width from the argument with the name `width`
let w = 12_usize;
format!("{:>width$}", "dynamic", width = w) // " dynamic"
To obey the options of the caller in a custom Display, call f.pad(&s) with your formatted string. pad applies the alignment and the fill automatically.
03_06_formatter_options.rs prints each formatted value on a line of its own. The last three lines come from a Padded(i64) type that calls f.pad:
left # (6 spaces follow the text)
right
center # (2 spaces follow the text)
fill******
000042
----hi-----
pi = 3.14
pi = 3.14159
pi = 3
truncated: truncate
truncated: trun
{:+} of 42 = +42
{:+} of -42 = -42
00000042
00002.50
*******+42
dynamic
Padded left: [42 ]
Padded right: [ 42]
Padded center: [ 42 ]
All assertions passed.
Numeric and Pointer Format Traits
The fmt module defines more traits than Display and Debug. A type character in the format string selects each one:
| Trait | Specifier | Example |
|---|---|---|
Binary | {:b} | "10101100" |
Octal | {:o} | "755" |
LowerHex | {:x} | "deadbeef" |
UpperHex | {:X} | "DEADBEEF" |
LowerExp | {:e} | "1.235e6" |
UpperExp | {:E} | "1.235E6" |
Pointer | {:p} | "0x7ff..." |
The # flag adds the usual prefix (0b, 0o, 0x).
let n: u32 = 0b1010_1100; // 172 in decimal
format!("{:b}", n) // "10101100"
format!("{:#b}", n) // "0b10101100": `#` adds the 0b prefix
format!("{:#X}", 0xDEAD_BEEFu32) // "0xDEADBEEF": the prefix stays lowercase
format!("{:.3e}", 1_234_567.89_f64) // "1.235e6": 3 digits after the decimal point
let x = 42u32;
format!("{:p}", &x) // the address of x, for example "0x16ee01754" (varies)
You can implement each of these traits on your own types. A common pattern is to implement LowerHex and UpperHex on a newtype for a byte buffer. Then the type formats as a hex string.
03_07_numeric_format_traits.rs prints the lines below. The Rgb lines come from a newtype with custom LowerHex and UpperHex implementations:
Binary: 10101100
Binary 0b: 0b10101100
Binary 8w: 10101100
Octal: 755
Octal 0o: 0o755
LowerHex: deadbeef
UpperHex: DEADBEEF
LowerHex 0x: 0xdeadbeef
UpperHex 0X: 0xDEADBEEF
LowerHex 10: 00deadbeef
Rgb lower: ff7f00
Rgb upper: FF7F00
LowerExp: 1.23456789e6
UpperExp: 1.23456789E6
Prec 3: 1.235e6
Pointer: 0x16ee01754 # (varies)
All assertions passed.
format_args! and write!
format! always allocates a String. format_args! captures the format string and its arguments as a fmt::Arguments<'_> value and does not allocate. You can pass the value to any type that implements fmt::Write or io::Write.
// The two traits have the same name. The aliases let you import them together.
use std::fmt::Write as FmtWrite;
use std::io::Write as IoWrite;
// Write to a String (String implements fmt::Write)
let mut buf = String::new();
write!(buf, "Hello, world!").unwrap(); // buf is "Hello, world!"
writeln!(buf, " Next line.").unwrap(); // appends the text and a '\n'
// Write to a Vec<u8> (Vec<u8> implements io::Write)
let mut bytes: Vec<u8> = Vec::new();
write!(bytes, "bytes: {}", 255_u8).unwrap(); // bytes is b"bytes: 255"
// A logger with no intermediate String: `args` goes directly to the sink
fn log_to(sink: &mut dyn FmtWrite, args: std::fmt::Arguments<'_>) {
sink.write_fmt(args).unwrap();
}
let mut output = String::new();
log_to(&mut output, format_args!("event={} level={}", "login", "INFO"));
// output is "event=login level=INFO"
fmt::Write and io::Write are different traits. write! works with the two traits. The macro calls write_fmt from the trait that is in scope. To prevent ambiguity, import only the trait that you need, or import the two traits with aliases.
03_08_format_args.rs prints:
value = 42
format_args passthrough: x = 1, y = 2
write! to String: "Hello, world! Next line.\n"
report:
item 0: value=0
item 1: value=10
item 2: value=20
write! to Vec<u8>: [98, 121, 116, 101, 115, 58, 32, 50, 53, 53]
log_to output: "event=login level=INFO"
Named args: Alice scored 99 points
Positional: yes and no and yes
All assertions passed.
format_into (1.98) is the fast path for integers only. It writes the decimal text into a NumBuffer on the stack and returns a &str that borrows from the buffer. It uses no heap and no dyn Write. You can use the same buffer again: each call overwrites the previous text.
Rust 1.98 supplied the buffer type only as core::fmt::NumBuffer. Since Rust 1.99, std::fmt and alloc::fmt re-export it, so the usual std::fmt import path is valid.
use std::fmt::NumBuffer; // 1.99 re-export. On 1.98 the path is core::fmt::NumBuffer.
// The buffer is a stack value. Its size comes from the integer type (u32 here).
let mut buf = NumBuffer::new(); // the compiler infers NumBuffer<u32> from the next line
assert_eq!(1972u32.format_into(&mut buf), "1972"); // the &str borrows from `buf`
// A negative number includes its sign. This call uses a temporary NumBuffer<i32>.
assert_eq!((-1972i32).format_into(&mut NumBuffer::new()), "-1972");
Debug Builder Helpers
Manual formatting of {:?} output is tedious, and the code must also handle the {:#?} pretty-print flag. The builder helpers do this work automatically:
| Helper | Created by | Use for |
|---|---|---|
DebugStruct | f.debug_struct("Name") | Struct with named fields |
DebugTuple | f.debug_tuple("Name") | Tuple struct |
DebugList | f.debug_list() | Sequence of values |
DebugSet | f.debug_set() | Unordered set of values |
DebugMap | f.debug_map() | Key–value pairs |
// QueryNode has the fields table: &'static str, limit: usize, columns: Vec<&'static str>.
impl fmt::Debug for QueryNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("QueryNode") // the type name in the output
.field("table", &self.table) // field name, then a value that implements Debug
.field("limit", &self.limit)
.field("columns", &self.columns) // the Vec uses its own Debug implementation
.finish() // completes the output and returns fmt::Result
}
}
The same Debug implementation gives compact output with {:?} and indented output with {:#?}.
03_09_debug_builders.rs has one type for each helper. It prints:
DebugStruct compact:
QueryNode { table: "users", limit: 10, columns: ["id", "name", "email"] }
DebugStruct pretty:
QueryNode {
table: "users",
limit: 10,
columns: [
"id",
"name",
"email",
],
}
DebugTuple: Pair(3, 7)
DebugList: ["parse", "validate", "execute"]
DebugSet: {"async", "rust", "web"}
DebugMap: {"host": "localhost", "port": "8080"}
All assertions passed.
Summary
| Tool | Purpose |
|---|---|
Display ({}) | String representation for users |
Debug ({:?}) | Representation for developers, which you can derive |
Formatter options | Width, alignment, fill, precision, sign, zero-padding |
Binary/Octal/Hex/Exp | Numeric format traits for {:b}, {:o}, {:x}, {:e} |
Pointer ({:p}) | Raw memory address |
format_args! | Argument capture without allocation |
format_into + NumBuffer (1.98, std::fmt path since 1.99) | Decimal integers into a stack buffer. The &str borrows from the buffer. |
write! / writeln! | Format into any fmt::Write or io::Write target |
| Debug builders | Automatic support for the {:#?} pretty-print flag |
Code Examples
| File | Description |
|---|---|
03_05_display_debug_traits.rs | Custom Display and Debug, to_string(), format! |
03_06_formatter_options.rs | Width, alignment, fill, precision, sign, zero-padding, run-time width |
03_07_numeric_format_traits.rs | Binary, Octal, LowerHex, UpperHex, LowerExp, UpperExp, Pointer |
03_08_format_args.rs | format_args!, write!, writeln!, a logger with no intermediate String |
03_09_debug_builders.rs | DebugStruct, DebugTuple, DebugList, DebugSet, DebugMap |
03_22_format_into.rs | {integer}::format_into and std::fmt::NumBuffer (1.98, std::fmt path since 1.99) |