Skip to main content

skilllite_fs/
grep.rs

1//! 递归 grep:按 regex 搜索目录
2
3use std::path::Path;
4
5use anyhow::Result;
6use regex::Regex;
7
8use crate::read_write;
9use crate::util;
10
11/// 默认跳过的目录
12pub const SKIP_DIRS: &[&str] = &[
13    ".git",
14    "node_modules",
15    "target",
16    "__pycache__",
17    "venv",
18    ".venv",
19    ".tox",
20];
21
22/// 单条匹配:(相对路径, 行号, 行内容)
23pub type GrepMatch = (String, usize, String);
24
25/// 递归 grep 目录,返回匹配行
26///
27/// - `base`: 用于生成相对路径的基准,若为 None 则使用完整路径
28/// - `include`: 可选 glob,如 "*.rs" 仅匹配扩展名
29/// - `skip_dirs`: 跳过的目录名,默认用 SKIP_DIRS
30/// - `max_matches`: 最大匹配数
31pub 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}