From 2bed202b8292d32c8e2e88df03b9387f549a63b7 Mon Sep 17 00:00:00 2001 From: nuggetcrab Date: Thu, 19 Jun 2025 06:42:30 -0500 Subject: [PATCH] Add backtrack named flag to parse (issue #15997) (#16000) # Description Addresses #15997 Adds a `--backtrack` or `-b` named flag to the `parse` command. Allows a user to specify a max backtrack limit for fancy-regex other than the default 1,000,000 limit. Uses a RegexBuilder to add the manual config. # User-Facing Changes Adds a new named flag `backtrack` to the `parse` command. The flag is optional and defaults to 1,000,000. # Tests + Formatting Added an example test to the parse command using `--backtrack 1500000`. --- crates/nu-command/src/strings/parse.rs | 51 +++++++++++++++++++++----- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/crates/nu-command/src/strings/parse.rs b/crates/nu-command/src/strings/parse.rs index 80e7e3f6e9..04ac6d9781 100644 --- a/crates/nu-command/src/strings/parse.rs +++ b/crates/nu-command/src/strings/parse.rs @@ -1,4 +1,4 @@ -use fancy_regex::{Captures, Regex}; +use fancy_regex::{Captures, Regex, RegexBuilder}; use nu_engine::command_prelude::*; use nu_protocol::{ListStream, Signals, engine::StateWorkingSet}; use std::collections::VecDeque; @@ -31,6 +31,12 @@ impl Command for Parse { (Type::List(Box::new(Type::Any)), Type::table()), ]) .switch("regex", "use full regex syntax for patterns", Some('r')) + .named( + "backtrack", + SyntaxShape::Int, + "set the max backtrack limit for regex", + Some('b'), + ) .allow_variants_without_examples(true) .category(Category::Strings) } @@ -96,6 +102,14 @@ impl Command for Parse { "capture0" => Value::test_string("b"), })])), }, + Example { + description: "Parse a string with a manually set fancy-regex backtrack limit", + example: "\"hi there\" | parse --backtrack 1500000 \"{foo} {bar}\"", + result: Some(Value::test_list(vec![Value::test_record(record! { + "foo" => Value::test_string("hi"), + "bar" => Value::test_string("there"), + })])), + }, ] } @@ -112,7 +126,10 @@ impl Command for Parse { ) -> Result { let pattern: Spanned = call.req(engine_state, stack, 0)?; let regex: bool = call.has_flag(engine_state, stack, "regex")?; - operate(engine_state, pattern, regex, call, input) + let backtrack_limit: usize = call + .get_flag(engine_state, stack, "backtrack")? + .unwrap_or(1_000_000); // 1_000_000 is fancy_regex default + operate(engine_state, pattern, regex, backtrack_limit, call, input) } fn run_const( @@ -123,7 +140,17 @@ impl Command for Parse { ) -> Result { let pattern: Spanned = call.req_const(working_set, 0)?; let regex: bool = call.has_flag_const(working_set, "regex")?; - operate(working_set.permanent(), pattern, regex, call, input) + let backtrack_limit: usize = call + .get_flag_const(working_set, "backtrack")? + .unwrap_or(1_000_000); + operate( + working_set.permanent(), + pattern, + regex, + backtrack_limit, + call, + input, + ) } } @@ -131,6 +158,7 @@ fn operate( engine_state: &EngineState, pattern: Spanned, regex: bool, + backtrack_limit: usize, call: &Call, input: PipelineData, ) -> Result { @@ -145,13 +173,16 @@ fn operate( build_regex(&pattern_item, pattern_span)? }; - let regex = Regex::new(&item_to_parse).map_err(|e| ShellError::GenericError { - error: "Error with regular expression".into(), - msg: e.to_string(), - span: Some(pattern_span), - help: None, - inner: vec![], - })?; + let regex = RegexBuilder::new(&item_to_parse) + .backtrack_limit(backtrack_limit) + .build() + .map_err(|e| ShellError::GenericError { + error: "Error with regular expression".into(), + msg: e.to_string(), + span: Some(pattern_span), + help: None, + inner: vec![], + })?; let columns = regex .capture_names()