Description
Proposal
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>
.
Problem statement
Currently, Rust's string API has a gap between two existing method families:
strip_prefix
/strip_suffix
: Remove a prefix/suffix if present, but returnOption<&str>
trim_start_matches
/trim_end_matches
: Always return&str
, but repeatedly remove all prefixes/suffixes that match a pattern
There'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 sketch
My 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.
Method signatures
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 patternprefix
, return the subslice after that prefix. Otherwise, return the original string slice.trim_suffix(suffix)
: If the string slice ends with the patternsuffix
, return the subslice before that suffix. Otherwise, return the original string slice.
Implementation
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
- Adds two more methods to an already large
str
API. - Potential for confusion with existing
strip_*
andtrim_*
methods. - Minor increase in standard library maintenance burden.
Alternatives
-
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 existingtrim_start_matches
/trim_end_matches
methods, using singular vs plural to distinguish behavior. However, this was rejected because:- The singular/plural distinction is subtle and error-prone.
trim_start_match
vstrim_start_matches
could easily be confused.trim_prefix
/trim_suffix
more clearly communicate the intent to remove a specific prefix/suffix.- The prefix/suffix terminology aligns naturally with existing
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
(neverOption
).strip_*
methods returnOption<&str>
when removal might fail.- The
_prefix
/_suffix
suffixes clearly indicate what is being trimmed and match the existingstrip_prefix
/strip_suffix
methods.
Why not just use value.strip_prefix().unwrap_or(value)
?
While the unwrap_or()
pattern works for simple cases, it has significant drawbacks:
- Poor method chaining: The
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();
- Verbosity: Requires storing intermediate results and repeating variable names.
- Unclear intent: The
unwrap_or()
pattern doesn't clearly communicate "remove if present, otherwise unchanged".
Links and related work
Many string processing libraries in other languages provide similar functionality:
- Python:
str.removeprefix()
andstr.removesuffix()
(Python 3.9+) - JavaScript: Various utility libraries provide
trimPrefix
/trimSuffix
functions - Go:
strings.TrimPrefix()
andstrings.TrimSuffix()
What happens now?
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 responses
The 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):
- We think this problem seems worth solving, and the standard library might be the right place to solve it.
- We think that this probably doesn't belong in the standard library.
Second, if there's a concrete solution:
- We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
- We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.