Change the behavior of --ignore-case and --multiline options for find (#16323)

# Description

Changes the behavior of `--ignore-case` and `--multiline` options for
`find`, to make them more consistent between regex mode and search term
mode, and to enable more options for using find.

# User-Facing Changes

Search term mode is now case-sensitive by default.

`--ignore-case` will make the search case-insensitive in search term
mode. In regex mode, the previous behavior of adding a (?i) flag to the
regex is preserved.

`--multiline` will no longer add a (?m) flag in regex mode. Instead, it
will make the search not split multi-line strings into lists of lines.

closes #16317
closes #16022
This commit is contained in:
new-years-eve
2025-08-04 16:27:00 +02:00
committed by GitHub
parent d528bb713b
commit 4f9c0775d9
3 changed files with 109 additions and 51 deletions

View File

@ -31,12 +31,12 @@ impl Command for Find {
)
.switch(
"ignore-case",
"case-insensitive regex mode; equivalent to (?i)",
"case-insensitive; when in regex mode, this is equivalent to (?i)",
Some('i'),
)
.switch(
"multiline",
"multi-line regex mode: ^ and $ match begin/end of line; equivalent to (?m)",
"don't split multi-line strings into lists of lines. you should use this option when using the (?m) or (?s) flags in regex mode",
Some('m'),
)
.switch(
@ -72,8 +72,8 @@ impl Command for Find {
result: None,
},
Example {
description: "Search and highlight text for a term in a string. Note that regular search is case insensitive",
example: r#"'Cargo.toml' | find cargo"#,
description: "Search and highlight text for a term in a string.",
example: r#"'Cargo.toml' | find Cargo"#,
result: Some(Value::test_string(
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mCargo\u{1b}[0m\u{1b}[37m.toml\u{1b}[0m"
.to_owned(),
@ -81,7 +81,7 @@ impl Command for Find {
},
Example {
description: "Search a number or a file size in a list of numbers",
example: r#"[1 5 3kb 4 3Mb] | find 5 3kb"#,
example: r#"[1 5 3kb 4 35 3Mb] | find 5 3kb"#,
result: Some(Value::list(
vec![Value::test_int(5), Value::test_filesize(3000)],
Span::test_data(),
@ -103,16 +103,16 @@ impl Command for Find {
)),
},
Example {
description: "Find using regex",
example: r#"[abc bde arc abf] | find --regex "ab""#,
description: "Search using regex",
example: r#"[abc odb arc abf] | find --regex "b.""#,
result: Some(Value::list(
vec![
Value::test_string(
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mab\u{1b}[0m\u{1b}[37mc\u{1b}[0m"
"\u{1b}[37ma\u{1b}[0m\u{1b}[41;37mbc\u{1b}[0m\u{1b}[37m\u{1b}[0m"
.to_string(),
),
Value::test_string(
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mab\u{1b}[0m\u{1b}[37mf\u{1b}[0m"
"\u{1b}[37ma\u{1b}[0m\u{1b}[41;37mbf\u{1b}[0m\u{1b}[37m\u{1b}[0m"
.to_string(),
),
],
@ -120,8 +120,8 @@ impl Command for Find {
)),
},
Example {
description: "Find using regex case insensitive",
example: r#"[aBc bde Arc abf] | find --regex "ab" -i"#,
description: "Case insensitive search",
example: r#"[aBc bde Arc abf] | find "ab" -i"#,
result: Some(Value::list(
vec![
Value::test_string(
@ -211,11 +211,33 @@ impl Command for Find {
Span::test_data(),
)),
},
Example {
description: "Find in a multi-line string",
example: r#""Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue" | find "ue""#,
result: Some(Value::list(
vec![
Value::test_string(
"\u{1b}[37mAnd roses are bl\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
),
Value::test_string(
"\u{1b}[37mAlter their h\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
),
],
Span::test_data(),
)),
},
Example {
description: "Find in a multi-line string without splitting the input into a list of lines",
example: r#""Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue" | find --multiline "ue""#,
result: Some(Value::test_string(
"\u{1b}[37mViolets are red\nAnd roses are bl\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\nWhen metamaterials\nAlter their h\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
)),
},
]
}
fn search_terms(&self) -> Vec<&str> {
vec!["filter", "regex", "search", "condition"]
vec!["filter", "regex", "search", "condition", "grep"]
}
fn run(
@ -227,11 +249,25 @@ impl Command for Find {
) -> Result<PipelineData, ShellError> {
let pattern = get_match_pattern_from_arguments(engine_state, stack, call)?;
let multiline = call.has_flag(engine_state, stack, "multiline")?;
let columns_to_search: Vec<_> = call
.get_flag(engine_state, stack, "columns")?
.unwrap_or_default();
let input = split_string_if_multiline(input, call.head);
let input = if multiline {
if let PipelineData::ByteStream(..) = input {
// ByteStream inputs are processed by iterating over the lines, which necessarily
// breaks the multi-line text being streamed into a list of lines.
return Err(ShellError::IncompatibleParametersSingle {
msg: "Flag `--multiline` currently doesn't work for byte stream inputs. Consider using `collect`".into(),
span: call.get_flag_span(stack, "multiline").expect("has flag"),
});
};
input
} else {
split_string_if_multiline(input, call.head)
};
find_in_pipelinedata(pattern, columns_to_search, engine_state, stack, input)
}
@ -242,8 +278,11 @@ struct MatchPattern {
/// the regex to be used for matching in text
regex: Regex,
/// the list of match terms converted to lowercase strings, or empty if a regex was provided
lower_terms: Vec<String>,
/// the list of match terms (converted to lowercase if needed), or empty if a regex was provided
search_terms: Vec<String>,
/// case-insensitive match
ignore_case: bool,
/// return a modified version of the value where matching parts are highlighted
highlight: bool,
@ -272,6 +311,10 @@ fn get_match_pattern_from_arguments(
let invert = call.has_flag(engine_state, stack, "invert")?;
let highlight = !call.has_flag(engine_state, stack, "no-highlight")?;
let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
let dotall = call.has_flag(engine_state, stack, "dotall")?;
let style_computer = StyleComputer::from_config(engine_state, stack);
// Currently, search results all use the same style.
// Also note that this sample string is passed into user-written code (the closure that may or may not be
@ -280,7 +323,7 @@ fn get_match_pattern_from_arguments(
let highlight_style =
style_computer.compute("search_result", &Value::string("search result", span));
let (regex_str, lower_terms) = if let Some(regex) = regex {
let (regex_str, search_terms) = if let Some(regex) = regex {
if !terms.is_empty() {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Cannot use a `--regex` parameter with additional search terms".into(),
@ -288,47 +331,54 @@ fn get_match_pattern_from_arguments(
});
}
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
let multiline = call.has_flag(engine_state, stack, "multiline")?;
let dotall = call.has_flag(engine_state, stack, "dotall")?;
let flags = match (insensitive, multiline, dotall) {
(false, false, false) => "",
(true, false, false) => "(?i)", // case insensitive
(false, true, false) => "(?m)", // multi-line mode
(false, false, true) => "(?s)", // allow . to match \n
(true, true, false) => "(?im)", // case insensitive and multi-line mode
(true, false, true) => "(?is)", // case insensitive and allow . to match \n
(false, true, true) => "(?ms)", // multi-line mode and allow . to match \n
(true, true, true) => "(?ims)", // case insensitive, multi-line mode and allow . to match \n
let flags = match (ignore_case, dotall) {
(false, false) => "",
(true, false) => "(?i)", // case insensitive
(false, true) => "(?s)", // allow . to match \n
(true, true) => "(?is)", // case insensitive and allow . to match \n
};
(flags.to_string() + regex.as_str(), Vec::new())
} else {
if dotall {
return Err(ShellError::IncompatibleParametersSingle {
msg: "Flag --dotall only works for regex search".into(),
span: call.get_flag_span(stack, "dotall").expect("has flag"),
});
}
let mut regex = String::new();
regex += "(?i)";
if ignore_case {
regex += "(?i)";
}
let lower_terms = terms
let search_terms = terms
.iter()
.map(|v| escape(&v.to_expanded_string("", &config).to_lowercase()).into())
.map(|v| {
if ignore_case {
v.to_expanded_string("", &config).to_lowercase()
} else {
v.to_expanded_string("", &config)
}
})
.collect::<Vec<String>>();
if let Some(term) = lower_terms.first() {
let escaped_terms = search_terms
.iter()
.map(|v| escape(v).into())
.collect::<Vec<String>>();
if let Some(term) = escaped_terms.first() {
regex += term;
}
for term in lower_terms.iter().skip(1) {
for term in escaped_terms.iter().skip(1) {
regex += "|";
regex += term;
}
let lower_terms = terms
.iter()
.map(|v| v.to_expanded_string("", &config).to_lowercase())
.collect::<Vec<String>>();
(regex, lower_terms)
(regex, search_terms)
};
let regex = Regex::new(regex_str.as_str()).map_err(|e| ShellError::TypeMismatch {
@ -338,7 +388,8 @@ fn get_match_pattern_from_arguments(
Ok(MatchPattern {
regex,
lower_terms,
search_terms,
ignore_case,
invert,
highlight,
string_style,
@ -507,7 +558,11 @@ fn value_should_be_printed(
columns_to_search: &[String],
config: &Config,
) -> bool {
let lower_value = value.to_expanded_string("", config).to_lowercase();
let value_as_string = if pattern.ignore_case {
value.to_expanded_string("", config).to_lowercase()
} else {
value.to_expanded_string("", config)
};
match value {
Value::Bool { .. }
@ -519,18 +574,18 @@ fn value_should_be_printed(
| Value::Float { .. }
| Value::Closure { .. }
| Value::Nothing { .. } => {
if !pattern.lower_terms.is_empty() {
if !pattern.search_terms.is_empty() {
// look for exact match when searching with terms
pattern
.lower_terms
.search_terms
.iter()
.any(|term: &String| term == &lower_value)
.any(|term: &String| term == &value_as_string)
} else {
string_should_be_printed(pattern, &lower_value)
string_should_be_printed(pattern, &value_as_string)
}
}
Value::Glob { .. } | Value::CellPath { .. } | Value::Custom { .. } => {
string_should_be_printed(pattern, &lower_value)
string_should_be_printed(pattern, &value_as_string)
}
Value::String { val, .. } => string_should_be_printed(pattern, val),
Value::List { vals, .. } => vals
@ -597,7 +652,8 @@ pub fn find_internal(
let pattern = MatchPattern {
regex,
lower_terms: vec![search_term.to_lowercase()],
search_terms: vec![search_term.to_lowercase()],
ignore_case: true,
highlight,
invert: false,
string_style,

View File

@ -26,10 +26,11 @@ fn find_with_list_search_with_char() {
#[test]
fn find_with_bytestream_search_with_char() {
let actual =
nu!("\"ABC\" | save foo.txt; let res = open foo.txt | find abc; rm foo.txt; $res | get 0");
let actual = nu!(
"\"ABC\" | save foo.txt; let res = open foo.txt | find -i abc; rm foo.txt; $res | get 0"
);
let actual_no_highlight = nu!(
"\"ABC\" | save foo.txt; let res = open foo.txt | find --no-highlight abc; rm foo.txt; $res | get 0"
"\"ABC\" | save foo.txt; let res = open foo.txt | find -i --no-highlight abc; rm foo.txt; $res | get 0"
);
assert_eq!(