12、编程挑战:文件处理与查找工具实现

编程挑战:文件处理与查找工具实现

一、文件处理程序的实现与优化

在编程过程中,我们常常会遇到处理文件内容的需求,比如统计重复行的数量。下面我们来详细探讨如何实现这样一个程序,并对其进行优化。

1. 初始实现

首先,我们创建了一个函数 run 来处理文件内容。为了记录最后一行文本和当前行的重复计数,我们引入了两个可变变量 last count 。以下是初始的代码实现:

pub fn run(config: Config) -> MyResult<()> {
    let mut file = open(&config.in_file)
        .map_err(|e| format!("{}: {}", config.in_file, e))?;
    let mut line = String::new();
    let mut last = String::new(); 
    let mut count: u64 = 0; 
    loop {
        let bytes = file.read_line(&mut line)?;
        if bytes == 0 {
            break;
        }
        if line.trim_end() != last.trim_end() { 
            if count > 0 { 
                print!("{:>4} {}", count, last); 
            }
            last = line.clone(); 
            count = 0; 
        }
        count += 1; 
        line.clear();
    }
    if count > 0 { 
        print!("{:>4} {}", count, last);
    }
    Ok(())
}

这段代码的执行步骤如下:
1. 打开输入文件,如果打开失败则处理错误。
2. 进入循环,逐行读取文件内容。
3. 当读取到文件末尾( bytes == 0 )时,跳出循环。
4. 比较当前行和上一行(去除行尾空白字符后),如果不同且计数大于 0,则打印计数和上一行内容。
5. 更新 last 为当前行,并将计数重置为 0。
6. 计数加 1,并清空当前行。
7. 处理文件的最后一行。

2. 代码优化

初始代码虽然能够完成基本功能,但存在一些问题,比如重复检查 count > 0 ,违反了 DRY(Don’t Repeat Yourself)原则。为了解决这个问题,我们引入了一个闭包 print 来封装打印逻辑:

let print = |count: &u64, line: &String| { 
    if count > &0 { 
        if config.count { 
            print!("{:>4} {}", &count, &line); 
        } else {
            print!("{}", &line); 
        }
    };
};

闭包 print 的工作流程如下:
1. 检查计数是否大于 0。
2. 如果 config.count true ,则打印计数和行内容;否则,只打印行内容。

使用闭包后,我们对 run 函数进行了更新:

loop {
    let bytes = file.read_line(&mut line)?;
    if bytes == 0 {
        break;
    }
    if line.trim_end() != last.trim_end() {
        print(&count, &last);
        last = line.clone();
        count = 0;
    }
    count += 1;
    line.clear();
}
print(&count, &last);
3. 输出文件处理

为了支持将结果输出到指定文件,我们需要进行一些额外的修改。首先,我们引入了 std::io::Write 特性,并根据 config.out_file 的值来决定是创建一个新文件还是使用标准输出:

let mut out_file: Box<dyn Write> = match &config.out_file { 
    Some(out_name) => Box::new(File::create(&out_name)?), 
    _ => Box::new(io::stdout()), 
};

然后,我们将 print 闭包中的 print! 宏替换为 write! 宏,以将输出写入到 out_file 中:

let mut print = |count: &u64, line: &String| -> MyResult<()> { 
    if count > &0 {
        if config.count {
            write!(out_file, "{:>4} {}", &count, &line)?;
        } else {
            write!(out_file, "{}", &line)?;
        }
    };
    Ok(())
};

最终的 run 函数如下:

pub fn run(config: Config) -> MyResult<()> {
    let mut file = open(&config.in_file)
        .map_err(|e| format!("{}: {}", config.in_file, e))?; 
    let mut out_file: Box<dyn Write> = match &config.out_file { 
        Some(out_name) => Box::new(File::create(&out_name)?),
        _ => Box::new(io::stdout()),
    };
    let mut print = |count: &u64, line: &String| -> MyResult<()> { 
        if count > &0 {
            if config.count {
                write!(out_file, "{:>4} {}", &count, &line)?;
            } else {
                write!(out_file, "{}", &line)?;
            }
        };
        Ok(())
    };
    let mut line = String::new();
    let mut last = String::new();
    let mut count: u64 = 0;
    loop {
        let bytes = file.read_line(&mut line)?;
        if bytes == 0 {
            break;
        }
        if line.trim_end() != last.trim_end() {
            print(&count, &last)?; 
            last = line.clone();
            count = 0;
        }
        count += 1;
        line.clear();
    }
    print(&count, &last)?; 
    Ok(())
}

这个最终版本的函数完成了以下几个关键步骤:
1. 打开输入文件。
2. 根据配置打开输出文件或使用标准输出。
3. 创建一个可变的 print 闭包来格式化输出。
4. 使用 print 闭包打印输出,并处理可能的错误。
5. 处理文件的最后一行。

二、文件查找工具 findr 的实现

除了文件处理程序,我们还可以实现一个文件查找工具 findr ,它可以根据指定的条件在一个或多个目录中查找文件、目录或链接。

1. find 工具概述

find 是一个强大的工具,它可以递归地搜索指定目录下的所有条目,包括文件、符号链接、套接字、目录等。它支持多种选项,如 -type 用于指定条目类型, -name 用于指定文件名模式等。以下是一些常见的 find 命令示例:
| 命令 | 功能 |
| — | — |
| find . | 递归搜索当前目录下的所有条目 |
| find . -type f | 只查找文件 |
| find . -type l | 只查找链接 |
| find . -type d | 只查找目录 |
| find . -name "*.csv" | 查找文件名以 .csv 结尾的条目 |
| find . -name "*.txt" -o -name "*.csv" | 查找文件名以 .txt .csv 结尾的条目 |

2. findr 项目初始化

我们将创建一个名为 findr 的项目,并在 Cargo.toml 中添加必要的依赖:

[dependencies]
clap = "2.33"
walkdir = "2"
regex = "1"
[dev-dependencies]
assert_cmd = "1"
predicates = "1"
rand = "0.8"
sys-info = "0.9" 

由于测试目录中的符号链接在复制时可能会丢失,我们使用一个 bash 脚本 cp-tests.sh 来复制测试文件:

$ ./cp-tests.sh
Usage: cp-tests.sh DEST_DIR
$ ./cp-tests.sh ~/work/rust/findr
Copying "tests" to "/Users/kyclark/work/rust/findr"
Fixing symlink
Done.

运行 cargo test 可以构建程序并运行测试,初始时所有测试应该都会失败。

3. 定义命令行参数

我们使用 clap 来定义 findr 的命令行接口:

fn main() {
    if let Err(e) = findr::get_args().and_then(findr::run) {
        eprintln!("{}", e);
        std::process::exit(1);
    }
}

预期的命令行接口如下:

$ cargo run -- --help
findr 0.1.0
Ken Youens-Clark <kyclark@gmail.com>
Rust find
USAGE:
    findr [OPTIONS] [--] [DIR]... 
FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information
OPTIONS:
    -n, --name <NAME>...    Name 
    -t, --type <TYPE>...    Entry type [possible values: f, d, l] 
ARGS:
    <DIR>...    Search directory [default: .] 

这里, -n|--name 选项可以指定一个或多个文件名模式, -t|--type 选项可以指定一个或多个条目类型( f 表示文件, d 表示目录, l 表示链接)。

4. 数据结构定义

为了更好地管理命令行参数,我们定义了一些数据结构:

use crate::EntryType::*; 
use clap::{App, Arg};
use regex::Regex;
use std::error::Error;
type MyResult<T> = Result<T, Box<dyn Error>>;
#[derive(Debug, PartialEq)] 
enum EntryType {
    Dir,
    File,
    Link,
}
#[derive(Debug)]
pub struct Config {
    dirs: Vec<String>, 
    names: Option<Vec<Regex>>, 
    entry_types: Option<Vec<EntryType>>, 
}

EntryType 是一个枚举类型,用于表示条目类型; Config 结构体用于存储命令行参数,包括搜索目录、文件名模式和条目类型。

5. 命令行参数解析

我们可以使用以下代码来开始实现 get_args 函数:

pub fn get_args() -> MyResult<Config> {
    let matches = App::new("findr")
        .version("0.1.0")
        .author("Ken Youens-Clark <kyclark@gmail.com>")
        .about("Rust find")
        // What goes here?
        .matches()
    Ok(Config {
        dirs: ...
        names: ...
        entry_types: ...
    })
}

run 函数中,我们可以先打印配置信息:

pub fn run(config: Config) -> MyResult<()> {
    println!("{:?}", config);
    Ok(())
}

当不提供任何参数时,默认的 Config 值如下:

$ cargo run
Config { dirs: ["."], names: None, entry_types: None }

当提供 --type 参数时, entry_types 会相应地更新:

$ cargo run -- --type f
Config { dirs: ["."], names: None, entry_types: Some([File]) }
$ cargo run -- --type d
Config { dirs: ["."], names: None, entry_types: Some([Dir]) }
$ cargo run -- --type l
Config { dirs: ["."], names: None, entry_types: Some([Link]) }

如果提供的 --type 值不在 f d l 范围内,程序会报错:

$ cargo run -- --type x
error: 'x' isn't a valid value for '--type <TYPE>...'
[possible values: d, f, l]
USAGE:
    findr --type <TYPE>
For more information try --help
6. 正则表达式匹配

在使用 --name 选项时,我们需要使用正则表达式来匹配文件名。需要注意的是,正则表达式的语法与文件通配符模式有所不同。例如,文件通配符 *.txt 表示以 .txt 结尾的文件,而在正则表达式中,需要使用 .*\.txt 来实现相同的匹配。为了确保正则表达式匹配字符串的结尾,可以在模式末尾添加 $

let re = Regex::new(".*[.]csv$").unwrap();
assert!(re.is_match("foo.csv"));
assert!(!re.is_match(".csv.foo"));

通过以上步骤,我们实现了一个文件处理程序和一个文件查找工具,并对代码进行了优化和完善。这些程序不仅可以帮助我们处理文件内容,还可以根据指定条件查找文件,提高了我们的工作效率。

三、 findr 工具的进一步实现与优化
1. 递归搜索文件路径

为了实现 findr 工具的核心功能,我们需要递归地搜索指定目录下的所有文件和目录。这里我们使用 walkdir crate 来完成这个任务。以下是一个简单的示例代码,展示了如何使用 walkdir 来遍历目录:

use walkdir::WalkDir;

fn search_dirs(dirs: &[String]) {
    for dir in dirs {
        for entry in WalkDir::new(dir) {
            let entry = entry.expect("Failed to read directory entry");
            println!("{}", entry.path().display());
        }
    }
}

这个函数接受一个目录路径的切片作为参数,然后使用 WalkDir::new 来创建一个迭代器,遍历指定目录下的所有条目,并打印出它们的路径。

2. 过滤条目类型和文件名

在遍历目录的过程中,我们需要根据用户指定的 --type --name 选项来过滤条目。以下是一个更新后的 search_dirs 函数,它会根据 Config 结构体中的信息进行过滤:

use walkdir::WalkDir;
use regex::Regex;
use crate::EntryType::*;

fn search_dirs(config: &Config) {
    for dir in &config.dirs {
        for entry in WalkDir::new(dir) {
            let entry = entry.expect("Failed to read directory entry");
            let path = entry.path();

            // 过滤条目类型
            if let Some(entry_types) = &config.entry_types {
                let is_valid_type = match (path.is_dir(), path.is_file(), path.is_symlink()) {
                    (true, false, false) => entry_types.contains(&Dir),
                    (false, true, false) => entry_types.contains(&File),
                    (false, false, true) => entry_types.contains(&Link),
                    _ => false,
                };
                if!is_valid_type {
                    continue;
                }
            }

            // 过滤文件名
            if let Some(names) = &config.names {
                let path_str = path.to_str().unwrap();
                let is_valid_name = names.iter().any(|re| re.is_match(path_str));
                if!is_valid_name {
                    continue;
                }
            }

            println!("{}", path.display());
        }
    }
}

这个函数的工作流程如下:
1. 遍历用户指定的每个目录。
2. 对于每个目录条目,首先检查是否需要过滤条目类型。如果指定了 --type 选项,根据条目是目录、文件还是链接,检查是否符合用户指定的类型。
3. 然后检查是否需要过滤文件名。如果指定了 --name 选项,使用正则表达式检查文件名是否匹配用户指定的模式。
4. 如果条目通过了类型和文件名的过滤,打印出条目路径。

3. 完整的 run 函数

结合前面的代码,我们可以实现一个完整的 run 函数,它会解析命令行参数,然后调用 search_dirs 函数进行文件搜索:

pub fn run(config: Config) -> MyResult<()> {
    search_dirs(&config);
    Ok(())
}

这个函数接受一个 Config 结构体作为参数,然后调用 search_dirs 函数进行文件搜索。如果搜索过程中没有出现错误,返回 Ok(())

四、总结与拓展
1. 总结

通过以上的实现,我们完成了一个简单的文件处理程序和一个文件查找工具 findr 。在文件处理程序中,我们学会了如何逐行读取文件内容,统计重复行的数量,并将结果输出到文件或标准输出。在 findr 工具的实现中,我们掌握了以下关键技术:
- 使用 clap 来解析命令行参数,限制用户输入的范围。
- 定义枚举类型和结构体来管理命令行参数。
- 使用 walkdir crate 递归地搜索文件路径。
- 使用正则表达式来匹配文件名。
- 过滤条目类型和文件名,只输出符合用户条件的条目。

2. 拓展建议

虽然我们已经实现了 findr 工具的基本功能,但它还有很多可以拓展的地方。以下是一些建议:
- 增加更多的过滤选项 :除了 --type --name 选项,还可以添加更多的过滤条件,如文件大小、修改时间、权限等。
- 支持更多的输出格式 :目前我们只是简单地打印出符合条件的条目路径,可以支持更多的输出格式,如 JSON、CSV 等。
- 优化性能 :对于大型目录结构,递归搜索可能会比较慢。可以考虑使用多线程或并行处理来提高搜索效率。
- 错误处理和日志记录 :增加更详细的错误处理和日志记录功能,帮助用户更好地调试和使用工具。

总结

本文详细介绍了文件处理程序和文件查找工具 findr 的实现过程。通过逐步优化代码,我们解决了初始代码中存在的问题,提高了代码的可读性和可维护性。同时,我们还学习了如何使用 Rust 的一些特性,如闭包、枚举类型、正则表达式等,来实现复杂的功能。希望这些内容能够帮助你更好地理解和掌握 Rust 编程,以及如何实现实用的命令行工具。

流程图: findr 工具搜索流程

graph LR
    A[开始] --> B[解析命令行参数]
    B --> C[遍历指定目录]
    C --> D{是否指定条目类型}
    D -- 是 --> E{条目类型是否符合}
    E -- 否 --> C
    E -- 是 --> F{是否指定文件名模式}
    D -- 否 --> F
    F -- 是 --> G{文件名是否匹配}
    G -- 否 --> C
    G -- 是 --> H[输出条目路径]
    F -- 否 --> H
    H --> I{是否还有条目}
    I -- 是 --> C
    I -- 否 --> J[结束]

表格: findr 工具命令行选项总结

选项 描述 示例
-n, --name <NAME>... 指定文件名模式,使用正则表达式匹配 --name ".*\.txt"
-t, --type <TYPE>... 指定条目类型,可选值为 f (文件)、 d (目录)、 l (链接) --type f
<DIR>... 搜索目录,默认为当前目录 a/b d

通过以上的总结和拓展建议,你可以进一步完善 findr 工具,使其更加实用和强大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值