Skip to content

Add trim_prefix and trim_suffix methods to str #597

Closed
@deven

Description

@deven

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 return Option<&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 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.

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_* and trim_* methods.
  • Minor increase in standard library maintenance burden.

Alternatives

  1. Extension trait in an external crate: This works but fragments the ecosystem and doesn't provide the discoverability of standard library methods.

  2. 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:

    • The singular/plural distinction is subtle and error-prone.
    • 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.
    • The prefix/suffix terminology aligns naturally with existing strip_prefix/strip_suffix methods.
  3. 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.
  • The _prefix/_suffix suffixes clearly indicate what is being trimmed and match the existing strip_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:

  1. 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();
  1. Verbosity: Requires storing intermediate results and repeating variable names.
  2. 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() and str.removesuffix() (Python 3.9+)
  • JavaScript: Various utility libraries provide trimPrefix/trimSuffix functions
  • Go: strings.TrimPrefix() and strings.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions