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); 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 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 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 let mut literal_lbrace = false;
292 if let Some((next_idx, '{')) = loop_input.peek().copied() {
293 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 output.push(':');
326 } else {
327 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
357fn 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}