From 3db9c81958a257a31ac546ef422e3a50e4001c82 Mon Sep 17 00:00:00 2001 From: new-years-eve Date: Mon, 16 Jun 2025 22:29:41 +0200 Subject: [PATCH] Search nested structures recursively in `find` command (#15850) # Description Instead of converting nested structures into strings and pattern-matching the strings, the `find` command will recursively search the nested structures for matches. - fixes #15618 # User-Facing Changes Text in nested structures will now be highlighted as well. Error values will always passed on instead of testing them against the search term There will be slight changes in match behavior, such as characters that are part of the string representations of data structures no longer matching all nested data structures. --- crates/nu-command/src/filters/find.rs | 82 ++++++++++++------------ crates/nu-command/tests/commands/find.rs | 17 +++++ 2 files changed, 57 insertions(+), 42 deletions(-) diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index 46b1d74b06..02728ee3c8 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -163,7 +163,12 @@ impl Command for Find { example: r#"[["Larry", "Moe"], ["Victor", "Marina"]] | find --regex "rr""#, result: Some(Value::list( vec![Value::list( - vec![Value::test_string("Larry"), Value::test_string("Moe")], + vec![ + Value::test_string( + "\u{1b}[37mLa\u{1b}[0m\u{1b}[41;37mrr\u{1b}[0m\u{1b}[37my\u{1b}[0m", + ), + Value::test_string("Moe"), + ], Span::test_data(), )], Span::test_data(), @@ -344,7 +349,10 @@ fn get_match_pattern_from_arguments( // map functions fn highlight_matches_in_string(pattern: &MatchPattern, val: String) -> String { - // strip haystack to remove existing ansi style + if !pattern.regex.is_match(&val).unwrap_or(false) { + return val; + } + let stripped_val = nu_utils::strip_ansi_string_unlikely(val); let mut last_match_end = 0; let mut highlighted = String::new(); @@ -390,7 +398,7 @@ fn highlight_matches_in_string(pattern: &MatchPattern, val: String) -> String { highlighted } -fn highlight_matches_in_record_or_value( +fn highlight_matches_in_value( pattern: &MatchPattern, value: Value, columns_to_search: &[String], @@ -412,16 +420,16 @@ fn highlight_matches_in_record_or_value( continue; } - if let Value::String { val: val_str, .. } = val { - if pattern.regex.is_match(val_str).unwrap_or(false) { - let val_str = std::mem::take(val_str); - *val = highlight_matches_in_string(pattern, val_str).into_value(span) - } - } + *val = highlight_matches_in_value(pattern, std::mem::take(val), &[]); } Value::record(record, span) } + Value::List { vals, .. } => vals + .into_iter() + .map(|item| highlight_matches_in_value(pattern, item, &[])) + .collect::>() + .into_value(span), Value::String { val, .. } => highlight_matches_in_string(pattern, val).into_value(span), _ => value, } @@ -444,24 +452,22 @@ fn find_in_pipelinedata( PipelineData::Value(_, _) => input .filter( move |value| { - record_or_value_should_be_printed(&pattern, value, &columns_to_search, &config) + value_should_be_printed(&pattern, value, &columns_to_search, &config) + != pattern.invert }, engine_state.signals(), )? .map( - move |x| { - highlight_matches_in_record_or_value(&map_pattern, x, &map_columns_to_search) - }, + move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search), engine_state.signals(), ), PipelineData::ListStream(stream, metadata) => { let stream = stream.modify(|iter| { iter.filter(move |value| { - record_or_value_should_be_printed(&pattern, value, &columns_to_search, &config) - }) - .map(move |x| { - highlight_matches_in_record_or_value(&map_pattern, x, &map_columns_to_search) + value_should_be_printed(&pattern, value, &columns_to_search, &config) + != pattern.invert }) + .map(move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search)) }); Ok(PipelineData::ListStream(stream, metadata)) @@ -495,7 +501,12 @@ fn string_should_be_printed(pattern: &MatchPattern, value: &str) -> bool { pattern.regex.is_match(value).unwrap_or(false) } -fn value_should_be_printed(pattern: &MatchPattern, value: &Value, config: &Config) -> bool { +fn value_should_be_printed( + pattern: &MatchPattern, + value: &Value, + columns_to_search: &[String], + config: &Config, +) -> bool { let lower_value = value.to_expanded_string("", config).to_lowercase(); match value { @@ -507,8 +518,7 @@ fn value_should_be_printed(pattern: &MatchPattern, value: &Value, config: &Confi | Value::Range { .. } | Value::Float { .. } | Value::Closure { .. } - | Value::Nothing { .. } - | Value::Error { .. } => { + | Value::Nothing { .. } => { if !pattern.lower_terms.is_empty() { // look for exact match when searching with terms pattern @@ -519,37 +529,25 @@ fn value_should_be_printed(pattern: &MatchPattern, value: &Value, config: &Confi string_should_be_printed(pattern, &lower_value) } } - Value::Glob { .. } - | Value::List { .. } - | Value::CellPath { .. } - | Value::Record { .. } - | Value::Custom { .. } => string_should_be_printed(pattern, &lower_value), + Value::Glob { .. } | Value::CellPath { .. } | Value::Custom { .. } => { + string_should_be_printed(pattern, &lower_value) + } Value::String { val, .. } => string_should_be_printed(pattern, val), - Value::Binary { .. } => false, - } -} - -fn record_or_value_should_be_printed( - pattern: &MatchPattern, - value: &Value, - columns_to_search: &[String], - config: &Config, -) -> bool { - let match_found = match value { + Value::List { vals, .. } => vals + .iter() + .any(|item| value_should_be_printed(pattern, item, &[], config)), Value::Record { val: record, .. } => { - // Only perform column selection if given columns. let col_select = !columns_to_search.is_empty(); record.iter().any(|(col, val)| { if col_select && !columns_to_search.contains(col) { return false; } - value_should_be_printed(pattern, val, config) + value_should_be_printed(pattern, val, &[], config) }) } - _ => value_should_be_printed(pattern, value, config), - }; - - match_found != pattern.invert + Value::Binary { .. } => false, + Value::Error { .. } => true, + } } // utility diff --git a/crates/nu-command/tests/commands/find.rs b/crates/nu-command/tests/commands/find.rs index d428e61c00..547fab65bf 100644 --- a/crates/nu-command/tests/commands/find.rs +++ b/crates/nu-command/tests/commands/find.rs @@ -293,3 +293,20 @@ fn find_with_string_search_with_special_char_6() { ); assert_eq!(actual_no_highlight.out, "[{\"d\":\"a[]b\"}]"); } + +#[test] +fn find_in_nested_list_dont_match_bracket() { + let actual = nu!(r#"[ [foo bar] [foo baz] ] | find "[" | to json -r"#); + + assert_eq!(actual.out, "[]"); +} + +#[test] +fn find_and_highlight_in_nested_list() { + let actual = nu!(r#"[ [foo bar] [foo baz] ] | find "foo" | to json -r"#); + + assert_eq!( + actual.out, + r#"[["\u001b[37m\u001b[0m\u001b[41;37mfoo\u001b[0m\u001b[37m\u001b[0m","bar"],["\u001b[37m\u001b[0m\u001b[41;37mfoo\u001b[0m\u001b[37m\u001b[0m","baz"]]"# + ); +}