1use std::path::Path;
4
5use anyhow::Result;
6use regex::Regex;
7
8use crate::read_write;
9use crate::util;
10
11pub const SKIP_DIRS: &[&str] = &[
13 ".git",
14 "node_modules",
15 "target",
16 "__pycache__",
17 "venv",
18 ".venv",
19 ".tox",
20];
21
22pub type GrepMatch = (String, usize, String);
24
25pub fn grep_directory(
32 path: &Path,
33 re: &Regex,
34 base: Option<&Path>,
35 include: Option<&str>,
36 skip_dirs: &[&str],
37 max_matches: usize,
38) -> Result<(Vec<GrepMatch>, usize)> {
39 let mut results = Vec::new();
40 let mut files_matched = 0usize;
41 grep_recursive(
42 path,
43 base,
44 re,
45 include,
46 skip_dirs,
47 max_matches,
48 &mut results,
49 &mut files_matched,
50 )?;
51 Ok((results, files_matched))
52}
53
54#[allow(clippy::too_many_arguments)]
55fn grep_recursive(
56 dir: &Path,
57 base: Option<&Path>,
58 re: &Regex,
59 include: Option<&str>,
60 skip_dirs: &[&str],
61 max_matches: usize,
62 results: &mut Vec<GrepMatch>,
63 files_matched: &mut usize,
64) -> Result<()> {
65 if !dir.is_dir() {
66 return grep_single_file(dir, base, re, results, files_matched, max_matches);
67 }
68 let entries = crate::dir::read_dir(dir)?;
69 for (path, is_dir) in entries {
70 if results.len() >= max_matches {
71 return Ok(());
72 }
73 let name = path
74 .file_name()
75 .unwrap_or_default()
76 .to_string_lossy()
77 .to_string();
78 if is_dir {
79 if skip_dirs.contains(&name.as_str()) || name.starts_with('.') {
80 continue;
81 }
82 grep_recursive(
83 &path,
84 base,
85 re,
86 include,
87 skip_dirs,
88 max_matches,
89 results,
90 files_matched,
91 )?;
92 } else {
93 if let Some(glob) = include {
94 if !util::matches_glob(&name, glob) {
95 continue;
96 }
97 }
98 if util::is_likely_binary(&path) {
99 continue;
100 }
101 grep_single_file(&path, base, re, results, files_matched, max_matches)?;
102 }
103 }
104 Ok(())
105}
106
107fn grep_single_file(
108 path: &Path,
109 base: Option<&Path>,
110 re: &Regex,
111 results: &mut Vec<GrepMatch>,
112 files_matched: &mut usize,
113 max_matches: usize,
114) -> Result<()> {
115 let content = match read_write::read_file(path) {
116 Ok(c) => c,
117 Err(_) => return Ok(()),
118 };
119 let rel_path = base
120 .and_then(|b| path.strip_prefix(b).ok())
121 .unwrap_or(path)
122 .to_string_lossy()
123 .to_string();
124 let mut file_has_match = false;
125 for (line_num, line) in content.lines().enumerate() {
126 if results.len() >= max_matches {
127 break;
128 }
129 if re.is_match(line) {
130 if !file_has_match {
131 *files_matched += 1;
132 file_has_match = true;
133 }
134 results.push((rel_path.clone(), line_num + 1, line.to_string()));
135 }
136 }
137 Ok(())
138}