Format Strings in Rust

Alex Woods

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.

The Display trait is for user-facing output, and the Debugtrait 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
If you would like line breaks and indentation, you can pass the # 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.

table-of-contents
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 string
  • write! - write the formatted string to the destination stream
  • writeln! - same as write!, but with a new line
  • print! - write the formatted string to standard output
  • println! - same as print! but with a new line
  • eprint! - write the formatted string to standard error
  • eprintln! - same as eprint!, but with a new line
  • format_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.

simple-log
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!

Want to know when I write a new article?

Get new posts in your inbox