Skip to main content

nu_command/strings/
parse.rs

1use fancy_regex::{Captures, Regex, RegexBuilder};
2use nu_engine::command_prelude::*;
3use nu_protocol::shell_error::generic::GenericError;
4use nu_protocol::{ListStream, Signals, engine::StateWorkingSet};
5use std::collections::VecDeque;
6
7#[derive(Clone)]
8pub struct Parse;
9
10impl Command for Parse {
11    fn name(&self) -> &str {
12        "parse"
13    }
14
15    fn description(&self) -> &str {
16        "Parse columns from string data using a simple pattern or a supplied regular expression."
17    }
18
19    fn search_terms(&self) -> Vec<&str> {
20        vec!["pattern", "match", "regex", "str extract"]
21    }
22
23    fn extra_description(&self) -> &str {
24        "The parse command always uses regular expressions even when you use a simple pattern. If a simple pattern is supplied, parse will transform that pattern into a regular expression."
25    }
26
27    fn signature(&self) -> nu_protocol::Signature {
28        Signature::build("parse")
29            .required("pattern", SyntaxShape::String, "The pattern to match.")
30            .input_output_types(vec![
31                (Type::String, Type::table()),
32                (Type::List(Box::new(Type::Any)), Type::table()),
33            ])
34            .switch("regex", "Use full regex syntax for patterns.", Some('r'))
35            .named(
36                "backtrack",
37                SyntaxShape::Int,
38                "Set the max backtrack limit for regex.",
39                Some('b'),
40            )
41            .allow_variants_without_examples(true)
42            .category(Category::Strings)
43    }
44
45    fn examples(&self) -> Vec<Example<'_>> {
46        vec![
47            Example {
48                description: "Parse a string into two named columns.",
49                example: r#""hi there" | parse "{foo} {bar}""#,
50                result: Some(Value::test_list(vec![Value::test_record(record! {
51                    "foo" => Value::test_string("hi"),
52                    "bar" => Value::test_string("there"),
53                })])),
54            },
55            Example {
56                description: "Parse a string, ignoring a column with _.",
57                example: r#""hello world" | parse "{foo} {_}""#,
58                result: Some(Value::test_list(vec![Value::test_record(record! {
59                    "foo" => Value::test_string("hello"),
60                })])),
61            },
62            Example {
63                description: "This is how the first example is interpreted in the source code.",
64                example: r#""hi there" | parse --regex '(?s)\A(?P<foo>.*?) (?P<bar>.*?)\z'"#,
65                result: Some(Value::test_list(vec![Value::test_record(record! {
66                    "foo" => Value::test_string("hi"),
67                    "bar" => Value::test_string("there"),
68                })])),
69            },
70            Example {
71                description: "Parse a string using fancy-regex named capture group pattern.",
72                example: r#""foo bar." | parse --regex '\s*(?<name>\w+)(?=\.)'"#,
73                result: Some(Value::test_list(vec![Value::test_record(record! {
74                    "name" => Value::test_string("bar"),
75                })])),
76            },
77            Example {
78                description: "Parse a string using fancy-regex capture group pattern.",
79                example: r#""foo! bar." | parse --regex '(\w+)(?=\.)|(\w+)(?=!)'"#,
80                result: Some(Value::test_list(vec![
81                    Value::test_record(record! {
82                        "capture0" => Value::test_nothing(),
83                        "capture1" => Value::test_string("foo"),
84                    }),
85                    Value::test_record(record! {
86                        "capture0" => Value::test_string("bar"),
87                        "capture1" => Value::test_nothing(),
88                    }),
89                ])),
90            },
91            Example {
92                description: "Parse a string using fancy-regex look behind pattern.",
93                example: r#"" @another(foo bar)   " | parse --regex '\s*(?<=[() ])(@\w+)(\([^)]*\))?\s*'"#,
94                result: Some(Value::test_list(vec![Value::test_record(record! {
95                    "capture0" => Value::test_string("@another"),
96                    "capture1" => Value::test_string("(foo bar)"),
97                })])),
98            },
99            Example {
100                description: "Parse a string using fancy-regex look ahead atomic group pattern.",
101                example: r#""abcd" | parse --regex '^a(bc(?=d)|b)cd$'"#,
102                result: Some(Value::test_list(vec![Value::test_record(record! {
103                    "capture0" => Value::test_string("b"),
104                })])),
105            },
106            Example {
107                description: "Parse a string with a manually set fancy-regex backtrack limit.",
108                example: r#""hi there" | parse --backtrack 1500000 "{foo} {bar}""#,
109                result: Some(Value::test_list(vec![Value::test_record(record! {
110                    "foo" => Value::test_string("hi"),
111                    "bar" => Value::test_string("there"),
112                })])),
113            },
114        ]
115    }
116
117    fn is_const(&self) -> bool {
118        true
119    }
120
121    fn run(
122        &self,
123        engine_state: &EngineState,
124        stack: &mut Stack,
125        call: &Call,
126        input: PipelineData,
127    ) -> Result<PipelineData, ShellError> {
128        let pattern: Spanned<String> = call.req(engine_state, stack, 0)?;
129        let regex: bool = call.has_flag(engine_state, stack, "regex")?;
130        let backtrack_limit: usize = call
131            .get_flag(engine_state, stack, "backtrack")?
132            .unwrap_or(1_000_000); // 1_000_000 is fancy_regex default
133        operate(engine_state, pattern, regex, backtrack_limit, call, input)
134    }
135
136    fn run_const(
137        &self,
138        working_set: &StateWorkingSet,
139        call: &Call,
140        input: PipelineData,
141    ) -> Result<PipelineData, ShellError> {
142        let pattern: Spanned<String> = call.req_const(working_set, 0)?;
143        let regex: bool = call.has_flag_const(working_set, "regex")?;
144        let backtrack_limit: usize = call
145            .get_flag_const(working_set, "backtrack")?
146            .unwrap_or(1_000_000);
147        operate(
148            working_set.permanent(),
149            pattern,
150            regex,
151            backtrack_limit,
152            call,
153            input,
154        )
155    }
156}
157
158fn operate(
159    engine_state: &EngineState,
160    pattern: Spanned<String>,
161    regex: bool,
162    backtrack_limit: usize,
163    call: &Call,
164    input: PipelineData,
165) -> Result<PipelineData, ShellError> {
166    let head = call.head;
167
168    let pattern_item = pattern.item;
169    let pattern_span = pattern.span;
170
171    let item_to_parse = if regex {
172        pattern_item
173    } else {
174        build_regex(&pattern_item, pattern_span)?
175    };
176
177    // Default backtrack limit matches fancy_regex / Regex::new, so those
178    // compilations can share the EngineState LRU cache. Custom limits must
179    // bypass the cache because the key is only the pattern string.
180    const DEFAULT_BACKTRACK_LIMIT: usize = 1_000_000;
181    let regex = if backtrack_limit == DEFAULT_BACKTRACK_LIMIT {
182        engine_state.compile_regex(&item_to_parse, pattern_span)?
183    } else {
184        RegexBuilder::new(&item_to_parse)
185            .backtrack_limit(backtrack_limit)
186            .build()
187            .map_err(|e| {
188                nu_protocol::engine::invalid_regex_value(&item_to_parse, e, pattern_span)
189            })?
190    };
191
192    let columns = regex
193        .capture_names()
194        .skip(1)
195        .enumerate()
196        .map(|(i, name)| {
197            name.map(String::from)
198                .unwrap_or_else(|| format!("capture{i}"))
199        })
200        .collect::<Vec<_>>();
201
202    match input {
203        PipelineData::Empty => Ok(PipelineData::empty()),
204        PipelineData::Value(value, ..) => match value {
205            Value::String { val, .. } => {
206                let captures = regex
207                    .captures_iter(val.as_str())
208                    .map(|captures| captures_to_value(captures, &columns, head))
209                    .collect::<Result<_, _>>()?;
210
211                Ok(Value::list(captures, head).into_pipeline_data())
212            }
213            Value::List { vals, .. } => {
214                let iter = vals.into_iter().map(move |val| {
215                    let span = val.span();
216                    let type_ = val.get_type();
217                    val.into_string()
218                        .map_err(|_| ShellError::OnlySupportsThisInputType {
219                            exp_input_type: "string".into(),
220                            wrong_type: type_.to_string(),
221                            dst_span: head,
222                            src_span: span,
223                        })
224                });
225
226                let iter = ParseIter {
227                    captures: VecDeque::new(),
228                    regex,
229                    columns,
230                    iter,
231                    span: head,
232                    signals: engine_state.signals().clone(),
233                };
234
235                Ok(ListStream::new(iter, head, Signals::empty()).into())
236            }
237            value => Err(ShellError::OnlySupportsThisInputType {
238                exp_input_type: "string".into(),
239                wrong_type: value.get_type().to_string(),
240                dst_span: head,
241                src_span: value.span(),
242            }),
243        },
244        PipelineData::ListStream(stream, ..) => Ok(stream
245            .modify(|stream| {
246                let iter = stream.map(move |val| {
247                    let span = val.span();
248                    val.into_string().map_err(|_| ShellError::PipelineMismatch {
249                        exp_input_type: "string".into(),
250                        dst_span: head,
251                        src_span: span,
252                    })
253                });
254
255                ParseIter {
256                    captures: VecDeque::new(),
257                    regex,
258                    columns,
259                    iter,
260                    span: head,
261                    signals: engine_state.signals().clone(),
262                }
263            })
264            .into()),
265        PipelineData::ByteStream(stream, ..) => {
266            let val = stream.into_string()?;
267
268            let captures = regex
269                .captures_iter(val.as_str())
270                .map(|captures| captures_to_value(captures, &columns, head))
271                .collect::<Result<_, _>>()?;
272
273            Ok(Value::list(captures, head).into_pipeline_data())
274        }
275    }
276}
277
278fn build_regex(input: &str, span: Span) -> Result<String, ShellError> {
279    let mut output = r#"(?s)\A"#.to_string();
280
281    // Single-pass scanner keeps parsing state explicit and avoids byte-offset bookkeeping.
282    let mut loop_input = input.char_indices().peekable();
283    let mut before = String::new();
284    let mut column = String::new();
285    let mut in_column = false;
286
287    while let Some((_, c)) = loop_input.next() {
288        if !in_column {
289            if c == '{' {
290                // If '{{', still creating a plaintext parse command, but just for a single '{' char.
291                let mut literal_lbrace = false;
292                if let Some((next_idx, '{')) = loop_input.peek().copied() {
293                    // Don't consume the second `{` if it starts a trailing capture like `{{name}`.
294                    let after = &input[next_idx + 1..];
295                    literal_lbrace = true;
296
297                    if !is_trailing_capture(after) {
298                        loop_input.next();
299                    }
300                }
301
302                if literal_lbrace {
303                    before.push(c);
304                    continue;
305                }
306
307                if !before.is_empty() {
308                    output.push_str(&fancy_regex::escape(&before));
309                    before.clear();
310                }
311
312                in_column = true;
313                continue;
314            }
315
316            before.push(c);
317            continue;
318        }
319
320        if c == '}' {
321            if !column.is_empty() {
322                output.push_str("(?");
323                if column == "_" {
324                    // discard placeholder column(s)
325                    output.push(':');
326                } else {
327                    // create capture group for column
328                    output.push_str("P<");
329                    output.push_str(&column);
330                    output.push('>');
331                }
332                output.push_str(".*?)");
333                column.clear();
334            }
335
336            in_column = false;
337            continue;
338        }
339
340        column.push(c);
341        if loop_input.peek().is_none() {
342            return Err(ShellError::DelimiterError {
343                msg: "Found opening `{` without an associated closing `}`".to_owned(),
344                span,
345            });
346        }
347    }
348
349    if !before.is_empty() {
350        output.push_str(&fancy_regex::escape(&before));
351    }
352
353    output.push_str(r#"\z"#);
354    Ok(output)
355}
356
357/// Returns true when the remainder after the second `{` in `{{` forms a trailing capture.
358///
359/// For example, this returns true for `name}` in `{{name}` and false for `name}x{tail}`.
360fn is_trailing_capture(after: &str) -> bool {
361    after
362        .find(['}', '{'])
363        .is_some_and(|pos| after.as_bytes()[pos] == b'}' && pos + 1 == after.len())
364}
365
366struct ParseIter<I: Iterator<Item = Result<String, ShellError>>> {
367    captures: VecDeque<Value>,
368    regex: Regex,
369    columns: Vec<String>,
370    iter: I,
371    span: Span,
372    signals: Signals,
373}
374
375impl<I: Iterator<Item = Result<String, ShellError>>> ParseIter<I> {
376    fn populate_captures(&mut self, str: &str) -> Result<(), ShellError> {
377        for captures in self.regex.captures_iter(str) {
378            self.captures
379                .push_back(captures_to_value(captures, &self.columns, self.span)?);
380        }
381        Ok(())
382    }
383}
384
385impl<I: Iterator<Item = Result<String, ShellError>>> Iterator for ParseIter<I> {
386    type Item = Value;
387
388    fn next(&mut self) -> Option<Value> {
389        loop {
390            if self.signals.interrupted() {
391                return None;
392            }
393
394            if let Some(val) = self.captures.pop_front() {
395                return Some(val);
396            }
397
398            let result = self
399                .iter
400                .next()?
401                .and_then(|str| self.populate_captures(&str));
402
403            if let Err(err) = result {
404                return Some(Value::error(err, self.span));
405            }
406        }
407    }
408}
409
410fn captures_to_value(
411    captures: Result<Captures<'_, str>, fancy_regex::Error>,
412    columns: &[String],
413    span: Span,
414) -> Result<Value, ShellError> {
415    let captures = captures.map_err(|err| {
416        ShellError::Generic(GenericError::new(
417            "Error with regular expression captures",
418            err.to_string(),
419            span,
420        ))
421    })?;
422
423    let record = columns
424        .iter()
425        .zip(captures.iter().skip(1))
426        .map(|(column, match_)| {
427            let match_value = match_
428                .map(|m| Value::string(m.as_str(), span))
429                .unwrap_or(Value::nothing(span));
430            (column.clone(), match_value)
431        })
432        .collect();
433
434    Ok(Value::record(record, span))
435}
436
437#[cfg(test)]
438mod test {
439    use super::*;
440
441    #[test]
442    fn test_examples() -> nu_test_support::Result {
443        nu_test_support::test().examples(Parse)
444    }
445}