Rust实现类grep工具:grepr的开发与实践
在文本处理和搜索领域,
grep
是一个强大且常用的工具。本文将介绍如何使用Rust语言实现一个类似
grep
的工具
grepr
,涵盖从项目搭建、参数定义到文件搜索和匹配行查找的全过程。
1. 项目起步
要开始开发
grepr
,首先需要创建一个新的Rust项目:
cargo new grepr
接着,将
09_grepr/tests
目录复制到新创建的项目中。同时,在
Cargo.toml
中添加所需的依赖:
[dependencies]
clap = "2.33"
regex = "1"
walkdir = "2"
sys-info = "0.9"
[dev-dependencies]
assert_cmd = "1"
predicates = "1"
rand = "0.8"
运行
cargo test
进行初始构建和测试,此时所有测试应该都会失败。
2. 参数定义
在
src/main.rs
中,我们可以这样开始:
fn main() {
if let Err(e) = grepr::get_args().and_then(grepr::run) {
eprintln!("{}", e);
std::process::exit(1);
}
}
在
src/lib.rs
中,定义配置结构体
Config
:
use clap::{App, Arg};
use regex::{Regex, RegexBuilder};
use std::error::Error;
type MyResult<T> = Result<T, Box<dyn Error>>;
#[derive(Debug)]
pub struct Config {
pattern: Regex,
files: Vec<String>,
recursive: bool,
count: bool,
invert_match: bool,
}
Config
结构体包含以下字段:
-
pattern
:编译后的正则表达式。
-
files
:字符串向量,存储输入文件的名称。
-
recursive
:布尔值,用于指示是否递归搜索目录。
-
count
:布尔值,用于指示是否显示匹配项的数量。
-
invert_match
:布尔值,用于指示是否查找不匹配模式的行。
接下来,定义
get_args
函数来解析命令行参数:
pub fn get_args() -> MyResult<Config> {
let matches = App::new("grepr")
.version("0.1.0")
.author("Ken Youens-Clark <kyclark@gmail.com>")
.about("Rust grep")
.arg(
Arg::with_name("pattern")
.value_name("PATTERN")
.help("Search pattern")
.required(true),
)
.arg(
Arg::with_name("files")
.value_name("FILE")
.help("Input file(s)")
.required(true)
.default_value("-")
.min_values(1),
)
.arg(
Arg::with_name("insensitive")
.value_name("INSENSITIVE")
.help("Case-insensitive")
.short("i")
.long("insensitive")
.takes_value(false),
)
.arg(
Arg::with_name("recursive")
.value_name("RECURSIVE")
.help("Recursive search")
.short("r")
.long("recursive")
.takes_value(false),
)
.arg(
Arg::with_name("count")
.value_name("COUNT")
.help("Count occurrences")
.short("c")
.long("count")
.takes_value(false),
)
.arg(
Arg::with_name("invert")
.value_name("INVERT")
.help("Invert match")
.short("v")
.long("invert-match")
.takes_value(false),
)
.get_matches();
let pattern = matches.value_of("pattern").unwrap();
let pattern = RegexBuilder::new(pattern)
.case_insensitive(matches.is_present("insensitive"))
.build()
.map_err(|_| format!("Invalid pattern \"{}\"", pattern))?;
Ok(Config {
pattern,
files: matches.values_of_lossy("files").unwrap(),
recursive: matches.is_present("recursive"),
count: matches.is_present("count"),
invert_match: matches.is_present("invert"),
})
}
get_args
函数的操作步骤如下:
1. 使用
clap
定义命令行参数,包括搜索模式、输入文件、大小写不敏感、递归搜索、匹配计数和反向匹配等选项。
2. 从命令行参数中获取搜索模式,并使用
RegexBuilder
创建正则表达式。
3. 根据命令行参数设置
Config
结构体的各个字段。
3. 正则表达式的语法
正则表达式有多种语法,
grep
提供了不同的选项来支持不同的正则表达式类型:
-
-E, --extended-regexp
:将模式解释为扩展正则表达式。
-
-e pattern, --regexp=pattern
:指定搜索模式。
-
-G, --basic-regexp
:将模式解释为基本正则表达式。
Rust的
regex
库语法类似于Perl风格的正则表达式,但缺少一些特性,如环视和反向引用。因此,
grepr
在处理扩展正则表达式时更像
egrep
,但无法处理需要反向引用的模式。
4. 查找待搜索的文件
为了使用编译后的正则表达式,需要找到所有待搜索的文件。定义
find_files
函数来实现这一功能:
fn find_files(files: &[String], recursive: bool) -> Vec<MyResult<String>> {
let mut results = vec![];
for path in files {
match path.as_str() {
"-" => results.push(Ok(path.to_string())),
_ => match fs::metadata(&path) {
Ok(metadata) => {
if metadata.is_dir() {
if recursive {
for entry in WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
results.push(Ok(entry
.path()
.display()
.to_string()));
}
} else {
results.push(Err(From::from(format!(
"{} is a directory",
path
))));
}
} else if metadata.is_file() {
results.push(Ok(path.to_string()));
}
}
Err(e) => {
results.push(Err(From::from(format!("{}: {}", path, e))))
}
},
}
}
results
}
find_files
函数的操作步骤如下:
1. 初始化一个空向量
results
来存储结果。
2. 遍历每个输入的文件名:
- 如果文件名是
-
,表示从标准输入读取,将其添加到结果向量中。
- 否则,获取文件的元数据:
- 如果是目录:
- 如果启用了递归搜索,遍历目录中的所有文件并添加到结果向量中。
- 否则,返回错误信息。
- 如果是文件,将其添加到结果向量中。
- 如果获取元数据失败,返回错误信息。
为了测试
find_files
函数,可以在
src/lib.rs
中添加测试模块:
#[cfg(test)]
mod tests {
use super::find_files;
use rand::{distributions::Alphanumeric, Rng};
#[test]
fn test_find_files() {
// 验证函数能找到已知存在的文件
let files = find_files(&["./tests/inputs/fox.txt".to_string()], false);
assert_eq!(files.len(), 1);
assert_eq!(files[0].as_ref().unwrap(), "./tests/inputs/fox.txt");
// 验证函数在未启用递归时拒绝目录
let files = find_files(&["./tests/inputs".to_string()], false);
assert_eq!(files.len(), 1);
if let Err(e) = &files[0] {
assert_eq!(
e.to_string(),
"./tests/inputs is a directory".to_string()
);
}
// 验证函数能递归查找目录中的文件
let res = find_files(&["./tests/inputs".to_string()], true);
let mut files: Vec<String> = res
.iter()
.map(|r| r.as_ref().unwrap().replace("\\", "/"))
.collect();
files.sort();
assert_eq!(files.len(), 4);
assert_eq!(
files,
vec![
"./tests/inputs/bustle.txt",
"./tests/inputs/empty.txt",
"./tests/inputs/fox.txt",
"./tests/inputs/nobody.txt",
]
);
// 生成一个随机字符串表示不存在的文件
let bad: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect();
// 验证函数将不存在的文件作为错误返回
let files = find_files(&[bad], false);
assert_eq!(files.len(), 1);
assert!(files[0].is_err());
}
}
5. 查找匹配的输入行
在正确处理输入文件后,需要打开文件并查找匹配的行。定义
open
函数来打开文件或标准输入:
fn open(filename: &str) -> MyResult<Box<dyn BufRead>> {
match filename {
"-" => Ok(Box::new(BufReader::new(io::stdin()))),
_ => Ok(Box::new(BufReader::new(File::open(filename)?))),
}
}
定义
find_lines
函数来查找匹配的行:
fn find_lines<T: BufRead>(
mut file: T,
pattern: &Regex,
invert_match: bool,
) -> MyResult<Vec<String>> {
let mut matches = vec![];
let mut line = String::new();
loop {
let bytes = file.read_line(&mut line)?;
if bytes == 0 {
break;
}
if (pattern.is_match(&line) && !invert_match)
|| (!pattern.is_match(&line) && invert_match)
{
matches.push(line.clone());
}
line.clear();
}
Ok(matches)
}
find_lines
函数的操作步骤如下:
1. 初始化一个空向量
matches
来存储匹配的行。
2. 逐行读取文件:
- 如果读取到文件末尾,退出循环。
- 如果当前行匹配模式且不进行反向匹配,或者不匹配模式且进行反向匹配,将该行添加到匹配向量中。
- 清空当前行。
3. 返回匹配的行向量。
为了测试
find_lines
函数,可以在测试模块中添加相应的测试:
#[cfg(test)]
mod test {
use super::{find_files, find_lines};
use rand::{distributions::Alphanumeric, Rng};
use regex::{Regex, RegexBuilder};
use std::io::Cursor;
#[test]
fn test_find_lines() {
let text = b"Lorem\nIpsum\r\nDOLOR";
// 模式 "or" 应匹配一行 "Lorem"
let re1 = Regex::new("or").unwrap();
let matches = find_lines(Cursor::new(&text), &re1, false);
assert!(matches.is_ok());
assert_eq!(matches.unwrap().len(), 1);
// 反向匹配时,函数应匹配另外两行
let matches = find_lines(Cursor::new(&text), &re1, true);
assert!(matches.is_ok());
assert_eq!(matches.unwrap().len(), 2);
// 这个正则表达式是大小写不敏感的
let re2 = RegexBuilder::new("or")
.case_insensitive(true)
.build()
.unwrap();
// 两行 "Lorem" 和 "DOLOR" 应匹配
let matches = find_lines(Cursor::new(&text), &re2, false);
assert!(matches.is_ok());
assert_eq!(matches.unwrap().len(), 2);
// 反向匹配时,剩下的一行应匹配
let matches = find_lines(Cursor::new(&text), &re2, true);
assert!(matches.is_ok());
assert_eq!(matches.unwrap().len(), 1);
}
#[test]
fn test_find_files() {} // 与之前相同
}
6. 运行程序
最后,定义
run
函数来运行整个程序:
pub fn run(config: Config) -> MyResult<()> {
let entries = find_files(&config.files, config.recursive);
let num_files = &entries.len();
let print = |fname: &str, val: &str| {
if num_files > &1 {
print!("{}:{}", fname, val);
} else {
print!("{}", val);
}
};
for entry in entries {
match entry {
Err(e) => eprintln!("{}", e),
Ok(filename) => match open(&filename) {
Err(e) => eprintln!("{}: {}", filename, e),
Ok(file) => {
match find_lines(
file,
&config.pattern,
config.invert_match,
) {
Err(e) => eprintln!("{}", e),
Ok(matches) => {
if config.count {
print(
&filename,
&format!("{}\n", &matches.len()),
);
} else {
for line in matches {
print(&filename, &line);
}
}
}
}
}
},
}
}
Ok(())
}
run
函数的操作步骤如下:
1. 调用
find_files
函数查找所有待搜索的文件。
2. 计算输入文件的数量。
3. 定义一个闭包
print
来根据输入文件的数量决定是否打印文件名。
4. 遍历每个文件:
- 如果出现错误,打印错误信息。
- 否则,打开文件:
- 如果打开文件失败,打印错误信息。
- 否则,调用
find_lines
函数查找匹配的行:
- 如果出现错误,打印错误信息。
- 否则,如果启用了计数选项,打印匹配的行数;否则,打印匹配的行。
通过以上步骤,我们就完成了一个类似
grep
的工具
grepr
的开发。在开发过程中,我们学习了如何使用Rust的
clap
、
regex
和
walkdir
库来处理命令行参数、正则表达式和文件搜索,同时也掌握了如何编写测试代码来确保函数的正确性。
以下是整个过程的流程图:
graph TD;
A[项目起步] --> B[参数定义];
B --> C[正则表达式语法];
C --> D[查找待搜索的文件];
D --> E[查找匹配的输入行];
E --> F[运行程序];
通过逐步实现这些功能,我们可以构建一个功能强大且稳定的文本搜索工具。在开发过程中,遇到困难时可以参考
grep
的输出,对比自己程序的输出,找出差异并解决问题。不断运行测试用例,确保每个功能都能正常工作。希望本文能帮助你更好地理解如何使用Rust实现一个类似
grep
的工具。
Rust实现类grep工具:grepr的开发与实践
7. 功能测试与验证
在完成
grepr
的开发后,需要对其各项功能进行全面测试,以确保其正确性和稳定性。以下是一些常见的测试场景及对应的命令示例:
7.1 默认输入为标准输入
cargo run -- fox
此命令会使用默认输入,即从标准输入读取数据,并查找包含 “fox” 的行。预期输出如下:
pattern "fox"
file "-"
7.2 显式指定标准输入
cargo run -- fox -
该命令显式指定从标准输入读取数据,查找包含 “fox” 的行,输出与上一个命令相同:
pattern "fox"
file "-"
7.3 处理多个输入文件
cargo run -- fox tests/inputs/*
此命令会在
tests/inputs
目录下的所有文件中查找包含 “fox” 的行,输出结果会列出每个匹配的文件及对应的行:
pattern "fox"
file "tests/inputs/bustle.txt"
file "tests/inputs/empty.txt"
file "tests/inputs/fox.txt"
file "tests/inputs/nobody.txt"
7.4 拒绝非递归的目录输入
cargo run -- fox tests/inputs
当未使用
--recursive
选项时,直接指定目录作为输入会导致错误,输出如下:
pattern "fox"
tests/inputs is a directory
7.5 递归搜索目录
cargo run -- -r fox tests/inputs
使用
--recursive
选项后,程序会递归搜索
tests/inputs
目录下的所有文件,查找包含 “fox” 的行:
pattern "fox"
file "tests/inputs/empty.txt"
file "tests/inputs/nobody.txt"
file "tests/inputs/bustle.txt"
file "tests/inputs/fox.txt"
7.6 处理不存在的文件
cargo run -- fox blargh tests/inputs/fox.txt
当输入中包含不存在的文件时,程序会输出相应的错误信息:
pattern "fox"
blargh: No such file or directory (os error 2)
file "tests/inputs/fox.txt"
7.7 打印匹配的行数
cargo run -- --count The tests/inputs/*
使用
--count
选项后,程序会输出每个文件中匹配的行数:
tests/inputs/bustle.txt:3
tests/inputs/empty.txt:0
tests/inputs/fox.txt:1
tests/inputs/nobody.txt:1
7.8 大小写不敏感搜索
cargo run -- --count --insensitive The tests/inputs/*
使用
--insensitive
选项后,程序会进行大小写不敏感的搜索,并输出每个文件中匹配的行数:
tests/inputs/bustle.txt:3
tests/inputs/empty.txt:0
tests/inputs/fox.txt:1
tests/inputs/nobody.txt:3
7.9 反向匹配
cargo run -- --count --invert-match The tests/inputs/*
使用
--invert-match
选项后,程序会查找不匹配模式的行,并输出每个文件中不匹配的行数:
tests/inputs/bustle.txt:6
tests/inputs/empty.txt:0
tests/inputs/fox.txt:0
tests/inputs/nobody.txt:8
8. 总结与展望
通过以上步骤,我们成功开发了一个类似
grep
的工具
grepr
。在开发过程中,我们深入学习了如何使用Rust的
clap
、
regex
和
walkdir
库来处理命令行参数、正则表达式和文件搜索,同时也掌握了如何编写测试代码来确保函数的正确性。
然而,
grepr
仍然有一些可以改进的地方。例如,目前的实现只支持基本的正则表达式语法,对于一些复杂的正则表达式,如需要反向引用的模式,还无法处理。未来可以考虑扩展正则表达式的支持,使其能够处理更多类型的模式。
另外,性能也是一个可以优化的方向。在处理大量文件或大文件时,程序的运行速度可能会受到影响。可以通过并行处理、优化文件读取和匹配算法等方式来提高程序的性能。
以下是对
grepr
功能和未来改进方向的总结表格:
| 功能 | 实现情况 | 未来改进方向 |
| ---- | ---- | ---- |
| 命令行参数处理 | 使用
clap
库实现 | 无 |
| 正则表达式匹配 | 使用
regex
库实现,支持基本语法 | 扩展支持更多复杂的正则表达式语法 |
| 文件搜索 | 使用
walkdir
库实现,支持递归搜索 | 无 |
| 匹配计数 | 支持 | 无 |
| 反向匹配 | 支持 | 无 |
| 大小写不敏感搜索 | 支持 | 无 |
| 性能 | 基本满足需求,处理大量文件或大文件时可能较慢 | 并行处理、优化文件读取和匹配算法 |
通过不断地改进和优化,
grepr
可以成为一个更加强大、高效的文本搜索工具。希望本文能为你在Rust开发和文本处理方面提供一些有用的参考和启示。
超级会员免费看

被折叠的 条评论
为什么被折叠?



