Repository: fancy-regex/fancy-regex (language: rust)

Issue:
Add Regex::replace (and replacen, replaceall)
I thought this crate would have a replace or replace_all like the rust regex crate does.
Is there any way to do replace with this crate?


Discussion / hints:
Yeah it doesn't provide replace functionality yet. You can implement it yourself by repeatedly calling `captures_from_pos` and adding the pieces of the string before/after matches.
With 0.5.0 you can now use `captures_iter`, which makes it a bit easier. But it would still be nice to have `replace`.

Fix patch:
diff --git a/src/lib.rs b/src/lib.rs
index 9cd9255..d281dea 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -157,6 +157,7 @@ mod compile;
 mod error;
 mod expand;
 mod parse;
+mod replacer;
 mod vm;
 
 use crate::analyze::analyze;
@@ -166,6 +167,7 @@ use crate::vm::Prog;
 
 pub use crate::error::{Error, Result};
 pub use crate::expand::Expander;
+pub use crate::replacer::{NoExpand, Replacer, ReplacerRef};
 use std::borrow::Cow;
 
 const MAX_RECURSION: usize = 64;
@@ -759,6 +761,178 @@ impl Regex {
             RegexImpl::Fancy { prog, .. } => prog.debug_print(),
         }
     }
+
+    /// Replaces the leftmost-first match with the replacement provided.
+    /// The replacement can be a regular string (where `$N` and `$name` are
+    /// expanded to match capture groups) or a function that takes the matches'
+    /// `Captures` and returns the replaced string.
+    ///
+    /// If no match is found, then a copy of the string is returned unchanged.
+    ///
+    /// # Replacement string syntax
+    ///
+    /// All instances of `$name` in the replacement text is replaced with the
+    /// corresponding capture group `name`.
+    ///
+    /// `name` may be an integer corresponding to the index of the
+    /// capture group (counted by order of opening parenthesis where `0` is the
+    /// entire match) or it can be a name (consisting of letters, digits or
+    /// underscores) corresponding to a named capture group.
+    ///
+    /// If `name` isn't a valid capture group (whether the name doesn't exist
+    /// or isn't a valid index), then it is replaced with the empty string.
+    ///
+    /// The longest possible name is used. e.g., `$1a` looks up the capture
+    /// group named `1a` and not the capture group at index `1`. To exert more
+    /// precise control over the name, use braces, e.g., `${1}a`.
+    ///
+    /// To write a literal `$` use `$$`.
+    ///
+    /// # Examples
+    ///
+    /// Note that this function is polymorphic with respect to the replacement.
+    /// In typical usage, this can just be a normal string:
+    ///
+    /// ```rust
+    /// # use fancy_regex::Regex;
+    /// let re = Regex::new("[^01]+").unwrap();
+    /// assert_eq!(re.replace("1078910", ""), "1010");
+    /// ```
+    ///
+    /// But anything satisfying the `Replacer` trait will work. For example,
+    /// a closure of type `|&Captures| -> String` provides direct access to the
+    /// captures corresponding to a match. This allows one to access
+    /// capturing group matches easily:
+    ///
+    /// ```rust
+    /// # use fancy_regex::{Regex, Captures};
+    /// let re = Regex::new(r"([^,\s]+),\s+(\S+)").unwrap();
+    /// let result = re.replace("Springsteen, Bruce", |caps: &Captures| {
+    ///     format!("{} {}", &caps[2], &caps[1])
+    /// });
+    /// assert_eq!(result, "Bruce Springsteen");
+    /// ```
+    ///
+    /// But this is a bit cumbersome to use all the time. Instead, a simple
+    /// syntax is supported that expands `$name` into the corresponding capture
+    /// group. Here's the last example, but using this expansion technique
+    /// with named capture groups:
+    ///
+    /// ```rust
+    /// # use fancy_regex::Regex;
+    /// let re = Regex::new(r"(?P<last>[^,\s]+),\s+(?P<first>\S+)").unwrap();
+    /// let result = re.replace("Springsteen, Bruce", "$first $last");
+    /// assert_eq!(result, "Bruce Springsteen");
+    /// ```
+    ///
+    /// Note that using `$2` instead of `$first` or `$1` instead of `$last`
+    /// would produce the same result. To write a literal `$` use `$$`.
+    ///
+    /// Sometimes the replacement string requires use of curly braces to
+    /// delineate a capture group replacement and surrounding literal text.
+    /// For example, if we wanted to join two words together with an
+    /// underscore:
+    ///
+    /// ```rust
+    /// # use fancy_regex::Regex;
+    /// let re = Regex::new(r"(?P<first>\w+)\s+(?P<second>\w+)").unwrap();
+    /// let result = re.replace("deep fried", "${first}_$second");
+    /// assert_eq!(result, "deep_fried");
+    /// ```
+    ///
+    /// Without the curly braces, the capture group name `first_` would be
+    /// used, and since it doesn't exist, it would be replaced with the empty
+    /// string.
+    ///
+    /// Finally, sometimes you just want to replace a literal string with no
+    /// regard for capturing group expansion. This can be done by wrapping a
+    /// byte string with `NoExpand`:
+    ///
+    /// ```rust
+    /// # use fancy_regex::Regex;
+    /// use fancy_regex::NoExpand;
+    ///
+    /// let re = Regex::new(r"(?P<last>[^,\s]+),\s+(\S+)").unwrap();
+    /// let result = re.replace("Springsteen, Bruce", NoExpand("$2 $last"));
+    /// assert_eq!(result, "$2 $last");
+    /// ```
+    pub fn replace<'t, R: Replacer>(&self, text: &'t str, rep: R) -> Cow<'t, str> {
+        self.replacen(text, 1, rep)
+    }
+
+    /// Replaces all non-overlapping matches in `text` with the replacement
+    /// provided. This is the same as calling `replacen` with `limit` set to
+    /// `0`.
+    ///
+    /// See the documentation for `replace` for details on how to access
+    /// capturing group matches in the replacement string.
+    pub fn replace_all<'t, R: Replacer>(&self, text: &'t str, rep: R) -> Cow<'t, str> {
+        self.replacen(text, 0, rep)
+    }
+
+    /// Replaces at most `limit` non-overlapping matches in `text` with the
+    /// replacement provided. If `limit` is 0, then all non-overlapping matches
+    /// are replaced.
+    ///
+    /// See the documentation for `replace` for details on how to access
+    /// capturing group matches in the replacement string.
+    pub fn replacen<'t, R: Replacer>(
+        &self,
+        text: &'t str,
+        limit: usize,
+        mut rep: R,
+    ) -> Cow<'t, str> {
+        // If we know that the replacement doesn't have any capture expansions,
+        // then we can fast path. The fast path can make a tremendous
+        // difference:
+        //
+        //   1) We use `find_iter` instead of `captures_iter`. Not asking for
+        //      captures generally makes the regex engines faster.
+        //   2) We don't need to look up all of the capture groups and do
+        //      replacements inside the replacement string. We just push it
+        //      at each match and be done with it.
+        if let Some(rep) = rep.no_expansion() {
+            let mut it = self.find_iter(text).enumerate().peekable();
+            if it.peek().is_none() {
+                return Cow::Borrowed(text);
+            }
+            let mut new = String::with_capacity(text.len());
+            let mut last_match = 0;
+            for (i, m) in it {
+                let m = m.unwrap();
+                if limit > 0 && i >= limit {
+                    break;
+                }
+                new.push_str(&text[last_match..m.start()]);
+                new.push_str(&rep);
+                last_match = m.end();
+            }
+            new.push_str(&text[last_match..]);
+            return Cow::Owned(new);
+        }
+
+        // The slower path, which we use if the replacement needs access to
+        // capture groups.
+        let mut it = self.captures_iter(text).enumerate().peekable();
+        if it.peek().is_none() {
+            return Cow::Borrowed(text);
+        }
+        let mut new = String::with_capacity(text.len());
+        let mut last_match = 0;
+        for (i, cap) in it {
+            let cap = cap.unwrap();
+            if limit > 0 && i >= limit {
+                break;
+            }
+            // unwrap on 0 is OK because captures only reports matches
+            let m = cap.get(0).unwrap();
+            new.push_str(&text[last_match..m.start()]);
+            rep.replace_append(&cap, &mut new);
+            last_match = m.end();
+        }
+        new.push_str(&text[last_match..]);
+        Cow::Owned(new)
+    }
 }
 
 impl<'t> Match<'t> {
diff --git a/src/replacer.rs b/src/replacer.rs
new file mode 100644
index 0000000..ca207c8
--- /dev/null
+++ b/src/replacer.rs
@@ -0,0 +1,160 @@
+use crate::Captures;
+use std::borrow::Cow;
+
+/// Replacer describes types that can be used to replace matches in a string.
+///
+/// In general, users of this crate shouldn't need to implement this trait,
+/// since implementations are already provided for `&str` along with other
+/// variants of string types and `FnMut(&Captures) -> String` (or any
+/// `FnMut(&Captures) -> T` where `T: AsRef<str>`), which covers most use cases.
+pub trait Replacer {
+    /// Appends text to `dst` to replace the current match.
+    ///
+    /// The current match is represented by `caps`, which is guaranteed to
+    /// have a match at capture group `0`.
+    ///
+    /// For example, a no-op replacement would be
+    /// `dst.push_str(caps.get(0).unwrap().as_str())`.
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String);
+
+    /// Return a fixed unchanging replacement string.
+    ///
+    /// When doing replacements, if access to `Captures` is not needed (e.g.,
+    /// the replacement byte string does not need `$` expansion), then it can
+    /// be beneficial to avoid finding sub-captures.
+    ///
+    /// In general, this is called once for every call to `replacen`.
+    fn no_expansion(&mut self) -> Option<Cow<str>> {
+        None
+    }
+
+    /// Return a `Replacer` that borrows and wraps this `Replacer`.
+    ///
+    /// This is useful when you want to take a generic `Replacer` (which might
+    /// not be cloneable) and use it without consuming it, so it can be used
+    /// more than once.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use fancy_regex::{Regex, Replacer};
+    ///
+    /// fn replace_all_twice<R: Replacer>(
+    ///     re: Regex,
+    ///     src: &str,
+    ///     mut rep: R,
+    /// ) -> String {
+    ///     let dst = re.replace_all(src, rep.by_ref());
+    ///     let dst = re.replace_all(&dst, rep.by_ref());
+    ///     dst.into_owned()
+    /// }
+    /// ```
+    fn by_ref(&mut self) -> ReplacerRef<Self> {
+        ReplacerRef(self)
+    }
+}
+
+/// By-reference adaptor for a `Replacer`
+///
+/// Returned by [`Replacer::by_ref`](trait.Replacer.html#method.by_ref).
+#[derive(Debug)]
+pub struct ReplacerRef<'a, R: ?Sized>(&'a mut R);
+
+impl<'a, R: Replacer + ?Sized + 'a> Replacer for ReplacerRef<'a, R> {
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        self.0.replace_append(caps, dst)
+    }
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        self.0.no_expansion()
+    }
+}
+
+impl<'a> Replacer for &'a str {
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        caps.expand(*self, dst);
+    }
+
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        no_expansion(self)
+    }
+}
+
+impl<'a> Replacer for &'a String {
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        self.as_str().replace_append(caps, dst)
+    }
+
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        no_expansion(self)
+    }
+}
+
+impl Replacer for String {
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        self.as_str().replace_append(caps, dst)
+    }
+
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        no_expansion(self)
+    }
+}
+
+impl<'a> Replacer for Cow<'a, str> {
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        self.as_ref().replace_append(caps, dst)
+    }
+
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        no_expansion(self)
+    }
+}
+
+impl<'a> Replacer for &'a Cow<'a, str> {
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        self.as_ref().replace_append(caps, dst)
+    }
+
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        no_expansion(self)
+    }
+}
+
+fn no_expansion<T: AsRef<str>>(t: &T) -> Option<Cow<'_, str>> {
+    let s = t.as_ref();
+    if s.contains('$') {
+        None
+    } else {
+        Some(Cow::Borrowed(s))
+    }
+}
+
+impl<F, T> Replacer for F
+where
+    F: FnMut(&Captures<'_>) -> T,
+    T: AsRef<str>,
+{
+    fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
+        dst.push_str((*self)(caps).as_ref());
+    }
+}
+
+/// `NoExpand` indicates literal string replacement.
+///
+/// It can be used with `replace` and `replace_all` to do a literal string
+/// replacement without expanding `$name` to their corresponding capture
+/// groups. This can be both convenient (to avoid escaping `$`, for example)
+/// and performant (since capture groups don't need to be found).
+///
+/// `'t` is the lifetime of the literal text.
+#[derive(Clone, Debug)]
+pub struct NoExpand<'t>(pub &'t str);
+
+impl<'t> Replacer for NoExpand<'t> {
+    fn replace_append(&mut self, _: &Captures<'_>, dst: &mut String) {
+        dst.push_str(self.0);
+    }
+
+    fn no_expansion(&mut self) -> Option<Cow<'_, str>> {
+        Some(Cow::Borrowed(self.0))
+    }
+}


Test patch:
diff --git a/tests/replace.rs b/tests/replace.rs
new file mode 100644
index 0000000..deef3c3
--- /dev/null
+++ b/tests/replace.rs
@@ -0,0 +1,82 @@
+use fancy_regex::{Captures, NoExpand};
+use std::borrow::Cow;
+
+mod common;
+
+#[test]
+fn replacer_string() {
+    let regex = common::regex(
+        r"\b([sS])uc(?:cs|s?)e(ed(?:ed|ing|s?)|ss(?:es|ful(?:ly)?|i(?:ons?|ve(?:ly)?)|ors?)?)\b",
+    );
+
+    // Replacer impl for &str
+    let result = regex.replace("a sucessful b", "${1}ucce$2");
+    assert_eq!(result, "a successful b");
+
+    // Replacer impl for &String
+    let repl_string = "${1}ucce$2".to_string();
+    let result = regex.replace("a Suceeded b", &repl_string);
+    assert_eq!(result, "a Succeeded b");
+
+    // Replacer impl for String
+    let result = regex.replace("a sucessor b", repl_string);
+    assert_eq!(result, "a successor b");
+}
+
+#[test]
+fn replacer_cow() {
+    let regex = common::regex(r"\b([oO])mmi(?=t)t?(t(?:ed|ing)|s)\b");
+
+    // Replacer impl for &Cow<str>
+    let result = regex.replace("a ommiting b", &Cow::from("${1}mit$2"));
+    assert_eq!(result, "a omitting b");
+
+    // Replacer for Cow<str>::Borrowed
+    let result = regex.replace("a ommited b", Cow::Borrowed("${1}mit$2"));
+    assert_eq!(result, "a omitted b");
+
+    // Replacer for Cow<str>::Owned
+    let result = regex.replace("a Ommits b", Cow::Owned("${1}mit$2".to_string()));
+    assert_eq!(result, "a Omits b");
+}
+
+#[test]
+fn replacer_noexpand() {
+    let regex = common::regex(r"\b([aA])n+ull(ar|ments?|s?)\b");
+
+    // Replacer impl for NoExpand
+    let result = regex.replace("a anullment b", NoExpand("${1}nnul$2"));
+    assert_eq!(result, "a ${1}nnul$2 b");
+}
+
+#[test]
+fn replacer_callback() {
+    let regex = common::regex(r"\b([aA])p(?:p[or]|ro)x\.?(?=[ \)\n])");
+
+    // Replacer impl for FnMut(&Captures)
+    let result = regex.replace("a Aprox b", |cap: &Captures| {
+        format!("{}pprox.", cap.get(1).unwrap().as_str())
+    });
+    assert_eq!(result, "a Approx. b");
+}
+
+/// `replace()` does only one replacement
+#[test]
+fn replace_one() {
+    let regex = common::regex("bla");
+    assert_eq!(regex.replace("blabla", "foo"), "foobla");
+}
+
+/// `replace_all()` replaces all non-overlapping matches
+#[test]
+fn replace_all() {
+    let regex = common::regex("aa");
+    assert_eq!(regex.replace_all("aaaa aaa aa a", "xx"), "xxxx xxa xx a");
+}
+
+/// `replacen()` replaces predefined number of times
+#[test]
+fn replacen() {
+    let regex = common::regex("bla");
+    assert_eq!(regex.replacen("blablabla", 2, "foo"), "foofoobla");
+}