From 833abc1698abb3ab1ce4e4014128251b164f31d2 Mon Sep 17 00:00:00 2001 From: wxy Date: Thu, 28 May 2026 17:08:11 +0800 Subject: [PATCH] first commit --- .gitignore | 1 + Cargo.lock | 110 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 ++++++ src/lib.rs | 24 +++++++++ src/main.rs | 71 ++++++++++++++++++++++++++ src/searcher/core.rs | 11 +++++ src/searcher/filter.rs | 48 ++++++++++++++++++ src/searcher/mod.rs | 5 ++ test.txt | 28 +++++++++++ 9 files changed, 313 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/searcher/core.rs create mode 100644 src/searcher/filter.rs create mode 100644 src/searcher/mod.rs create mode 100644 test.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..80a6e1a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,110 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "rustgrep" +version = "0.1.0" +dependencies = [ + "crossbeam", + "walkdir", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..98a6636 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rustgrep" +version = "0.1.0" +edition = "2024" + +[profile.release] +opt-level = "z" # 告诉编译器:放弃优化文件大小("z" 表示追求最小体积) +lto = true # 开启全链接优化(Link-Time Optimization),把没用到的依赖代码彻底删干净 +codegen-units = 1 # 降低并行编译限制,让编译器能更激进地优化代码 +panic = "abort" # 发生崩溃时直接退出,不保留复杂的栈回溯信息(能省巨多体积) +strip = true # 自动剥离所有的调试信息和符号表(相当于执行了 Linux 的 strip 命令) + +[dependencies] +walkdir = "2" +crossbeam = "0.8" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fdcdfe6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,24 @@ +use std::fs; +use std::path::Path; + +pub mod searcher; +pub use searcher::{ should_skip_dir, is_valid_extension }; + +// 处理单个文件搜索 +pub fn search_in_file(query: &str, path: &Path, ignore_case: bool) -> Result<(), std::io::Error> { + let contents = fs::read_to_string(path)?; + // 🌟 2. 跨模块调用:通过包名或相对路径调用 searcher 里的函数 + let results = searcher::search(query, &contents, ignore_case); + + if !results.is_empty() { + println!("\x1b[35m{:?} :\x1b[0m", path); + let total_lines = contents.lines().count(); + let max_width = total_lines.to_string().len(); + + for (line_num, line) in results { + println!("\x1b[32m{:>width$} |\x1b[0m {}", line_num, line, width = max_width); + } + println!(); + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ff71599 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,71 @@ +use std::env; +use std::path::Path; +use walkdir::WalkDir; + +use rustgrep::{ search_in_file, should_skip_dir, is_valid_extension }; + +fn main() { + // 1.收集命令行参数到一个 Vector + let args: Vec = env::args().collect(); + + // 2.简单的边界检查,防止索引越界 + if args.len() < 3 { + println!("用法提示:minigrep <搜索关键字> <文件路径>"); + return; + } + + let query = &args[1].clone(); + let target_path = &args[2]; + + let extensions_str: Option = if args.len() >= 4 { Some(args[3].clone()) } else { None }; + + // 检查环境变量是否设置了 IGNORE_CASE 只要有值就是true + let ignore_case = env::var("IGNORE_CASE").is_ok(); + + let temp_query: String; + + let real_query = if ignore_case { + temp_query = query.to_lowercase(); + &temp_query + } else { + query + }; + + // 判断目标路径是否存在 + if !Path::new(target_path).exists() { + eprintln!("错误:路径 '{}' 不存在", target_path); + std::process::exit(1); + } + + let mut it = WalkDir::new(target_path).into_iter(); + + // 使用WalkDir递归遍历路径 + while let Some(entry) = it.next() { + // 通过match安全处理异常 + match entry { + Ok(dir_entry) => { + let path = dir_entry.path(); + + if path.is_dir() && should_skip_dir(path) { + it.skip_current_dir(); + continue; + } + + // 只搜索文件 跳过文件夹 + if path.is_file() { + if !is_valid_extension(path, &extensions_str) { + continue; + } + + if let Err(e) = search_in_file(real_query, path, ignore_case) { + // 只在严重错误时打印警告 + if e.kind() != std::io::ErrorKind::InvalidData { + eprintln!("警告: 无法读取文件 {:?}: {}", path, e); + } + } + } + } + Err(e) => eprintln!("遍历目录时出错: {}", e), + } + } +} diff --git a/src/searcher/core.rs b/src/searcher/core.rs new file mode 100644 index 0000000..66fde89 --- /dev/null +++ b/src/searcher/core.rs @@ -0,0 +1,11 @@ +/// 核心搜索函数 <'a> 是显式生命周期注解 +pub fn search<'a>(query: &str, contents: &'a str, ignore_case: bool) -> Vec<(usize, &'a str)> { + contents + .lines() + .enumerate() + .map(|(idx, line)| (idx + 1, line)) + .filter(|(_, line)| { + if ignore_case { line.to_lowercase().contains(&query) } else { line.contains(query) } + }) + .collect() +} diff --git a/src/searcher/filter.rs b/src/searcher/filter.rs new file mode 100644 index 0000000..befedb4 --- /dev/null +++ b/src/searcher/filter.rs @@ -0,0 +1,48 @@ +use std::path::Path; +use std::sync::OnceLock; + +// 定义静态黑名单目录数组 +const IGNORE_DIRS: &[&str] = &["target", ".git", "node_modules", ".idea", "build"]; + +static EXT_LIST: OnceLock> = OnceLock::new(); + +/// 判断是否需要跳过该目录(True 代表需要拦截并剪枝) +pub fn should_skip_dir(path: &Path) -> bool { + path.file_name() + .and_then(|name| name.to_str()) + .map_or(false, |name| IGNORE_DIRS.contains(&name)) +} + +/// 判断文件是否满足后缀过滤规则(True 代表匹配成功,可以读取) +pub fn is_valid_extension(path: &Path, extensions_str: &Option) -> bool { + // 1. 没有参数直接放行 + let ext_list_str = match extensions_str { + Some(s) if !s.trim().is_empty() => s, + _ => { + return true; + } + }; + + let extensions = EXT_LIST.get_or_init(move || { + ext_list_str + .split(",") + .map(|s| s.trim().to_lowercase()) + .filter(|s| !s.is_empty()) + .collect::>() + }); + + let has_white_list = extensions.iter().any(|s| !s.starts_with('!')); + + // 如果传了后缀限制,拿当前文件的后缀去匹配 + path.extension() + .and_then(|os_str| os_str.to_str()) + .map(|s| s.to_lowercase()) + .map_or(false, |ext| { + let ignore = format!("!{}", ext); + if extensions.contains(&ignore) { + false + } else { + if has_white_list { extensions.contains(&ext) } else { true } + } + }) +} diff --git a/src/searcher/mod.rs b/src/searcher/mod.rs new file mode 100644 index 0000000..50a58ed --- /dev/null +++ b/src/searcher/mod.rs @@ -0,0 +1,5 @@ +mod core; +mod filter; + +pub use core::search; +pub use filter::{ should_skip_dir, is_valid_extension }; diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..ed65495 --- /dev/null +++ b/test.txt @@ -0,0 +1,28 @@ +Rust: +safe, fast, productive. +Pick three. +Trust me. +Rust: +safe, fast, productive. +Pick three. +Trust me. +Rust: +safe, fast, productive. +Pick three. +Trust me. +Rust: +safe, fast, productive. +Pick three. +Trust me. +Rust: +safe, fast, productive. +Pick three. +Trust me. +Rust: +safe, fast, productive. +Pick three. +Trust me. +Rust: +safe, fast, productive. +Pick three. +Trust me. \ No newline at end of file