diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index 8ac3a96579..b7d50b7756 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -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 { 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, + /// the list of match terms (converted to lowercase if needed), or empty if a regex was provided + search_terms: Vec, + + /// 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::>(); - if let Some(term) = lower_terms.first() { + let escaped_terms = search_terms + .iter() + .map(|v| escape(v).into()) + .collect::>(); + + 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::>(); - - (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, diff --git a/crates/nu-command/tests/commands/find.rs b/crates/nu-command/tests/commands/find.rs index 547fab65bf..2448e3ac0c 100644 --- a/crates/nu-command/tests/commands/find.rs +++ b/crates/nu-command/tests/commands/find.rs @@ -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!( diff --git a/typos.toml b/typos.toml index 76d8b8959a..dcf591566f 100644 --- a/typos.toml +++ b/typos.toml @@ -19,6 +19,7 @@ extend-ignore-re = [ "0x\\[ba be\\]", "\\)BaR'", "fo�.txt", + "ue", ] [type.rust.extend-words]