Add trim_prefix
and trim_suffix
methods to str
which remove at most one occurrence of a specified prefix or suffix, always returning &str
rather than Option<&str>
.
Currently, Rust's string API has a gap between two existing method families:
strip_prefix
/strip_suffix
: Remove a prefix/suffix if present, but return Option<&str>
trim_start_matches
/trim_end_matches
: Always return &str
, but repeatedly remove all prefixes/suffixes that match a patternThere's no method that removes at most one occurrence of a prefix/suffix while always returning a string slice. This breaks method chaining entirely and forces developers to write verbose code for a common pattern:
let result = if let Some(stripped) = s.strip_prefix(prefix) { stripped } else { s };
This can be simplified somewhat by using unwrap_or()
, but still this remains more verbose and awkward than necessary, and the source variable still needs to be used twice:
let result = s.strip_prefix(prefix).unwrap_or(s);Motivating examples or use cases
For example, suppose a string contains a URL, but it might also have leading/trailing whitespace, possible open and/or closing angle brackets and possibly more leading/trailing whitespace outside any angle brackets. Here are a number of variations matching that pattern:
let s = " < https://example.com/ > "; let s = "< https://example.com/ >"; let s = " <https://example.com/> "; let s = "<https://example.com/>"; let s = "<https://example.com/"; let s = "<https://example.com/"; let s = "https://example.com/>"; let s = " https://example.com/ "; let s = "https://example.com/";
The URL could be extracted from any of these strings using strip_prefix
and strip_suffix
, but it is awkward, requiring several steps with variables for each step:
let s = " < https://example.com/ > "; let s = s.trim(); let s = s.strip_prefix('<').unwrap_or(s); let s = s.strip_suffix('>').unwrap_or(s); let result = s.trim(); assert_eq!(result, "https://example.com/");
There is no easy way to accomplish the same thing with a simple method chain.
Solution sketchMy proposal is to add new trim_prefix
and trim_suffix
methods to str
, which would work similarly to the strip_prefix
and strip_suffix
methods, but they would always return &str
instead of Option<&str>
, allowing easy method chaining. This way, the multi-step process above could be replaced with the method chain .trim().trim_prefix('<').trim_suffix('>').trim()
, which is much simpler and easier to read and understand:
assert_eq!(" < https://example.com/ > ".trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( "< https://example.com/ >" .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( " <https://example.com/> " .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( "<https://example.com/>" .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( "<https://example.com/" .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( "<https://example.com/" .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( "https://example.com/>" .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( " https://example.com/ " .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/"); assert_eq!( "https://example.com/" .trim().trim_prefix('<').trim_suffix('>').trim(), "https://example.com/");
These methods would complement the existing string manipulation methods by providing a middle ground between the fallible strip_prefix
/strip_suffix
methods and the greedy trim_start_matches
/trim_end_matches
methods.
impl str { pub fn trim_prefix<P>(&self, prefix: P) -> &str where P: Pattern, pub fn trim_suffix<P>(&self, suffix: P) -> &str where P: Pattern, <P as Pattern>::Searcher<'a>: for<'a> ReverseSearcher<'a>, }Behavior specification
trim_prefix(prefix)
: If the string slice starts with the pattern prefix
, return the subslice after that prefix. Otherwise, return the original string slice.trim_suffix(suffix)
: If the string slice ends with the pattern suffix
, return the subslice before that suffix. Otherwise, return the original string slice.impl str { /// Returns a string slice with the optional prefix removed. /// /// If the string starts with the pattern `prefix`, returns the substring after the prefix. /// Unlike [`strip_prefix`], this method always returns a string slice instead of returning [`Option`]. /// /// If the string does not start with `prefix`, returns the original string unchanged. /// /// The [pattern] can be a `&str`, [`char`], a slice of [`char`]s, or a /// function or closure that determines if a character matches. /// /// [`char`]: prim@char /// [pattern]: self::pattern /// [`strip_prefix`]: Self::strip_prefix /// /// # Examples /// /// ``` /// // Prefix present - removes it /// assert_eq!("foo:bar".trim_prefix("foo:"), "bar"); /// assert_eq!("foofoo".trim_prefix("foo"), "foo"); /// /// // Prefix absent - returns original string /// assert_eq!("foo:bar".trim_prefix("bar"), "foo:bar"); /// ``` #[must_use = "this returns the remaining substring as a new slice, \ without modifying the original"] #[unstable(feature = "trim_prefix_suffix", issue = "none")] pub fn trim_prefix<P: Pattern>(&self, prefix: P) -> &str { prefix.strip_prefix_of(self).unwrap_or(self) } /// Returns a string slice with the optional suffix removed. /// /// If the string ends with the pattern `suffix`, returns the substring before the suffix. /// Unlike [`strip_suffix`], this method always returns a string slice instead of returning [`Option`]. /// /// If the string does not end with `suffix`, returns the original string unchanged. /// /// The [pattern] can be a `&str`, [`char`], a slice of [`char`]s, or a /// function or closure that determines if a character matches. /// /// [`char`]: prim@char /// [pattern]: self::pattern /// [`strip_suffix`]: Self::strip_suffix /// /// # Examples /// /// ``` /// // Suffix present - removes it /// assert_eq!("bar:foo".trim_suffix(":foo"), "bar"); /// assert_eq!("foofoo".trim_suffix("foo"), "foo"); /// /// // Suffix absent - returns original string /// assert_eq!("bar:foo".trim_suffix("bar"), "bar:foo"); /// ``` #[must_use = "this returns the remaining substring as a new slice, \ without modifying the original"] #[unstable(feature = "trim_prefix_suffix", issue = "none")] pub fn trim_suffix<P: Pattern>(&self, suffix: P) -> &str where for<'a> P::Searcher<'a>: ReverseSearcher<'a>, { suffix.strip_suffix_of(self).unwrap_or(self) } }Examples
// String literals assert_eq!("hello world".trim_prefix("hello"), " world"); assert_eq!("hello world".trim_prefix("hi"), "hello world"); assert_eq!("hello world".trim_suffix("world"), "hello "); assert_eq!("hello world".trim_suffix("universe"), "hello world"); // Characters assert_eq!("xhello".trim_prefix('x'), "hello"); assert_eq!("hellox".trim_suffix('x'), "hello"); // Empty prefix/suffix assert_eq!("hello".trim_prefix(""), "hello"); assert_eq!("hello".trim_suffix(""), "hello"); // Multiple occurrences (only first/last is removed) assert_eq!("aaahello".trim_prefix('a'), "aahello"); assert_eq!("helloaaa".trim_suffix('a'), "helloaa");Drawbacks
str
API.strip_*
and trim_*
methods.Extension trait in an external crate: This works but fragments the ecosystem and doesn't provide the discoverability of standard library methods.
Alternative naming: trim_start_match
/trim_end_match
names would follow the pattern of existing trim_start_matches
/trim_end_matches
methods, using singular vs plural to distinguish behavior. However, this was rejected because:
trim_start_match
vs trim_start_matches
could easily be confused.trim_prefix
/trim_suffix
more clearly communicate the intent to remove a specific prefix/suffix.strip_prefix
/strip_suffix
methods.Generic over removal count: A method that could remove N occurrences was considered too complex for the common use case.
The trim_prefix
/trim_suffix
naming follows established conventions:
trim_*
methods always return &str
(never Option
).strip_*
methods return Option<&str>
when removal might fail._prefix
/_suffix
suffixes clearly indicate what is being trimmed and match the existing strip_prefix
/strip_suffix
methods.value.strip_prefix().unwrap_or(value)
?
While the unwrap_or()
pattern works for simple cases, it has significant drawbacks:
unwrap_or()
approach breaks fluent interfaces entirely.// Clean, readable chaining with proposed methods: let result = value.trim().trim_prefix(prefix).trim_suffix(suffix).trim(); // Current approach - chaining is impossible: let result = value.trim(); let result = result.strip_prefix(prefix).unwrap_or(result); let result = result.strip_suffix(suffix).unwrap_or(result); let result = result.trim(); // Attempting to chain with current methods doesn't work: let trimmed = value.trim(); let result = trimmed .strip_prefix(prefix).unwrap_or(trimmed) .strip_suffix(suffix).unwrap_or(???) // Can't reference intermediate values .trim();
unwrap_or()
pattern doesn't clearly communicate "remove if present, otherwise unchanged".Many string processing libraries in other languages provide similar functionality:
str.removeprefix()
and str.removesuffix()
(Python 3.9+)trimPrefix
/trimSuffix
functionsstrings.TrimPrefix()
and strings.TrimSuffix()
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responsesThe libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
Second, if there's a concrete solution:
kennytm, clarfonthey, hanna-kruppe, bbb651, wmstack and 1 morebbb651 and Scripter17
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4