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.
This commit is contained in:
new-years-eve 2025-06-16 22:29:41 +02:00 committed by GitHub
parent 55240d98a5
commit 3db9c81958
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 57 additions and 42 deletions

View File

@ -163,7 +163,12 @@ impl Command for Find {
example: r#"[["Larry", "Moe"], ["Victor", "Marina"]] | find --regex "rr""#, example: r#"[["Larry", "Moe"], ["Victor", "Marina"]] | find --regex "rr""#,
result: Some(Value::list( result: Some(Value::list(
vec![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(),
)], )],
Span::test_data(), Span::test_data(),
@ -344,7 +349,10 @@ fn get_match_pattern_from_arguments(
// map functions // map functions
fn highlight_matches_in_string(pattern: &MatchPattern, val: String) -> String { 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 stripped_val = nu_utils::strip_ansi_string_unlikely(val);
let mut last_match_end = 0; let mut last_match_end = 0;
let mut highlighted = String::new(); let mut highlighted = String::new();
@ -390,7 +398,7 @@ fn highlight_matches_in_string(pattern: &MatchPattern, val: String) -> String {
highlighted highlighted
} }
fn highlight_matches_in_record_or_value( fn highlight_matches_in_value(
pattern: &MatchPattern, pattern: &MatchPattern,
value: Value, value: Value,
columns_to_search: &[String], columns_to_search: &[String],
@ -412,16 +420,16 @@ fn highlight_matches_in_record_or_value(
continue; continue;
} }
if let Value::String { val: val_str, .. } = val { *val = highlight_matches_in_value(pattern, std::mem::take(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)
}
}
} }
Value::record(record, span) Value::record(record, span)
} }
Value::List { vals, .. } => vals
.into_iter()
.map(|item| highlight_matches_in_value(pattern, item, &[]))
.collect::<Vec<Value>>()
.into_value(span),
Value::String { val, .. } => highlight_matches_in_string(pattern, val).into_value(span), Value::String { val, .. } => highlight_matches_in_string(pattern, val).into_value(span),
_ => value, _ => value,
} }
@ -444,24 +452,22 @@ fn find_in_pipelinedata(
PipelineData::Value(_, _) => input PipelineData::Value(_, _) => input
.filter( .filter(
move |value| { 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(), engine_state.signals(),
)? )?
.map( .map(
move |x| { move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search),
highlight_matches_in_record_or_value(&map_pattern, x, &map_columns_to_search)
},
engine_state.signals(), engine_state.signals(),
), ),
PipelineData::ListStream(stream, metadata) => { PipelineData::ListStream(stream, metadata) => {
let stream = stream.modify(|iter| { let stream = stream.modify(|iter| {
iter.filter(move |value| { iter.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
.map(move |x| {
highlight_matches_in_record_or_value(&map_pattern, x, &map_columns_to_search)
}) })
.map(move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search))
}); });
Ok(PipelineData::ListStream(stream, metadata)) 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) 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(); let lower_value = value.to_expanded_string("", config).to_lowercase();
match value { match value {
@ -507,8 +518,7 @@ fn value_should_be_printed(pattern: &MatchPattern, value: &Value, config: &Confi
| Value::Range { .. } | Value::Range { .. }
| Value::Float { .. } | Value::Float { .. }
| Value::Closure { .. } | Value::Closure { .. }
| Value::Nothing { .. } | Value::Nothing { .. } => {
| Value::Error { .. } => {
if !pattern.lower_terms.is_empty() { if !pattern.lower_terms.is_empty() {
// look for exact match when searching with terms // look for exact match when searching with terms
pattern pattern
@ -519,37 +529,25 @@ fn value_should_be_printed(pattern: &MatchPattern, value: &Value, config: &Confi
string_should_be_printed(pattern, &lower_value) string_should_be_printed(pattern, &lower_value)
} }
} }
Value::Glob { .. } Value::Glob { .. } | Value::CellPath { .. } | Value::Custom { .. } => {
| Value::List { .. } string_should_be_printed(pattern, &lower_value)
| Value::CellPath { .. }
| Value::Record { .. }
| Value::Custom { .. } => string_should_be_printed(pattern, &lower_value),
Value::String { val, .. } => string_should_be_printed(pattern, val),
Value::Binary { .. } => false,
} }
} Value::String { val, .. } => string_should_be_printed(pattern, val),
Value::List { vals, .. } => vals
fn record_or_value_should_be_printed( .iter()
pattern: &MatchPattern, .any(|item| value_should_be_printed(pattern, item, &[], config)),
value: &Value,
columns_to_search: &[String],
config: &Config,
) -> bool {
let match_found = match value {
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
// Only perform column selection if given columns.
let col_select = !columns_to_search.is_empty(); let col_select = !columns_to_search.is_empty();
record.iter().any(|(col, val)| { record.iter().any(|(col, val)| {
if col_select && !columns_to_search.contains(col) { if col_select && !columns_to_search.contains(col) {
return false; return false;
} }
value_should_be_printed(pattern, val, config) value_should_be_printed(pattern, val, &[], config)
}) })
} }
_ => value_should_be_printed(pattern, value, config), Value::Binary { .. } => false,
}; Value::Error { .. } => true,
}
match_found != pattern.invert
} }
// utility // utility

View File

@ -293,3 +293,20 @@ fn find_with_string_search_with_special_char_6() {
); );
assert_eq!(actual_no_highlight.out, "[{\"d\":\"a[]b\"}]"); 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"]]"#
);
}