How to trim whitespace and strip characters
Short answer: str::trim removes whitespace from both ends and returns a slice of what is left; trim_start and trim_end do one side each. For characters that are not whitespace, trim_matches takes a char, an array of chars, or a closure. The detail worth knowing: \r is whitespace, so trim() clears the stray carriage return a Windows line ending leaves behind.
From a fresh project — cargo new trim-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.
Trimming whitespace
Section titled “Trimming whitespace”fn main() { let messy = " \t padded text \n "; println!("{:?}", messy.trim()); println!("{:?}", messy.trim_start()); println!("{:?}", messy.trim_end());}"padded text""padded text \n "" \t padded text"All three return &str — a slice of the same string, nothing allocated, the original untouched. Spaces, tabs and newlines come off both ends; everything else stays.
What counts as whitespace
Section titled “What counts as whitespace”trim follows char::is_whitespace, the Unicode White_Space property, so the non-breaking space and the ideographic space go with the obvious ones. The zero-width space does not: it looks like a space and is not one.
fn main() { println!("{:?}", "\u{00A0}text\u{00A0}".trim()); println!("{:?}", "\u{3000}text\u{3000}".trim()); println!("{:?}", "\u{200B}text\u{200B}".trim()); println!("{}", '\u{00A0}'.is_whitespace()); println!("{}", '\u{200B}'.is_whitespace());}"text""text""\u{200b}text\u{200b}"truefalsetrim_matches(char::is_whitespace) is the same thing written out longhand. {:?} is what makes the invisible visible: Debug escapes the bytes that {} prints raw.
Stripping characters you name
Section titled “Stripping characters you name”trim_matches takes a pattern instead of the whitespace set: a single char, an array of chars, or a closure that decides per character. Each match comes off both ends, repeatedly, until neither end matches any more.
fn main() { println!("{:?}", "///usr/local/bin///".trim_matches('/')); println!("{:?}", ",a,b,,".trim_matches(',')); println!("{:?}", "aXbXa".trim_matches(['a', 'X'])); println!("{:?}", "((a + b))".trim_matches(|c| "()[]{}".contains(c)));}"usr/local/bin""a,b""b""a + b"Separators inside the string survive — only the ends are trimmed. trim_start_matches and trim_end_matches keep the same patterns for one end only.
A &str pattern is refused, even a one-character one. Trimming from both ends needs a searcher that runs backwards, and the searcher for multi-character patterns does not:
fn main() { let text = " x "; println!("{:?}", text.trim_matches(" "));}error[E0277]: the trait bound `for<'a> StrSearcher<'a, '_>: DoubleEndedSearcher<'a>` is not satisfied --> src/main.rs:3:40 |3 | println!("{:?}", text.trim_matches(" ")); | ------------ ^^^ the nightly-only, unstable trait `for<'a> DoubleEndedSearcher<'a>` is not implemented for `StrSearcher<'a, '_>` | | | required by a bound introduced by this call | = help: the following other types implement trait `DoubleEndedSearcher<'a>`: CharArrayRefSearcher<'a, 'b, N> CharArraySearcher<'a, N> CharPredicateSearcher<'a, F> CharSearcher<'a> CharSliceSearcher<'a, 'b>note: required by a bound in `core::str::<impl str>::trim_matches` --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/str/mod.rs:2378:4
For more information about this error, try `rustc --explain E0277`.Write ' ' for a single character and [' ', '\t'] for a set. A chunk longer than one character needs a loop around strip_prefix and strip_suffix.
The line ending you did not see
Section titled “The line ending you did not see”Text often arrives as one blob — a file read into a String, a response body — and splitting it on '\n' leaves half of a Windows line ending behind:
fn main() { let text = "alpha\r\nbeta\r\n"; let first = text.split('\n').next().unwrap(); println!("{first:?}"); println!("{}", first == "alpha"); println!("{}", first.trim() == "alpha"); for line in text.lines() { println!("{line:?}"); }}"alpha\r"falsetrue"alpha""beta"The first piece is "alpha\r", not "alpha", so the comparison is false. The \r is what makes this hard to spot: a carriage return moves the terminal’s cursor back to the start of the line, so println!("{first}") prints something that looks clean while the byte is still there. A trailing \r is enough to make "42\r".parse::<i32>() return Err. trim() removes it, because \r is whitespace. The newline is the other half:
fn main() { println!("{:?}", "alpha\r\n".trim_end_matches('\n')); println!("{:?}", "alpha\r\n".trim_end());}"alpha\r""alpha"trim_end_matches('\n') takes the newline and leaves the carriage return sitting at the end; trim_end() takes both. And lines() splits on \r\n as well as \n, which is why the block above shows clean lines — for a whole file, that is the simpler fix.
The error you get when you want a String
Section titled “The error you get when you want a String”trim borrows. Asking for an owned String without saying so is a type error, and the compiler’s own suggestion is the fix:
fn main() { let text = String::from(" padded "); let trimmed: String = text.trim(); println!("{trimmed}");}error[E0308]: mismatched types --> src/main.rs:3:27 |3 | let trimmed: String = text.trim(); | ------ ^^^^^^^^^^^ expected `String`, found `&str` | | | expected due to this |help: try using a conversion method |3 | let trimmed: String = text.trim().to_string(); | ++++++++++++
For more information about this error, try `rustc --explain E0308`.Add .to_string() when the trimmed text has to outlive the string it came from; keep the &str when it does not. Which conversion to use has the full comparison.
Related
Section titled “Related”- How to split a string —
split,split_whitespace, andsplit_once - When to use String::from, to_string, or into? — when the trimmed text needs to be owned
- How to format strings with println! and format! — the
{:?}that shows what{}hides - Working with text — the rest of the chapter