Format Strings in Rust
Alex Woods
October 31, 2022
Rust has a family of macros, like println!
and format!
, that accept format strings. These are string literals that, at runtime, transform whatever is contained inside {}
.
They have their own grammar, which is worth taking a second to understand. So let's print a bunch of strings.
// no curly braces
println!("Testing 1") // "Testing 1"
// interpolate something from the scope
let blue = "blue".to_string();
println!("The car is {blue}.") // The car is blue.
// interpolate a named argument
println!("The car is {color}.", color = "blue"); // The car is blue.
// single positional argument
println!("The car is {}.", "blue") // The car is blue.
// multiple positional arguments
println!("The car is {} or {}", "blue", "red") // The car is blue or red
// control the order of the positional arguments used
println!("The car is {1} or {0}", "blue", "red") // The car is red or blue
Formatting with a type
In the above examples, we were implicitly using the Display
trait, but some types, like structs and vectors, don't implement Display
.
struct User {
name: String,
}
let user = User {
name: "John D. Rockefeller".to_string(),
};
// error: `User` doesn't implement `std::fmt::Display`
println!("{user}");
We could implement Display
ourselves. In fact, this is a good way to go from an enum to a string in Rust.
struct User {
name: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// I've always liked Kotlin's data class output format
write!(f, "User(name={})", self.name)
}
}
let user = User {
name: "John D. Rockefeller".to_string(),
};
println!("{}", user); // User(name=John D. Rockefeller)
Or, we could use a different trait, called Debug
.
Display
trait is for user-facing output, and the Debug
trait is for programmer-facing output. Debug
is not unlike data classes in other languages.In fact, if all the fields of the struct implement Debug
, we can automatically generate a Debug
implementation using the derive
attribute.
We have to also use the formatter ?
in the format string, to tell it to use the Debug
type instead of Display
.
// Automatically generates an implementation of the Debug trait
+#[derive(Debug)]
struct User {
name: String,
}
let user = User {
name: "John D. Rockefeller".to_string(),
};
// Format `user` with `Debug`, not `Display`
+println!("{user:?}");
-println!("{user}");
// output: User { name: "John D. Rockefeller" }
Just like we switched the type to format with using the ?
formatter, we can do that for other types as well.
let businessman: &str = "John D. Rockefeller";
// Format with `Pointer`
println!("{businessman:p}"); // 0x102eecc84
#
sign to the formatter. e.g.println!("{user:#?}");
.What other cool things can you do with format strings?
Well, precision can be useful. For floating-point integers, it specifies the number of digits after the decimal point.
println!("{PI:.5}"); // 3.14159
For a string, this means truncation.
println!("{:.2}", "hello"); // he
Also, fill / alignment. This allows you to shift an arguments.
fn table_of_contents_line(chapter_name: &str, page: usize, length: usize) {
let width = length - chapter_name.len();
// using the fill character '.'
// right align argument at index 1 in 'width' columns
println!("{0}{1:.>width$}", chapter_name, page)
}
let page_width = 30;
table_of_contents_line("Chapter One", 7, page_width);
table_of_contents_line("Chapter Two", 25, page_width);
table_of_contents_line("Chapter Three", 42, page_width);
table_of_contents_line("Chapter Four", 60, page_width);
// Chapter One..................7
// Chapter Two.................25
// Chapter Three...............42
// Chapter Four................60
What macros take format strings?
Like I said, there's a family of macros which take format strings.
format!
- return the formatted stringwrite!
- write the formatted string to the destination streamwriteln!
- same aswrite!
, but with a new lineprint!
- write the formatted string to standard outputprintln!
- same asprint!
but with a new lineeprint!
- write the formatted string to standard erroreprintln!
- same aseprint!
, but with a new lineformat_args!
- creates an intermediate type that can be passed to functions which accept format strings. This is useful if you're creating your own function that you want to accept format strings. (see the next example)
Creating our own macro that takes a format string
Lastly, we're going to implement our own macro, log!
, which takes a format string. Basically a simplified version of the log crate.
use std::fmt;
#[derive(Debug)]
enum Level {
Debug,
Info,
Warn,
Error,
}
#[macro_export]
macro_rules! log {
($level:expr, $($args:tt)*) => {
println!("({:?}): {}", $level, format_args!($($args)*))
};
}
fn main() {
log!(
Level::Debug,
"Commit with sha {sha:.7} has been deployed to {environment}.",
sha = "28ad4819a5912d139aaa09bc6d2fddcd50b9ed42",
environment = "production"
);
}
// (Debug): Commit with sha 28ad481 has been deployed to production.
I learned a ton writing this, and I hope you learned something reading it. It's possible, probable even, that something is wrong or suboptimal in this article. If you notice anything, please reach out.
Happy printing!