Skip to content

Commit 4f9c077

Browse files
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
1 parent d528bb7 commit 4f9c077

File tree

3 files changed

+109
-51
lines changed

3 files changed

+109
-51
lines changed

crates/nu-command/src/filters/find.rs

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ impl Command for Find {
3131
)
3232
.switch(
3333
"ignore-case",
34-
"case-insensitive regex mode; equivalent to (?i)",
34+
"case-insensitive; when in regex mode, this is equivalent to (?i)",
3535
Some('i'),
3636
)
3737
.switch(
3838
"multiline",
39-
"multi-line regex mode: ^ and $ match begin/end of line; equivalent to (?m)",
39+
"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",
4040
Some('m'),
4141
)
4242
.switch(
@@ -72,16 +72,16 @@ impl Command for Find {
7272
result: None,
7373
},
7474
Example {
75-
description: "Search and highlight text for a term in a string. Note that regular search is case insensitive",
76-
example: r#"'Cargo.toml' | find cargo"#,
75+
description: "Search and highlight text for a term in a string.",
76+
example: r#"'Cargo.toml' | find Cargo"#,
7777
result: Some(Value::test_string(
7878
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mCargo\u{1b}[0m\u{1b}[37m.toml\u{1b}[0m"
7979
.to_owned(),
8080
)),
8181
},
8282
Example {
8383
description: "Search a number or a file size in a list of numbers",
84-
example: r#"[1 5 3kb 4 3Mb] | find 5 3kb"#,
84+
example: r#"[1 5 3kb 4 35 3Mb] | find 5 3kb"#,
8585
result: Some(Value::list(
8686
vec![Value::test_int(5), Value::test_filesize(3000)],
8787
Span::test_data(),
@@ -103,25 +103,25 @@ impl Command for Find {
103103
)),
104104
},
105105
Example {
106-
description: "Find using regex",
107-
example: r#"[abc bde arc abf] | find --regex "ab""#,
106+
description: "Search using regex",
107+
example: r#"[abc odb arc abf] | find --regex "b.""#,
108108
result: Some(Value::list(
109109
vec![
110110
Value::test_string(
111-
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mab\u{1b}[0m\u{1b}[37mc\u{1b}[0m"
111+
"\u{1b}[37ma\u{1b}[0m\u{1b}[41;37mbc\u{1b}[0m\u{1b}[37m\u{1b}[0m"
112112
.to_string(),
113113
),
114114
Value::test_string(
115-
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mab\u{1b}[0m\u{1b}[37mf\u{1b}[0m"
115+
"\u{1b}[37ma\u{1b}[0m\u{1b}[41;37mbf\u{1b}[0m\u{1b}[37m\u{1b}[0m"
116116
.to_string(),
117117
),
118118
],
119119
Span::test_data(),
120120
)),
121121
},
122122
Example {
123-
description: "Find using regex case insensitive",
124-
example: r#"[aBc bde Arc abf] | find --regex "ab" -i"#,
123+
description: "Case insensitive search",
124+
example: r#"[aBc bde Arc abf] | find "ab" -i"#,
125125
result: Some(Value::list(
126126
vec![
127127
Value::test_string(
@@ -211,11 +211,33 @@ impl Command for Find {
211211
Span::test_data(),
212212
)),
213213
},
214+
Example {
215+
description: "Find in a multi-line string",
216+
example: r#""Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue" | find "ue""#,
217+
result: Some(Value::list(
218+
vec![
219+
Value::test_string(
220+
"\u{1b}[37mAnd roses are bl\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
221+
),
222+
Value::test_string(
223+
"\u{1b}[37mAlter their h\u{1b}[0m\u{1b}[41;37mue\u{1b}[0m\u{1b}[37m\u{1b}[0m",
224+
),
225+
],
226+
Span::test_data(),
227+
)),
228+
},
229+
Example {
230+
description: "Find in a multi-line string without splitting the input into a list of lines",
231+
example: r#""Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue" | find --multiline "ue""#,
232+
result: Some(Value::test_string(
233+
"\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",
234+
)),
235+
},
214236
]
215237
}
216238

217239
fn search_terms(&self) -> Vec<&str> {
218-
vec!["filter", "regex", "search", "condition"]
240+
vec!["filter", "regex", "search", "condition", "grep"]
219241
}
220242

221243
fn run(
@@ -227,11 +249,25 @@ impl Command for Find {
227249
) -> Result<PipelineData, ShellError> {
228250
let pattern = get_match_pattern_from_arguments(engine_state, stack, call)?;
229251

252+
let multiline = call.has_flag(engine_state, stack, "multiline")?;
253+
230254
let columns_to_search: Vec<_> = call
231255
.get_flag(engine_state, stack, "columns")?
232256
.unwrap_or_default();
233257

234-
let input = split_string_if_multiline(input, call.head);
258+
let input = if multiline {
259+
if let PipelineData::ByteStream(..) = input {
260+
// ByteStream inputs are processed by iterating over the lines, which necessarily
261+
// breaks the multi-line text being streamed into a list of lines.
262+
return Err(ShellError::IncompatibleParametersSingle {
263+
msg: "Flag `--multiline` currently doesn't work for byte stream inputs. Consider using `collect`".into(),
264+
span: call.get_flag_span(stack, "multiline").expect("has flag"),
265+
});
266+
};
267+
input
268+
} else {
269+
split_string_if_multiline(input, call.head)
270+
};
235271

236272
find_in_pipelinedata(pattern, columns_to_search, engine_state, stack, input)
237273
}
@@ -242,8 +278,11 @@ struct MatchPattern {
242278
/// the regex to be used for matching in text
243279
regex: Regex,
244280

245-
/// the list of match terms converted to lowercase strings, or empty if a regex was provided
246-
lower_terms: Vec<String>,
281+
/// the list of match terms (converted to lowercase if needed), or empty if a regex was provided
282+
search_terms: Vec<String>,
283+
284+
/// case-insensitive match
285+
ignore_case: bool,
247286

248287
/// return a modified version of the value where matching parts are highlighted
249288
highlight: bool,
@@ -272,6 +311,10 @@ fn get_match_pattern_from_arguments(
272311
let invert = call.has_flag(engine_state, stack, "invert")?;
273312
let highlight = !call.has_flag(engine_state, stack, "no-highlight")?;
274313

314+
let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
315+
316+
let dotall = call.has_flag(engine_state, stack, "dotall")?;
317+
275318
let style_computer = StyleComputer::from_config(engine_state, stack);
276319
// Currently, search results all use the same style.
277320
// Also note that this sample string is passed into user-written code (the closure that may or may not be
@@ -280,55 +323,62 @@ fn get_match_pattern_from_arguments(
280323
let highlight_style =
281324
style_computer.compute("search_result", &Value::string("search result", span));
282325

283-
let (regex_str, lower_terms) = if let Some(regex) = regex {
326+
let (regex_str, search_terms) = if let Some(regex) = regex {
284327
if !terms.is_empty() {
285328
return Err(ShellError::IncompatibleParametersSingle {
286329
msg: "Cannot use a `--regex` parameter with additional search terms".into(),
287330
span: call.get_flag_span(stack, "regex").expect("has flag"),
288331
});
289332
}
290333

291-
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
292-
let multiline = call.has_flag(engine_state, stack, "multiline")?;
293-
let dotall = call.has_flag(engine_state, stack, "dotall")?;
294-
295-
let flags = match (insensitive, multiline, dotall) {
296-
(false, false, false) => "",
297-
(true, false, false) => "(?i)", // case insensitive
298-
(false, true, false) => "(?m)", // multi-line mode
299-
(false, false, true) => "(?s)", // allow . to match \n
300-
(true, true, false) => "(?im)", // case insensitive and multi-line mode
301-
(true, false, true) => "(?is)", // case insensitive and allow . to match \n
302-
(false, true, true) => "(?ms)", // multi-line mode and allow . to match \n
303-
(true, true, true) => "(?ims)", // case insensitive, multi-line mode and allow . to match \n
334+
let flags = match (ignore_case, dotall) {
335+
(false, false) => "",
336+
(true, false) => "(?i)", // case insensitive
337+
(false, true) => "(?s)", // allow . to match \n
338+
(true, true) => "(?is)", // case insensitive and allow . to match \n
304339
};
305340

306341
(flags.to_string() + regex.as_str(), Vec::new())
307342
} else {
343+
if dotall {
344+
return Err(ShellError::IncompatibleParametersSingle {
345+
msg: "Flag --dotall only works for regex search".into(),
346+
span: call.get_flag_span(stack, "dotall").expect("has flag"),
347+
});
348+
}
349+
308350
let mut regex = String::new();
309351

310-
regex += "(?i)";
352+
if ignore_case {
353+
regex += "(?i)";
354+
}
311355

312-
let lower_terms = terms
356+
let search_terms = terms
313357
.iter()
314-
.map(|v| escape(&v.to_expanded_string("", &config).to_lowercase()).into())
358+
.map(|v| {
359+
if ignore_case {
360+
v.to_expanded_string("", &config).to_lowercase()
361+
} else {
362+
v.to_expanded_string("", &config)
363+
}
364+
})
315365
.collect::<Vec<String>>();
316366

317-
if let Some(term) = lower_terms.first() {
367+
let escaped_terms = search_terms
368+
.iter()
369+
.map(|v| escape(v).into())
370+
.collect::<Vec<String>>();
371+
372+
if let Some(term) = escaped_terms.first() {
318373
regex += term;
319374
}
320375

321-
for term in lower_terms.iter().skip(1) {
376+
for term in escaped_terms.iter().skip(1) {
322377
regex += "|";
323378
regex += term;
324379
}
325380

326-
let lower_terms = terms
327-
.iter()
328-
.map(|v| v.to_expanded_string("", &config).to_lowercase())
329-
.collect::<Vec<String>>();
330-
331-
(regex, lower_terms)
381+
(regex, search_terms)
332382
};
333383

334384
let regex = Regex::new(regex_str.as_str()).map_err(|e| ShellError::TypeMismatch {
@@ -338,7 +388,8 @@ fn get_match_pattern_from_arguments(
338388

339389
Ok(MatchPattern {
340390
regex,
341-
lower_terms,
391+
search_terms,
392+
ignore_case,
342393
invert,
343394
highlight,
344395
string_style,
@@ -507,7 +558,11 @@ fn value_should_be_printed(
507558
columns_to_search: &[String],
508559
config: &Config,
509560
) -> bool {
510-
let lower_value = value.to_expanded_string("", config).to_lowercase();
561+
let value_as_string = if pattern.ignore_case {
562+
value.to_expanded_string("", config).to_lowercase()
563+
} else {
564+
value.to_expanded_string("", config)
565+
};
511566

512567
match value {
513568
Value::Bool { .. }
@@ -519,18 +574,18 @@ fn value_should_be_printed(
519574
| Value::Float { .. }
520575
| Value::Closure { .. }
521576
| Value::Nothing { .. } => {
522-
if !pattern.lower_terms.is_empty() {
577+
if !pattern.search_terms.is_empty() {
523578
// look for exact match when searching with terms
524579
pattern
525-
.lower_terms
580+
.search_terms
526581
.iter()
527-
.any(|term: &String| term == &lower_value)
582+
.any(|term: &String| term == &value_as_string)
528583
} else {
529-
string_should_be_printed(pattern, &lower_value)
584+
string_should_be_printed(pattern, &value_as_string)
530585
}
531586
}
532587
Value::Glob { .. } | Value::CellPath { .. } | Value::Custom { .. } => {
533-
string_should_be_printed(pattern, &lower_value)
588+
string_should_be_printed(pattern, &value_as_string)
534589
}
535590
Value::String { val, .. } => string_should_be_printed(pattern, val),
536591
Value::List { vals, .. } => vals
@@ -597,7 +652,8 @@ pub fn find_internal(
597652

598653
let pattern = MatchPattern {
599654
regex,
600-
lower_terms: vec![search_term.to_lowercase()],
655+
search_terms: vec![search_term.to_lowercase()],
656+
ignore_case: true,
601657
highlight,
602658
invert: false,
603659
string_style,

crates/nu-command/tests/commands/find.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ fn find_with_list_search_with_char() {
2626

2727
#[test]
2828
fn find_with_bytestream_search_with_char() {
29-
let actual =
30-
nu!("\"ABC\" | save foo.txt; let res = open foo.txt | find abc; rm foo.txt; $res | get 0");
29+
let actual = nu!(
30+
"\"ABC\" | save foo.txt; let res = open foo.txt | find -i abc; rm foo.txt; $res | get 0"
31+
);
3132
let actual_no_highlight = nu!(
32-
"\"ABC\" | save foo.txt; let res = open foo.txt | find --no-highlight abc; rm foo.txt; $res | get 0"
33+
"\"ABC\" | save foo.txt; let res = open foo.txt | find -i --no-highlight abc; rm foo.txt; $res | get 0"
3334
);
3435

3536
assert_eq!(

typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ extend-ignore-re = [
1919
"0x\\[ba be\\]",
2020
"\\)BaR'",
2121
"fo�.txt",
22+
"ue",
2223
]
2324

2425
[type.rust.extend-words]

0 commit comments

Comments
 (0)