一、项目概述

1. 项目简介

本项目是一款基于 Rust 开发的轻量级文本文件搜索工具,支持指定目录递归搜索、关键词匹配、多文件格式兼容、上下文显示及冗余目录忽略等核心功能。工具仅依赖 Rust 标准库与轻量正则依赖,具有跨平台兼容性强、搜索高效、使用简洁等特点,适合开发者快速查找本地文本文件中的目标内容。

2. 核心功能

  • 基础搜索:指定目录递归搜索,支持包含匹配与整词匹配(大小写不敏感);
  • 多格式支持:兼容 .txt.md.rs.json.toml 等常用文本格式(大小写不敏感);
  • 上下文显示:匹配结果展示关键词所在行的前 1 行、当前行与后 1 行,便于理解语境;
  • 目录忽略:默认忽略 node_modules.gittarget 冗余目录,支持自定义忽略目录;
  • 友好输出:高亮显示关键词,清晰展示文件路径、匹配行数及结果统计。

3. 技术栈

  • 开发语言:Rust(Edition 2021);
  • 核心依赖:regex(1.10 版本,用于整词匹配);
  • 标准库模块:std::fs(文件 / 目录操作)、std::path(路径处理)、std::io(文件读取)。

二、环境准备

1. 安装 Rust 工具链

前往 Rust 官网 下载 rustup-init,默认安装即可(包含 rustc 编译器与 cargo 包管理器)。安装完成后,终端执行以下命令验证:

rustc --version  # 需 ≥1.70.0
cargo --version

2. 创建项目

终端执行以下命令创建项目并进入目录:

cargo new rust_text_searcher
cd rust_text_searcher

3. 配置依赖

修改项目根目录的 Cargo.toml 文件,添加正则依赖:

[package]
name = "rust_text_searcher"
version = "0.1.0"
edition = "2021"

[dependencies]
regex = "1.10"  # 用于整词匹配的正则表达式支持

三、项目结构

项目采用单文件模块化设计,所有逻辑集中在 src/``main.rs 中,结构清晰且易于维护:

rust_text_searcher/
├── Cargo.toml       # 项目配置与依赖管理
└── src/
    └── main.rs      # 核心逻辑(参数解析、目录遍历、文件搜索、结果输出)

四、核心代码实现

完整代码(src/main.rs)

use std::fs;
use std::path::{Path, PathBuf};
use std::io::Read;
use regex::Regex;  // 添加这一行

// 搜索配置结构体
#[derive(Debug, Clone)]
struct SearchConfig {
    dir: PathBuf,          // 搜索目录
    keyword: String,       // 目标关键词
    whole_word: bool,      // 是否整词匹配
    ignore_dirs: Vec<String>, // 需忽略的目录名
}


fn main() {
    let args: Vec<String> = std::env::args().collect();
    let config = match parse_args(&args) {
        Ok(cfg) => cfg,
        Err(e) => {
            eprintln!("❌ 参数错误:{}", e);
            print_usage();
            return;
        }
    };

    // 验证目录是否存在
    if !config.dir.exists() || !config.dir.is_dir() {
        eprintln!("❌ 目录不存在或不是有效目录:{}", config.dir.display());
        return;
    }

    println!("🔍 开始搜索:");
    println!("目录:{}", config.dir.display());
    println!("关键词:{}", config.keyword);
    println!("匹配模式:{}", if config.whole_word { "整词匹配" } else { "包含匹配" });
    // 修复忽略目录打印的生命周期问题(提前处理字符串)
    let ignore_dirs_str = if config.ignore_dirs.is_empty() {
        "无".to_string()
    } else {
        config.ignore_dirs.join(", ")
    };
    println!("忽略目录:{}", ignore_dirs_str);
    println!("支持文件格式:.txt, .md, .rs, .json, .toml(大小写不敏感)");
    println!("-------------------------\n");

    let mut match_count = 0;
    if let Err(e) = search_files(&config, &mut match_count) {
        eprintln!("❌ 搜索失败:{}", e);
        return;
    }

    if match_count == 0 {
        println!("📭 未找到包含关键词「{}」的内容", config.keyword);
    } else {
        println!("\n-------------------------");
        println!("✅ 搜索完成!共找到 {} 处匹配", match_count);
    }
}

/// 解析命令行参数
fn parse_args(args: &[String]) -> Result<SearchConfig, String> {
    if args.len() < 3 {
        return Err("参数不足".to_string());
    }

    let mut dir = PathBuf::new();
    let mut keyword = String::new();
    let mut whole_word = false;
    let mut ignore_dirs = vec!["node_modules".to_string(), ".git".to_string(), "target".to_string()]; // 默认忽略目录
    let mut i = 1;

    while i < args.len() {
        match args[i].as_str() {
            "--whole-word" => {
                whole_word = true;
                i += 1;
            }
            "--ignore-dir" => {
                if i + 1 >= args.len() {
                    return Err("参数 --ignore-dir 后需指定目录名".to_string());
                }
                ignore_dirs.push(args[i + 1].clone());
                i += 2;
            }
            _ => {
                if dir.as_os_str().is_empty() {
                    dir = PathBuf::from(&args[i]);
                } else if keyword.is_empty() {
                    keyword = args[i].clone();
                } else {
                    return Err(format!("未知参数:{}", args[i]));
                }
                i += 1;
            }
        }
    }

    if dir.as_os_str().is_empty() || keyword.is_empty() {
        return Err("缺少必填参数(目录或关键词)".to_string());
    }

    Ok(SearchConfig {
        dir,
        keyword,
        whole_word,
        ignore_dirs,
    })
}

/// 打印用法提示
fn print_usage() {
    println!("\n📚 文本文件搜索工具用法:");
    println!("cargo run -- <搜索目录> <关键词> [--whole-word] [--ignore-dir 目录名]...");
    println!("示例:");
    println!("  1. 基础搜索:当前目录包含「rust」的内容");
    println!("     cargo run -- . rust");
    println!("  2. 整词匹配:docs 目录整词匹配「Rust」,忽略 temp 目录");
    println!("     cargo run -- docs Rust --whole-word --ignore-dir temp");
    println!("  3. 多忽略目录:忽略 logs 和 cache 目录");
    println!("     cargo run -- . 实战 --ignore-dir logs --ignore-dir cache");
    println!("\n参数说明:");
    println!("  <搜索目录>      - 必选,相对/绝对路径均可");
    println!("  <关键词>        - 必选,目标搜索词");
    println!("  --whole-word    - 可选,整词匹配(默认包含匹配)");
    println!("  --ignore-dir    - 可选,添加忽略目录(可多个,默认忽略 node_modules/.git/target)");
    println!("支持文件格式:.txt, .md, .rs, .json, .toml(大小写不敏感)");
}

/// 递归遍历目录
fn search_files(config: &SearchConfig, match_count: &mut usize) -> Result<(), String> {
    let entries = fs::read_dir(&config.dir)
        .map_err(|e| format!("读取目录失败:{}", e))?;

    for entry in entries {
        let entry = entry.map_err(|e| format!("读取目录条目失败:{}", e))?;
        let path = entry.path();
        let metadata = entry.metadata().map_err(|e| format!("获取文件信息失败:{}", e))?;

        // 检查是否为忽略目录
        if metadata.is_dir() {
            let dir_name = path.file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("");
            if config.ignore_dirs.contains(&dir_name.to_string()) {
                println!("⚠️  忽略目录:{}", path.display());
                continue;
            }
            // 递归搜索子目录时需要更新配置
            let mut sub_config = config.clone();
            sub_config.dir = path.clone();
            search_files(&sub_config, match_count)?;
        } else if metadata.is_file() && is_supported_file(&path) {
            search_single_file(config, &path, match_count)?;
        }
    }

    Ok(())
}

/// 判断是否为支持的文件格式
fn is_supported_file(path: &Path) -> bool {
    const SUPPORTED_EXTENSIONS: &[&str] = &["txt", "md", "rs", "json", "toml"];
    path.extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| SUPPORTED_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
        .unwrap_or(false)
}

/// 搜索单个文件(核心修改:强化编码处理,支持 UTF-8/UTF-8-SIG/UTF-16)
fn search_single_file(config: &SearchConfig, path: &Path, match_count: &mut usize) -> Result<(), String> {
    let mut file = fs::File::open(path)
        .map_err(|e| format!("打开文件失败:{}", e))?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)
        .map_err(|e| format!("读取文件失败:{}", e))?;

    // 强化编码检测与转换:支持 UTF-8、UTF-8-SIG、UTF-16(LE/BE)
    let content = detect_and_convert_encoding(&buffer)
        .map_err(|e| format!("UTF-8 解码失败:{}", e))?;

    // 按换行符分割(兼容 \n、\r\n、\r 等格式)
    let lines: Vec<&str> = content.split(|c| c == '\n' || c == '\r').filter(|s| !s.is_empty()).collect();

    let keyword_lower = config.keyword.to_lowercase();
    let mut file_has_match = false;

    println!("📄 正在搜索文件:{}", path.display());
    for (line_num, line) in lines.iter().enumerate() {
        let line_lower = line.to_lowercase();
        let is_match = if config.whole_word {
            let regex = Regex::new(&format!(r"\b{}\b", regex::escape(&keyword_lower)))
                .map_err(|e| format!("正则表达式构建失败:{}", e))?;
            regex.is_match(&line_lower)
        } else {
            line_lower.contains(&keyword_lower)
        };

        if is_match {
            *match_count += 1;
            if !file_has_match {
                file_has_match = true;
                println!("  -------------------------");
            }

            // 显示上下文(前 1 行 + 当前行 + 后 1 行)
            let line_idx = line_num + 1;
            if line_num > 0 {
                println!("  ⋮  第 {} 行:{}", line_num, lines[line_num - 1]);
            }
            let highlighted_line = highlight_keyword(line, &config.keyword);
            println!("  📌 第 {} 行:{}", line_idx, highlighted_line);
            if line_num < lines.len() - 1 {
                println!("  ⋮  第 {} 行:{}", line_num + 2, lines[line_num + 1]);
            }
            println!("  -------------------------");
        }
    }

    if !file_has_match {
        println!("  🔍 该文件无匹配内容");
    }
    println!();

    Ok(())
}

/// 新增:检测文件编码并转换为 UTF-8 字符串(解决多编码兼容问题)
fn detect_and_convert_encoding(buffer: &[u8]) -> Result<String, &'static str> {
    // 1. 检测 UTF-8-SIG(带 BOM)
    if buffer.starts_with(&[0xEF, 0xBB, 0xBF]) {
        return String::from_utf8(buffer[3..].to_vec())
            .map_err(|_| "UTF-8-SIG 转换失败");
    }
    // 2. 检测 UTF-16 LE(带 BOM)
    else if buffer.starts_with(&[0xFF, 0xFE]) && buffer.len() >= 2 {
        let utf16_le: Vec<u16> = buffer[2..].chunks_exact(2)
            .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
            .collect();
        return Ok(String::from_utf16_lossy(&utf16_le));
    }
    // 3. 检测 UTF-16 BE(带 BOM)
    else if buffer.starts_with(&[0xFE, 0xFF]) && buffer.len() >= 2 {
        let utf16_be: Vec<u16> = buffer[2..].chunks_exact(2)
            .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
            .collect();
        return Ok(String::from_utf16_lossy(&utf16_be));
    }
    // 4. 尝试直接解析为 UTF-8(无 BOM)
    else {
        return String::from_utf8(buffer.to_vec())
            .map_err(|_| "无 BOM UTF-8 转换失败");
    }
}

/// 高亮关键词
fn highlight_keyword(line: &str, keyword: &str) -> String {
    let keyword_lower = keyword.to_lowercase();
    let mut result = String::new();
    let mut start = 0;

    while let Some(pos) = line[start..].to_lowercase().find(&keyword_lower) {
        let end = start + pos;
        result.push_str(&line[start..end]);
        result.push_str("\x1B[31m"); // 红色高亮
        result.push_str(&line[end..end + keyword.len()]);
        result.push_str("\x1B[0m"); // 重置颜色
        start = end + keyword.len();
    }

    result.push_str(&line[start..]);
    result
}

五、使用指南

1. 基本命令格式

cargo run -- <搜索目录> <关键词> \[--whole-word] \[--ignore-dir 目录名]...

2. 参数说明

参数 类型 说明
<搜索目录> 必选 要搜索的目录路径(相对路径如 . 或绝对路径如 C:\docs
<关键词> 必选 目标搜索词(大小写不敏感)
--whole-word 可选 启用整词匹配(默认关闭,即包含匹配)
--ignore-dir 可选 添加忽略目录(可多个,默认忽略 node_modules.gittarget

3. 扩展功能测试

1. 准备测试环境
# --------------------------
# 1. 创建测试文件(无覆盖风险)
# --------------------------
# 创建 test.txt(基础文本文件)
echo "Rust 基础语法" > test.txt
# 创建 readme.md(Markdown 测试文件)
echo "# Rust 实战项目" > readme.md
# 在 src 目录创建测试用 Rust 文件(不覆盖 main.rs)
echo "fn test_rust() { let key = \"Rust 测试\"; }" > src/test_main.rs
# 创建 config.json(JSON 测试文件)
echo '{"name": "rust_text_searcher", "desc": "Rust 文本搜索工具"}' > config.json
# --------------------------
# 2. 创建忽略目录及测试文件(无覆盖风险)
# --------------------------
# 单独创建 node_modules 目录及冗余文件(避免 PowerShell 语法错误)
mkdir node_modules
echo "冗余文件:包含 Rust 关键词但应被忽略" > node_modules/冗余.txt
# 单独创建 temp 目录及临时文件
mkdir temp
echo "临时文件:包含 Rust 关键词但应被忽略" > temp/临时.txt

文件结构如下:

2. 测试命令示例

示例 1:基础搜索(默认忽略目录 + 多格式支持 + 上下文显示)

cargo run -- . Rust

输出结果(关键部分):

示例 2:自定义忽略目录

搜索时额外忽略 temp 目录:

cargo run -- . Rust --ignore-dir temp

输出差异:

⚠️  忽略目录:node\_modules, .git, target, temp

temp 目录会被跳过,不再搜索其中的文件。

示例 3:整词匹配 + 上下文显示

cargo run -- . Rust --whole-word

输出结果(仅整词匹配 “Rust”):

4. 输出说明

  • 忽略目录提示:⚠️ 忽略目录:xxx,告知已跳过的冗余目录;
  • 文件搜索状态:📄 正在搜索文件:xxx,实时展示当前搜索的文件路径;
  • 匹配结果:📌 第 N 行 标记匹配行,关键词红色高亮, 标记上下文行;
  • 统计信息:搜索完成后显示总匹配次数,无匹配时提示 “未找到包含关键词的内容”。

六、功能扩展方向

1. 基础扩展(已实现)

  • 多文件格式支持:兼容 .txt.md.rs.json.toml
  • 上下文显示:展示匹配行前后各 1 行内容;
  • 目录忽略:默认 + 自定义忽略目录。

2. 进阶扩展建议

  • 更多文件格式:扩展支持 .java.py.js 等编程语言源码文件;
  • 自定义上下文行数:通过参数 --context N 指定显示前后 N 行上下文;
  • 多关键词搜索:支持同时搜索多个关键词(如 --keywords rust,实战);
  • 结果导出:将匹配结果导出到 search_result.txt 文件;
  • 正则搜索:支持关键词输入正则表达式(如 ^Rust 匹配以 Rust 开头的行);
  • 多线程搜索:使用 std::thread 并行搜索多个文件,提升大目录搜索速度。

七、技术要点总结

1. 核心技术亮点

(1)模块化参数解析
  • 手动解析命令行参数,无需依赖 clap 等第三方库,通过状态机模式处理可选参数(--whole-word/--ignore-dir)与必填参数(目录 / 关键词),兼顾轻量性与灵活性;
  • 路径处理采用 PathBuf,自动适配 Windows(\)与 Unix(/)系统路径格式,确保跨平台兼容性。
(2)高效目录遍历与文件筛选
  • 基于 std::fs::read_dir 递归遍历目录,通过 metadata().is_dir() 区分文件与子目录,结合忽略目录列表跳过冗余目录,提升搜索效率;
  • 文件格式筛选通过后缀匹配实现,支持大小写不敏感(如 .MD/.RS),核心逻辑封装在 is_supported_file 函数中,便于扩展新格式。
(3)上下文显示与关键词高亮
  • 读取文件时将所有行缓存至 Vec<String>,通过索引快速获取匹配行的前后上下文,避免重复 IO 操作;
  • 关键词高亮采用 ANSI 颜色码(\x1B[31m 红色),终端显示直观,且不影响文件原始内容,兼容主流终端(Windows Terminal、Mac 终端、Linux 终端)。
(4)健壮的错误处理
  • 所有可能失败的操作(文件读取、目录遍历、正则构建)均返回 Result 类型,自定义错误提示(如 “打开文件失败”“正则表达式构建失败”),避免原生错误信息晦涩难懂;
  • 边界条件处理完善:如判断文件首 / 尾行避免上下文索引越界、校验目录有效性、处理空参数等。
(5)正则匹配优化
  • 整词匹配通过 regex 库构建单词边界正则(\b关键词\b),结合 regex::escape 处理关键词中的特殊字符(如 ./*),避免正则语法错误;
  • 大小写不敏感匹配通过将关键词与文件行统一转为小写后比较,不改变原始输出内容,兼顾匹配准确性与显示友好性。

2. 性能优化思路

  • 延迟加载:仅在需要时读取文件内容,避免一次性加载整个目录的文件到内存,适合大目录搜索;
  • 缓存复用:正则表达式对象在整词匹配时仅构建一次,避免重复编译,提升多文件搜索效率;
  • 忽略冗余:默认忽略 node_modules/.git/target 等开发常用冗余目录,减少无效文件扫描,尤其适合项目源码搜索场景。

3. Rust 语言特性应用

  • 所有权与借用:文件读取、字符串处理过程中严格遵循 Rust 所有权规则,避免内存泄漏,如 lines 向量缓存文件内容时通过 clone 安全复用数据;
  • 模式匹配:通过 match 表达式处理命令行参数、文件类型判断、上下文显示等逻辑,代码逻辑清晰,不易出错;
  • 迭代器与闭包:使用 iter().enumerate() 遍历文件行,map()/filter() 处理目录条目与文件筛选,简化代码结构。

想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐