16、Rust实现类grep工具:grepr的开发与实践

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开发和文本处理方面提供一些有用的参考和启示。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值