1use fancy_regex::{Regex, escape};
2use nu_ansi_term::Style;
3use nu_color_config::StyleComputer;
4use nu_engine::command_prelude::*;
5use nu_protocol::Config;
6
7#[derive(Clone)]
8pub struct Find;
9
10impl Command for Find {
11 fn name(&self) -> &str {
12 "find"
13 }
14
15 fn signature(&self) -> Signature {
16 Signature::build(self.name())
17 .input_output_types(vec![
18 (
19 Type::List(Box::new(Type::Any)),
22 Type::List(Box::new(Type::Any)),
23 ),
24 (Type::String, Type::Any),
25 ])
26 .named(
27 "regex",
28 SyntaxShape::String,
29 "regex to match with",
30 Some('r'),
31 )
32 .switch(
33 "ignore-case",
34 "case-insensitive; when in regex mode, this is equivalent to (?i)",
35 Some('i'),
36 )
37 .switch(
38 "multiline",
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",
40 Some('m'),
41 )
42 .switch(
43 "dotall",
44 "dotall regex mode: allow a dot . to match newlines \\n; equivalent to (?s)",
45 Some('s'),
46 )
47 .named(
48 "columns",
49 SyntaxShape::List(Box::new(SyntaxShape::String)),
50 "column names to be searched",
51 Some('c'),
52 )
53 .switch(
54 "no-highlight",
55 "no-highlight mode: find without marking with ansi code",
56 Some('n'),
57 )
58 .switch("invert", "invert the match", Some('v'))
59 .switch(
60 "rfind",
61 "search from the end of the string and only return the first match",
62 Some('R'),
63 )
64 .rest("rest", SyntaxShape::Any, "Terms to search.")
65 .category(Category::Filters)
66 }
67
68 fn description(&self) -> &str {
69 "Searches terms in the input."
70 }
71
72 fn examples(&self) -> Vec<Example<'_>> {
73 vec![
74 Example {
75 description: "Search for multiple terms in a command output",
76 example: r#"ls | find toml md sh"#,
77 result: None,
78 },
79 Example {
80 description: "Search and highlight text for a term in a string.",
81 example: r#"'Cargo.toml' | find Cargo"#,
82 result: Some(Value::test_string(
83 "\u{1b}[39m\u{1b}[0m\u{1b}[41;39mCargo\u{1b}[0m\u{1b}[39m.toml\u{1b}[0m"
84 .to_owned(),
85 )),
86 },
87 Example {
88 description: "Search a number or a file size in a list of numbers",
89 example: r#"[1 5 3kb 4 35 3Mb] | find 5 3kb"#,
90 result: Some(Value::list(
91 vec![Value::test_int(5), Value::test_filesize(3000)],
92 Span::test_data(),
93 )),
94 },
95 Example {
96 description: "Search a char in a list of string",
97 example: r#"[moe larry curly] | find l"#,
98 result: Some(Value::list(
99 vec![
100 Value::test_string(
101 "\u{1b}[39m\u{1b}[0m\u{1b}[41;39ml\u{1b}[0m\u{1b}[39marry\u{1b}[0m",
102 ),
103 Value::test_string(
104 "\u{1b}[39mcur\u{1b}[0m\u{1b}[41;39ml\u{1b}[0m\u{1b}[39my\u{1b}[0m",
105 ),
106 ],
107 Span::test_data(),
108 )),
109 },
110 Example {
111 description: "Search using regex",
112 example: r#"[abc odb arc abf] | find --regex "b.""#,
113 result: Some(Value::list(
114 vec![
115 Value::test_string(
116 "\u{1b}[39ma\u{1b}[0m\u{1b}[41;39mbc\u{1b}[0m\u{1b}[39m\u{1b}[0m"
117 .to_string(),
118 ),
119 Value::test_string(
120 "\u{1b}[39ma\u{1b}[0m\u{1b}[41;39mbf\u{1b}[0m\u{1b}[39m\u{1b}[0m"
121 .to_string(),
122 ),
123 ],
124 Span::test_data(),
125 )),
126 },
127 Example {
128 description: "Case insensitive search",
129 example: r#"[aBc bde Arc abf] | find "ab" -i"#,
130 result: Some(Value::list(
131 vec![
132 Value::test_string(
133 "\u{1b}[39m\u{1b}[0m\u{1b}[41;39maB\u{1b}[0m\u{1b}[39mc\u{1b}[0m"
134 .to_string(),
135 ),
136 Value::test_string(
137 "\u{1b}[39m\u{1b}[0m\u{1b}[41;39mab\u{1b}[0m\u{1b}[39mf\u{1b}[0m"
138 .to_string(),
139 ),
140 ],
141 Span::test_data(),
142 )),
143 },
144 Example {
145 description: "Find value in records using regex",
146 example: r#"[[version name]; ['0.1.0' nushell] ['0.1.1' fish] ['0.2.0' zsh]] | find --regex "nu""#,
147 result: Some(Value::test_list(vec![Value::test_record(record! {
148 "version" => Value::test_string("0.1.0"),
149 "name" => Value::test_string("\u{1b}[39m\u{1b}[0m\u{1b}[41;39mnu\u{1b}[0m\u{1b}[39mshell\u{1b}[0m".to_string()),
150 })])),
151 },
152 Example {
153 description: "Find inverted values in records using regex",
154 example: r#"[[version name]; ['0.1.0' nushell] ['0.1.1' fish] ['0.2.0' zsh]] | find --regex "nu" --invert"#,
155 result: Some(Value::test_list(vec![
156 Value::test_record(record! {
157 "version" => Value::test_string("0.1.1"),
158 "name" => Value::test_string("fish".to_string()),
159 }),
160 Value::test_record(record! {
161 "version" => Value::test_string("0.2.0"),
162 "name" =>Value::test_string("zsh".to_string()),
163 }),
164 ])),
165 },
166 Example {
167 description: "Find value in list using regex",
168 example: r#"[["Larry", "Moe"], ["Victor", "Marina"]] | find --regex "rr""#,
169 result: Some(Value::list(
170 vec![Value::list(
171 vec![
172 Value::test_string(
173 "\u{1b}[39mLa\u{1b}[0m\u{1b}[41;39mrr\u{1b}[0m\u{1b}[39my\u{1b}[0m",
174 ),
175 Value::test_string("Moe"),
176 ],
177 Span::test_data(),
178 )],
179 Span::test_data(),
180 )),
181 },
182 Example {
183 description: "Find inverted values in records using regex",
184 example: r#"[["Larry", "Moe"], ["Victor", "Marina"]] | find --regex "rr" --invert"#,
185 result: Some(Value::list(
186 vec![Value::list(
187 vec![Value::test_string("Victor"), Value::test_string("Marina")],
188 Span::test_data(),
189 )],
190 Span::test_data(),
191 )),
192 },
193 Example {
194 description: "Remove ANSI sequences from result",
195 example: "[[foo bar]; [abc 123] [def 456]] | find --no-highlight 123",
196 result: Some(Value::list(
197 vec![Value::test_record(record! {
198 "foo" => Value::test_string("abc"),
199 "bar" => Value::test_int(123)
200 })],
201 Span::test_data(),
202 )),
203 },
204 Example {
205 description: "Find and highlight text in specific columns",
206 example: "[[col1 col2 col3]; [moe larry curly] [larry curly moe]] | find moe --columns [col1]",
207 result: Some(Value::list(
208 vec![Value::test_record(record! {
209 "col1" => Value::test_string(
210 "\u{1b}[39m\u{1b}[0m\u{1b}[41;39mmoe\u{1b}[0m\u{1b}[39m\u{1b}[0m"
211 .to_string(),
212 ),
213 "col2" => Value::test_string("larry".to_string()),
214 "col3" => Value::test_string("curly".to_string()),
215 })],
216 Span::test_data(),
217 )),
218 },
219 Example {
220 description: "Find in a multi-line string",
221 example: "'Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue' | find ue",
222 result: Some(Value::list(
223 vec![
224 Value::test_string(
225 "\u{1b}[39mAnd roses are bl\u{1b}[0m\u{1b}[41;39mue\u{1b}[0m\u{1b}[39m\u{1b}[0m",
226 ),
227 Value::test_string(
228 "\u{1b}[39mAlter their h\u{1b}[0m\u{1b}[41;39mue\u{1b}[0m\u{1b}[39m\u{1b}[0m",
229 ),
230 ],
231 Span::test_data(),
232 )),
233 },
234 Example {
235 description: "Find in a multi-line string without splitting the input into a list of lines",
236 example: "'Violets are red\nAnd roses are blue\nWhen metamaterials\nAlter their hue' | find --multiline ue",
237 result: Some(Value::test_string(
238 "\u{1b}[39mViolets are red\nAnd roses are bl\u{1b}[0m\u{1b}[41;39mue\u{1b}[0m\u{1b}[39m\nWhen metamaterials\nAlter their h\u{1b}[0m\u{1b}[41;39mue\u{1b}[0m\u{1b}[39m\u{1b}[0m",
239 )),
240 },
241 Example {
242 description: "Find and highlight the last occurrence in a string",
243 example: r#"'hello world hello' | find --rfind hello"#,
244 result: Some(Value::test_string(
245 "\u{1b}[39mhello world \u{1b}[0m\u{1b}[41;39mhello\u{1b}[0m\u{1b}[39m\u{1b}[0m",
246 )),
247 },
248 ]
249 }
250
251 fn search_terms(&self) -> Vec<&str> {
252 vec!["filter", "regex", "search", "condition", "grep"]
253 }
254
255 fn run(
256 &self,
257 engine_state: &EngineState,
258 stack: &mut Stack,
259 call: &Call,
260 input: PipelineData,
261 ) -> Result<PipelineData, ShellError> {
262 let pattern = get_match_pattern_from_arguments(engine_state, stack, call)?;
263
264 let multiline = call.has_flag(engine_state, stack, "multiline")?;
265
266 let columns_to_search: Vec<_> = call
267 .get_flag(engine_state, stack, "columns")?
268 .unwrap_or_default();
269
270 let input = if multiline {
271 if let PipelineData::ByteStream(..) = input {
272 return Err(ShellError::IncompatibleParametersSingle {
275 msg: "Flag `--multiline` currently doesn't work for byte stream inputs. Consider using `collect`".into(),
276 span: call.get_flag_span(stack, "multiline").expect("has flag"),
277 });
278 };
279 input
280 } else {
281 split_string_if_multiline(input, call.head)
282 };
283
284 find_in_pipelinedata(pattern, columns_to_search, engine_state, stack, input)
285 }
286}
287
288#[derive(Clone)]
289struct MatchPattern {
290 regex: Regex,
292
293 search_terms: Vec<String>,
295
296 ignore_case: bool,
298
299 highlight: bool,
301
302 invert: bool,
304
305 rfind: bool,
307
308 string_style: Style,
310
311 highlight_style: Style,
313}
314
315fn get_match_pattern_from_arguments(
316 engine_state: &EngineState,
317 stack: &mut Stack,
318 call: &Call,
319) -> Result<MatchPattern, ShellError> {
320 let config = stack.get_config(engine_state);
321
322 let span = call.head;
323 let regex = call.get_flag::<String>(engine_state, stack, "regex")?;
324 let terms = call.rest::<Value>(engine_state, stack, 0)?;
325
326 let invert = call.has_flag(engine_state, stack, "invert")?;
327 let highlight = !call.has_flag(engine_state, stack, "no-highlight")?;
328 let rfind = call.has_flag(engine_state, stack, "rfind")?;
329
330 let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
331
332 let dotall = call.has_flag(engine_state, stack, "dotall")?;
333
334 let style_computer = StyleComputer::from_config(engine_state, stack);
335 let string_style = style_computer.compute("string", &Value::string("search result", span));
339 let highlight_style =
340 style_computer.compute("search_result", &Value::string("search result", span));
341
342 let (regex_str, search_terms) = if let Some(regex) = regex {
343 if !terms.is_empty() {
344 return Err(ShellError::IncompatibleParametersSingle {
345 msg: "Cannot use a `--regex` parameter with additional search terms".into(),
346 span: call.get_flag_span(stack, "regex").expect("has flag"),
347 });
348 }
349
350 let flags = match (ignore_case, dotall) {
351 (false, false) => "",
352 (true, false) => "(?i)", (false, true) => "(?s)", (true, true) => "(?is)", };
356
357 (flags.to_string() + regex.as_str(), Vec::new())
358 } else {
359 if dotall {
360 return Err(ShellError::IncompatibleParametersSingle {
361 msg: "Flag --dotall only works for regex search".into(),
362 span: call.get_flag_span(stack, "dotall").expect("has flag"),
363 });
364 }
365
366 let mut regex = String::new();
367
368 if ignore_case {
369 regex += "(?i)";
370 }
371
372 let search_terms = terms
373 .iter()
374 .map(|v| {
375 if ignore_case {
376 v.to_expanded_string("", &config).to_lowercase()
377 } else {
378 v.to_expanded_string("", &config)
379 }
380 })
381 .collect::<Vec<String>>();
382
383 let escaped_terms = search_terms
384 .iter()
385 .map(|v| escape(v).into())
386 .collect::<Vec<String>>();
387
388 if let Some(term) = escaped_terms.first() {
389 regex += term;
390 }
391
392 for term in escaped_terms.iter().skip(1) {
393 regex += "|";
394 regex += term;
395 }
396
397 (regex, search_terms)
398 };
399
400 let regex = Regex::new(regex_str.as_str()).map_err(|e| ShellError::TypeMismatch {
401 err_message: format!("invalid regex: {e}"),
402 span,
403 })?;
404
405 Ok(MatchPattern {
406 regex,
407 search_terms,
408 ignore_case,
409 invert,
410 highlight,
411 rfind,
412 string_style,
413 highlight_style,
414 })
415}
416
417fn highlight_matches_in_string(pattern: &MatchPattern, val: String) -> String {
420 if !pattern.regex.is_match(&val).unwrap_or(false) {
421 return val;
422 }
423
424 let stripped_val = nu_utils::strip_ansi_string_unlikely(val);
425
426 if pattern.rfind {
427 highlight_last_match(pattern, &stripped_val)
428 } else {
429 highlight_all_matches(pattern, &stripped_val)
430 }
431}
432
433fn highlight_last_match(pattern: &MatchPattern, text: &str) -> String {
434 let last_match = pattern.regex.find_iter(text).fold(None, |_, m| m.ok());
436
437 match last_match {
438 Some(m) => {
439 let start = m.start();
440 let end = m.end();
441 format!(
442 "{}{}{}",
443 pattern.string_style.paint(&text[..start]),
444 pattern.highlight_style.paint(&text[start..end]),
445 pattern.string_style.paint(&text[end..])
446 )
447 }
448 None => pattern.string_style.paint(text).to_string(),
449 }
450}
451
452fn highlight_all_matches(pattern: &MatchPattern, text: &str) -> String {
453 let mut last_match_end = 0;
454 let mut highlighted = String::new();
455
456 for cap in pattern.regex.captures_iter(text) {
457 let capture = match cap {
458 Ok(capture) => capture,
459 Err(_) => return pattern.string_style.paint(text).to_string(),
460 };
461
462 let m = match capture.get(0) {
463 Some(m) => m,
464 None => continue,
465 };
466
467 highlighted.push_str(
468 &pattern
469 .string_style
470 .paint(&text[last_match_end..m.start()])
471 .to_string(),
472 );
473 highlighted.push_str(
474 &pattern
475 .highlight_style
476 .paint(&text[m.start()..m.end()])
477 .to_string(),
478 );
479 last_match_end = m.end();
480 }
481
482 highlighted.push_str(
483 &pattern
484 .string_style
485 .paint(&text[last_match_end..])
486 .to_string(),
487 );
488 highlighted
489}
490
491fn highlight_matches_in_value(
492 pattern: &MatchPattern,
493 value: Value,
494 columns_to_search: &[String],
495) -> Value {
496 if !pattern.highlight || pattern.invert {
497 return value;
498 }
499 let span = value.span();
500
501 match value {
502 Value::Record { val: record, .. } => {
503 let col_select = !columns_to_search.is_empty();
504
505 let mut record = record.into_owned();
507
508 for (col, val) in record.iter_mut() {
509 if col_select && !columns_to_search.contains(col) {
510 continue;
511 }
512
513 *val = highlight_matches_in_value(pattern, std::mem::take(val), &[]);
514 }
515
516 Value::record(record, span)
517 }
518 Value::List { vals, .. } => vals
519 .into_iter()
520 .map(|item| highlight_matches_in_value(pattern, item, &[]))
521 .collect::<Vec<Value>>()
522 .into_value(span),
523 Value::String { val, .. } => highlight_matches_in_string(pattern, val).into_value(span),
524 _ => value,
525 }
526}
527
528fn find_in_pipelinedata(
529 pattern: MatchPattern,
530 columns_to_search: Vec<String>,
531 engine_state: &EngineState,
532 stack: &mut Stack,
533 input: PipelineData,
534) -> Result<PipelineData, ShellError> {
535 let config = stack.get_config(engine_state);
536
537 let map_pattern = pattern.clone();
538 let map_columns_to_search = columns_to_search.clone();
539
540 match input {
541 PipelineData::Empty => Ok(PipelineData::empty()),
542 PipelineData::Value(_, _) => input
543 .filter(
544 move |value| {
545 value_should_be_printed(&pattern, value, &columns_to_search, &config)
546 != pattern.invert
547 },
548 engine_state.signals(),
549 )?
550 .map(
551 move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search),
552 engine_state.signals(),
553 ),
554 PipelineData::ListStream(stream, metadata) => {
555 let stream = stream.modify(|iter| {
556 iter.filter(move |value| {
557 value_should_be_printed(&pattern, value, &columns_to_search, &config)
558 != pattern.invert
559 })
560 .map(move |x| highlight_matches_in_value(&map_pattern, x, &map_columns_to_search))
561 });
562
563 Ok(PipelineData::list_stream(stream, metadata))
564 }
565 PipelineData::ByteStream(stream, ..) => {
566 let span = stream.span();
567 if let Some(lines) = stream.lines() {
568 let mut output: Vec<Value> = vec![];
569 for line in lines {
570 let line = line?;
571 if string_should_be_printed(&pattern, &line) != pattern.invert {
572 if pattern.highlight && !pattern.invert {
573 output
574 .push(highlight_matches_in_string(&pattern, line).into_value(span))
575 } else {
576 output.push(line.into_value(span))
577 }
578 }
579 }
580 Ok(Value::list(output, span).into_pipeline_data())
581 } else {
582 Ok(PipelineData::empty())
583 }
584 }
585 }
586}
587
588fn string_should_be_printed(pattern: &MatchPattern, value: &str) -> bool {
591 pattern.regex.is_match(value).unwrap_or(false)
592}
593
594fn value_should_be_printed(
595 pattern: &MatchPattern,
596 value: &Value,
597 columns_to_search: &[String],
598 config: &Config,
599) -> bool {
600 let value_as_string = if pattern.ignore_case {
601 value.to_expanded_string("", config).to_lowercase()
602 } else {
603 value.to_expanded_string("", config)
604 };
605
606 match value {
607 Value::Bool { .. }
608 | Value::Int { .. }
609 | Value::Filesize { .. }
610 | Value::Duration { .. }
611 | Value::Date { .. }
612 | Value::Range { .. }
613 | Value::Float { .. }
614 | Value::Closure { .. }
615 | Value::Nothing { .. } => {
616 if !pattern.search_terms.is_empty() {
617 pattern
619 .search_terms
620 .iter()
621 .any(|term: &String| term == &value_as_string)
622 } else {
623 string_should_be_printed(pattern, &value_as_string)
624 }
625 }
626 Value::Glob { .. } | Value::CellPath { .. } | Value::Custom { .. } => {
627 string_should_be_printed(pattern, &value_as_string)
628 }
629 Value::String { val, .. } => string_should_be_printed(pattern, val),
630 Value::List { vals, .. } => vals
631 .iter()
632 .any(|item| value_should_be_printed(pattern, item, &[], config)),
633 Value::Record { val: record, .. } => {
634 let col_select = !columns_to_search.is_empty();
635 record.iter().any(|(col, val)| {
636 if col_select && !columns_to_search.contains(col) {
637 return false;
638 }
639 value_should_be_printed(pattern, val, &[], config)
640 })
641 }
642 Value::Binary { .. } => false,
643 Value::Error { .. } => true,
644 }
645}
646
647fn split_string_if_multiline(input: PipelineData, head_span: Span) -> PipelineData {
650 let span = input.span().unwrap_or(head_span);
651 match input {
652 PipelineData::Value(Value::String { ref val, .. }, _) => {
653 if val.contains('\n') {
654 Value::list(
655 val.lines()
656 .map(|s| Value::string(s.to_string(), span))
657 .collect(),
658 span,
659 )
660 .into_pipeline_data_with_metadata(input.metadata())
661 } else {
662 input
663 }
664 }
665 _ => input,
666 }
667}
668
669pub fn find_internal(
671 input: PipelineData,
672 engine_state: &EngineState,
673 stack: &mut Stack,
674 search_term: &str,
675 columns_to_search: &[&str],
676 highlight: bool,
677) -> Result<PipelineData, ShellError> {
678 let span = input.span().unwrap_or(Span::unknown());
679
680 let style_computer = StyleComputer::from_config(engine_state, stack);
681 let string_style = style_computer.compute("string", &Value::string("search result", span));
682 let highlight_style =
683 style_computer.compute("search_result", &Value::string("search result", span));
684
685 let regex_str = format!("(?i){}", escape(search_term));
686
687 let regex = Regex::new(regex_str.as_str()).map_err(|e| ShellError::TypeMismatch {
688 err_message: format!("invalid regex: {e}"),
689 span: Span::unknown(),
690 })?;
691
692 let pattern = MatchPattern {
693 regex,
694 search_terms: vec![search_term.to_lowercase()],
695 ignore_case: true,
696 highlight,
697 invert: false,
698 rfind: false,
699 string_style,
700 highlight_style,
701 };
702
703 let columns_to_search = columns_to_search
704 .iter()
705 .map(|str| String::from(*str))
706 .collect();
707
708 find_in_pipelinedata(pattern, columns_to_search, engine_state, stack, input)
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn test_examples() {
717 use crate::test_examples;
718
719 test_examples(Find)
720 }
721}