vrl/stdlib/
replace.rs

1use crate::compiler::prelude::*;
2use std::sync::LazyLock;
3
4static DEFAULT_COUNT: LazyLock<Value> = LazyLock::new(|| Value::Integer(-1));
5
6static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
7    vec![
8        Parameter::required("value", kind::BYTES, "The original string."),
9        Parameter::required(
10            "pattern",
11            kind::BYTES | kind::REGEX,
12            "Replace all matches of this pattern. Can be a static string or a regular expression.",
13        ),
14        Parameter::required(
15            "with",
16            kind::BYTES,
17            "The string that the matches are replaced with.",
18        ),
19        Parameter::optional(
20            "count",
21            kind::INTEGER,
22            "The maximum number of replacements to perform. `-1` means replace all matches.",
23        )
24        .default(&DEFAULT_COUNT),
25    ]
26});
27
28#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // TODO consider removal options
29fn replace(value: &Value, with_value: &Value, count: Value, pattern: Value) -> Resolved {
30    let value = value.try_bytes_utf8_lossy()?;
31    let with = with_value.try_bytes_utf8_lossy()?;
32    let count = count.try_integer()?;
33    match pattern {
34        Value::Bytes(bytes) => {
35            let pattern = String::from_utf8_lossy(&bytes);
36            let replaced = match count {
37                i if i > 0 => value.replacen(pattern.as_ref(), &with, i as usize),
38                i if i < 0 => value.replace(pattern.as_ref(), &with),
39                _ => value.into_owned(),
40            };
41
42            Ok(replaced.into())
43        }
44        Value::Regex(regex) => {
45            let replaced = match count {
46                i if i > 0 => Bytes::copy_from_slice(
47                    regex.replacen(&value, i as usize, with.as_ref()).as_bytes(),
48                )
49                .into(),
50                i if i < 0 => {
51                    Bytes::copy_from_slice(regex.replace_all(&value, with.as_ref()).as_bytes())
52                        .into()
53                }
54                _ => value.into(),
55            };
56
57            Ok(replaced)
58        }
59        value => Err(ValueError::Expected {
60            got: value.kind(),
61            expected: Kind::regex() | Kind::bytes(),
62        }
63        .into()),
64    }
65}
66
67#[derive(Clone, Copy, Debug)]
68pub struct Replace;
69
70impl Function for Replace {
71    fn identifier(&self) -> &'static str {
72        "replace"
73    }
74
75    fn usage(&self) -> &'static str {
76        indoc! {"
77            Replaces all matching instances of `pattern` in `value`.
78
79            The `pattern` argument accepts regular expression capture groups.
80
81            **Note when using capture groups**:
82            - You will need to escape the `$` by using `$$` to avoid Vector interpreting it as an
83              [environment variable when loading configuration](/docs/reference/environment_variables/#escaping)
84            - If you want a literal `$` in the replacement pattern, you will also need to escape this
85              with `$$`. When combined with environment variable interpolation in config files this
86              means you will need to use `$$$$` to have a literal `$` in the replacement pattern.
87        "}
88    }
89
90    fn category(&self) -> &'static str {
91        Category::String.as_ref()
92    }
93
94    fn return_kind(&self) -> u16 {
95        kind::BYTES
96    }
97
98    fn parameters(&self) -> &'static [Parameter] {
99        PARAMETERS.as_slice()
100    }
101
102    fn examples(&self) -> &'static [Example] {
103        &[
104            example! {
105                title: "Replace literal text",
106                source: r#"replace("Apples and Bananas", "and", "not")"#,
107                result: Ok("Apples not Bananas"),
108            },
109            example! {
110                title: "Replace using regular expression",
111                source: r#"replace("Apples and Bananas", r'(?i)bananas', "Pineapples")"#,
112                result: Ok("Apples and Pineapples"),
113            },
114            example! {
115                title: "Replace first instance",
116                source: r#"replace("Bananas and Bananas", "Bananas", "Pineapples", count: 1)"#,
117                result: Ok("Pineapples and Bananas"),
118            },
119            example! {
120                title: "Replace with capture groups",
121                source: indoc! {r#"
122                    # Note that in the context of Vector configuration files, an extra `$` escape character is required
123                    # (i.e. `$$num`) to avoid interpreting `num` as an environment variable.
124                    replace("foo123bar", r'foo(?P<num>\d+)bar', "$num")
125                "#},
126                result: Ok(r#""123""#),
127            },
128            example! {
129                title: "Replace all",
130                source: r#"replace("foobar", "o", "i")"#,
131                result: Ok("fiibar"),
132            },
133        ]
134    }
135
136    fn compile(
137        &self,
138        _state: &state::TypeState,
139        _ctx: &mut FunctionCompileContext,
140        arguments: ArgumentList,
141    ) -> Compiled {
142        let value = arguments.required("value");
143        let pattern = arguments.required("pattern");
144        let with = arguments.required("with");
145        let count = arguments.optional("count");
146
147        Ok(ReplaceFn {
148            value,
149            pattern,
150            with,
151            count,
152        }
153        .as_expr())
154    }
155}
156
157#[derive(Debug, Clone)]
158struct ReplaceFn {
159    value: Box<dyn Expression>,
160    pattern: Box<dyn Expression>,
161    with: Box<dyn Expression>,
162    count: Option<Box<dyn Expression>>,
163}
164
165impl FunctionExpression for ReplaceFn {
166    fn resolve(&self, ctx: &mut Context) -> Resolved {
167        let value = self.value.resolve(ctx)?;
168        let with_value = self.with.resolve(ctx)?;
169        let count = self
170            .count
171            .map_resolve_with_default(ctx, || DEFAULT_COUNT.clone())?;
172        let pattern = self.pattern.resolve(ctx)?;
173
174        replace(&value, &with_value, count, pattern)
175    }
176
177    fn type_def(&self, _: &state::TypeState) -> TypeDef {
178        TypeDef::bytes().infallible()
179    }
180}
181
182#[cfg(test)]
183#[allow(clippy::trivial_regex)]
184mod test {
185    use super::*;
186
187    test_function![
188        replace => Replace;
189
190        replace_string1 {
191             args: func_args![value: "I like apples and bananas",
192                              pattern: "a",
193                              with: "o"
194             ],
195             want: Ok("I like opples ond bononos"),
196             tdef: TypeDef::bytes().infallible(),
197         }
198
199        replace_string2 {
200             args: func_args![value: "I like apples and bananas",
201                              pattern: "a",
202                              with: "o",
203                              count: -1
204             ],
205             want: Ok("I like opples ond bononos"),
206             tdef: TypeDef::bytes().infallible(),
207         }
208
209        replace_string3 {
210             args: func_args![value: "I like apples and bananas",
211                              pattern: "a",
212                              with: "o",
213                              count: 0
214             ],
215             want: Ok("I like apples and bananas"),
216             tdef: TypeDef::bytes().infallible(),
217         }
218
219        replace_string4 {
220             args: func_args![value: "I like apples and bananas",
221                              pattern: "a",
222                              with: "o",
223                              count: 1
224             ],
225             want: Ok("I like opples and bananas"),
226             tdef: TypeDef::bytes().infallible(),
227         }
228
229        replace_string5 {
230             args: func_args![value: "I like apples and bananas",
231                              pattern: "a",
232                              with: "o",
233                              count: 2
234             ],
235             want: Ok("I like opples ond bananas"),
236             tdef: TypeDef::bytes().infallible(),
237         }
238
239
240        replace_regex1 {
241             args: func_args![value: "I like opples ond bananas",
242                              pattern: regex::Regex::new("a").unwrap(),
243                              with: "o"
244             ],
245             want: Ok("I like opples ond bononos"),
246             tdef: TypeDef::bytes().infallible(),
247         }
248
249
250        replace_regex2 {
251             args: func_args![value: "I like apples and bananas",
252                              pattern: regex::Regex::new("a").unwrap(),
253                              with: "o",
254                              count: -1
255             ],
256             want: Ok("I like opples ond bononos"),
257             tdef: TypeDef::bytes().infallible(),
258         }
259
260        replace_regex3 {
261             args: func_args![value: "I like apples and bananas",
262                              pattern: regex::Regex::new("a").unwrap(),
263                              with: "o",
264                              count: 0
265             ],
266             want: Ok("I like apples and bananas"),
267             tdef: TypeDef::bytes().infallible(),
268         }
269
270        replace_regex4 {
271             args: func_args![value: "I like apples and bananas",
272                              pattern: regex::Regex::new("a").unwrap(),
273                              with: "o",
274                              count: 1
275             ],
276             want: Ok("I like opples and bananas"),
277             tdef: TypeDef::bytes().infallible(),
278         }
279
280        replace_regex5 {
281             args: func_args![value: "I like apples and bananas",
282                              pattern: regex::Regex::new("a").unwrap(),
283                              with: "o",
284                              count: 2
285             ],
286             want: Ok("I like opples ond bananas"),
287             tdef: TypeDef::bytes().infallible(),
288         }
289
290        replace_other {
291            args: func_args![value: "I like apples and bananas",
292                             pattern: "apples",
293                             with: "biscuits"
294            ],
295             want: Ok( "I like biscuits and bananas"),
296             tdef: TypeDef::bytes().infallible(),
297         }
298
299        replace_other2 {
300             args: func_args![value: "I like apples and bananas",
301                              pattern: regex::Regex::new("a").unwrap(),
302                              with: "o",
303                              count: 1
304             ],
305             want: Ok("I like opples and bananas"),
306             tdef: TypeDef::bytes().infallible(),
307         }
308
309        replace_other3 {
310            args: func_args![value: "I like [apples] and bananas",
311                             pattern: regex::Regex::new("\\[apples\\]").unwrap(),
312                             with: "biscuits"
313            ],
314            want: Ok("I like biscuits and bananas"),
315            tdef: TypeDef::bytes().infallible(),
316        }
317    ];
318}