mirror of
https://github.com/nushell/nushell.git
synced 2025-08-10 14:59:16 +02:00
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:
@ -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,
|
||||
|
@ -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!(
|
||||
|
@ -19,6 +19,7 @@ extend-ignore-re = [
|
||||
"0x\\[ba be\\]",
|
||||
"\\)BaR'",
|
||||
"fo<66>.txt",
|
||||
"ue",
|
||||
]
|
||||
|
||||
[type.rust.extend-words]
|
||||
|
Reference in New Issue
Block a user