How to replace placeholders in a template
Short answer: str::replace swaps every occurrence of one substring for another, so a chain of calls fills a template with a few slots: template.replace("{name}", &name).replace("{count}", &count). That is the whole answer when the template and the values are yours. A real templating crate pays once the template needs conditionals or loops, or arrives from somewhere you don’t control.
From a fresh project — cargo new template-demo — replace src/main.rs with each block below and run it with cargo run. The project name only shows up in cargo’s own output; nothing else on this page depends on it.
The chain of replace calls
Section titled “The chain of replace calls”One call per slot, chained. replace takes a plain &str pattern — no regex, no escaping rules — and hands back a String.
fn main() { let template = "Hello, {name}! You have {count} new messages."; let filled = template .replace("{name}", "Ada") .replace("{count}", "3"); println!("{filled}");
let twice = "{x} and {x}"; println!("{}", twice.replace("{x}", "value"));}Hello, Ada! You have 3 new messages.value and valueEvery occurrence goes, not only the first; replacen takes a count when you want fewer. The regex article ends by handing this job over: a pattern finds variable text, replace substitutes fixed slots.
The trap: a value that looks like a slot
Section titled “The trap: a value that looks like a slot”Each call works on the string as it stands at that moment, and the chain cannot tell which braces came from the template and which came from a value:
fn main() { let filled = "{a} then {b}".replace("{a}", "{b}").replace("{b}", "done"); println!("{filled}");}done then doneThe value substituted for {a} is itself a {b} slot, so the second call fills it and the first slot ends up as done as well. With {key}-style slots the closing brace keeps one key from swallowing the front of a longer one, so keys sharing a prefix are safe. The trap needs a value containing braces — a config file, a form field, anything you did not type. When values come from such a place, fill in one pass instead.
One pass, with str::find
Section titled “One pass, with str::find”Walk the template once: copy the text up to each {, substitute at the slot, carry on after the }. Nothing that goes into the output is scanned again, so a value containing braces survives untouched.
fn fill(template: &str, values: &[(&str, &str)]) -> String { let mut out = String::with_capacity(template.len()); let mut rest = template; while let Some(open) = rest.find('{') { out.push_str(&rest[..open]); let after_open = &rest[open + 1..]; let Some(close) = after_open.find('}') else { // No closing brace: the rest of the template is literal text. out.push_str(&rest[open..]); return out; }; let key = &after_open[..close]; match values.iter().find(|(k, _)| *k == key) { Some((_, value)) => out.push_str(value), None => { // Unknown key: leave the slot as written. out.push('{'); out.push_str(key); out.push('}'); } } rest = &after_open[close + 1..]; } out.push_str(rest); out}
fn main() { let template = "Hello, {name}! You have {count} new messages."; println!("{}", fill(template, &[("name", "Ada"), ("count", "3")])); println!("{}", fill("{a} then {b}", &[("a", "{b}"), ("b", "done")])); println!("{}", fill("unknown {key} stays", &[("name", "Ada")])); println!("{}", fill("unclosed {slot", &[("name", "Ada")]));}Hello, Ada! You have 3 new messages.{b} then doneunknown {key} staysunclosed {slotLine two is the point: the {b} that came out of the values stays a {b}. An unknown key is left as written and a { with no closing } is copied through — choices, not requirements; a template from a config file argues for the stricter version that returns a Result and names the key it could not resolve. With a dozen keys, a HashMap<&str, &str> replaces the slice scan.
When a real templating crate pays
Section titled “When a real templating crate pays”replace runs out when the template grows a feature it cannot express. Conditionals and loops are the usual trigger — {% if %}, {% for %} — and no chain of calls grows those. Error handling is next: fill above lets an unknown key through on purpose.
Tera and Handlebars are the two you’ll meet most — Tera’s syntax is the Jinja2/Django one, Handlebars’ is its own. Both parse the whole template up front and refuse a broken one, and Handlebars escapes every value it substitutes by default, so HTML output is the other reason to hand the job over. Both are ordinary dependencies; nothing about this page changes except how much template language you get.
If the template is a literal in your own source, none of this applies: format!("Hello, {name}!") checks the slots at compile time, which is what the formatting article covers.
Related
Section titled “Related”- How to match and replace text with regex — when the text to find isn’t a literal
- How to format strings with println! and format! — when the text is yours to build
- Working with text — the rest of the chapter