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)] fn 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}