17、Rust 实现文件比较工具:commr 详解

Rust 实现文件比较工具:commr 详解

1. 背景与基础概念

在数据处理和文本分析中,经常需要比较两个文件的内容,找出它们的共同行和独特行。 comm 工具就是为此而生,它能读取两个已排序的文件,并输出三列文本:仅在文件 1 中的行、仅在文件 2 中的行以及两个文件共有的行。

2. comm 工具介绍
  • BSD comm 工具
    • 功能 comm 工具读取两个按字典序排序的文件 file1 file2 ,并输出三列文本。
    • 选项
      • -1 :抑制第一列的打印。
      • -2 :抑制第二列的打印。
      • -3 :抑制第三列的打印。
      • -i :对行进行不区分大小写的比较。
    • 示例
# 假设 example.txt 文件内容如下
a
b
c
d
# 显示仅在 example.txt 中的行、仅在 stdin 中的行和共同行
$ echo -e "B\nc" | comm example.txt -
# 输出结果
  B
a
b

c
d
# 不区分大小写比较,仅显示共同行
$ echo -e "B\nc" | comm -1 -2 -i example.txt -
b
c
  • GNU comm 工具
    • 功能 :与 BSD comm 类似,但有一些额外选项。
    • 额外选项
      • --check-order :检查输入是否正确排序。
      • --nocheck-order :不检查输入是否正确排序。
      • --output-delimiter=STR :用指定字符串分隔列。
      • --total :输出摘要。
      • -z, --zero-terminated :行分隔符为 NUL,而不是换行符。
    • 示例
# 仅显示两个文件中都存在的行
$ comm -12 file1 file2
# 显示文件 1 中不在文件 2 中的行,反之亦然
$ comm -3 file1 file2
3. commr 项目起步

为了在 Rust 中实现类似 comm 的功能,我们将创建一个名为 commr 的项目。
- 创建项目

cargo new commr
  • 添加依赖 :在 Cargo.toml 文件中添加以下依赖:
[dependencies]
clap = "2.33"

[dev-dependencies]
assert_cmd = "1"
predicates = "1"
rand = "0.8"
  • 复制测试目录 :将测试目录复制到项目中,并运行测试:
cp -r 10_commr/tests commr/
cd commr
cargo test
4. 定义参数

src/main.rs 中,我们可以这样定义程序的入口:

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

src/lib.rs 中,定义 Config 结构体来存储程序的配置:

use clap::{App, Arg};
use std::error::Error;
type MyResult<T> = Result<T, Box<dyn Error>>;

#[derive(Debug)]
pub struct Config {
    file1: String, 
    file2: String, 
    suppress_col1: bool, 
    suppress_col2: bool, 
    suppress_col3: bool, 
    insensitive: bool, 
    delimiter: String, 
}

pub fn get_args() -> MyResult<Config> {
    let matches = App::new("commr")
       .version("0.1.0")
       .author("Ken Youens-Clark <kyclark@gmail.com>")
       .about("Rust comm")
       .arg(
            Arg::with_name("file1")
               .value_name("FILE1")
               .help("Input file 1")
               .takes_value(true)
               .required(true),
        )
       .arg(
            Arg::with_name("file2")
               .value_name("FILE2")
               .help("Input file 2")
               .takes_value(true)
               .required(true),
        )
       .arg(
            Arg::with_name("suppress_col1")
               .short("1")
               .value_name("COL1")
               .takes_value(false)
               .help("Suppress printing of column 1"),
        )
       .arg(
            Arg::with_name("suppress_col2")
               .short("2")
               .value_name("COL2")
               .takes_value(false)
               .help("Suppress printing of column 2"),
        )
       .arg(
            Arg::with_name("suppress_col3")
               .short("3")
               .value_name("COL3")
               .takes_value(false)
               .help("Suppress printing of column 3"),
        )
       .arg(
            Arg::with_name("insensitive")
               .short("i")
               .value_name("INSENSITIVE")
               .takes_value(false)
               .help("Case insensitive comparison of lines"),
        )
       .arg(
            Arg::with_name("delimiter")
               .short("d")
               .long("output-delimiter")
               .value_name("DELIM")
               .help("Output delimiter")
               .takes_value(true),
        )
       .get_matches();
    Ok(Config {
        file1: matches.value_of("file1").unwrap().to_string(),
        file2: matches.value_of("file2").unwrap().to_string(),
        suppress_col1: matches.is_present("suppress_col1"),
        suppress_col2: matches.is_present("suppress_col2"),
        suppress_col3: matches.is_present("suppress_col3"),
        insensitive: matches.is_present("insensitive"),
        delimiter: matches.value_of("delimiter").unwrap_or("\t").to_string(), 
    })
}

pub fn run(config: Config) -> MyResult<()> {
    println!("{:#?}", config);
    Ok(())
}
5. 验证和打开输入文件

为了验证和打开输入文件,我们可以定义一个 open 函数:

use clap::{App, Arg};
use std::{
    error::Error,
    fs::File,
    io::{self, BufRead, BufReader},
};

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)
               .map_err(|e| format!("{}: {}", filename, e))?, 
        ))),
    }
}

pub fn run(config: Config) -> MyResult<()> {
    let filename1 = &config.file1;
    let filename2 = &config.file2;
    if filename1.as_str() == "-" && filename2.as_str() == "-" { 
        return Err(From::from("Both input files cannot be STDIN (\"-\")"));
    }
    let _file1 = open(&filename1)?; 
    let _file2 = open(&filename2)?;
    println!("Opened {} and {}", filename1, filename2); 
    Ok(())
}
6. 处理文件

在验证参数和打开文件后,我们需要迭代每个文件的行并进行比较。

pub fn run(config: Config) -> MyResult<()> {
    let filename1 = &config.file1;
    let filename2 = &config.file2;
    if filename1.as_str() == "-" && filename2.as_str() == "-" {
        return Err(From::from("Both input files cannot be STDIN (\"-\")"));
    }
    let case = |line: String| { 
        if config.insensitive {
            line.to_lowercase()
        } else {
            line
        }
    };
    let mut lines1 =
        open(filename1)?.lines().filter_map(Result::ok).map(case); 
    let mut lines2 =
        open(filename2)?.lines().filter_map(Result::ok).map(case);
    let mut line1 = lines1.next(); 
    let mut line2 = lines2.next();
    loop {
        match (&line1, &line2) { 
            (Some(val1), Some(val2)) => match val1.cmp(val2) { 
                Equal => { 
                    println!("{}", val1);
                    line1 = lines1.next();
                    line2 = lines2.next();
                }
                Less => { 
                    println!("{}", val1);
                    line1 = lines1.next();
                }
                _ => { 
                    println!("{}", val2);
                    line2 = lines2.next();
                }
            },
            (Some(val1), None) => {
                println!("{}", val1); 
                line1 = lines1.next();
            }
            (None, Some(val2)) => {
                println!("{}", val2); 
                line2 = lines2.next();
            }
            (None, None) => break,
        };
    }
    Ok(())
}
7. 输出格式化

为了更好地控制输出,我们可以定义一个 Column 枚举和一个 printer 闭包:

enum Column {
    Col1(String),
    Col2(String),
    Col3(String),
}

pub fn run(config: Config) -> MyResult<()> {
    let filename1 = &config.file1;
    let filename2 = &config.file2;
    if filename1.as_str() == "-" && filename2.as_str() == "-" {
        return Err(From::from("Both input files cannot be STDIN (\"-\")"));
    }
    let case = |line: String| { 
        if config.insensitive {
            line.to_lowercase()
        } else {
            line
        }
    };
    let mut lines1 =
        open(filename1)?.lines().filter_map(Result::ok).map(case); 
    let mut lines2 =
        open(filename2)?.lines().filter_map(Result::ok).map(case);
    let default_col1 = if config.suppress_col1 { 
        ""
    } else {
        &config.delimiter
    };
    let default_col2 = if config.suppress_col2 { 
        ""
    } else {
        &config.delimiter
    };
    let printer = |col: Column| {
        let out = match col {
            Col1(val) => { 
                if config.suppress_col1 {
                    "".to_string()
                } else {
                    val
                }
            }
            Col2(val) => format!( 
                "{}{}",
                default_col1,
                if config.suppress_col2 { "" } else { &val },
            ),
            Col3(val) => format!( 
                "{}{}{}",
                default_col1,
                default_col2,
                if config.suppress_col3 { "" } else { &val },
            ),
        };
        if !out.trim().is_empty() { 
            println!("{}", out);
        }
    };
    let mut line1 = lines1.next(); 
    let mut line2 = lines2.next();
    loop {
        match (&line1, &line2) {
            (Some(val1), Some(val2)) => match val1.cmp(val2) {
                Equal => {
                    printer(Col3(val1.to_string())); 
                    line1 = lines1.next();
                    line2 = lines2.next();
                }
                Less => {
                    printer(Col1(val1.to_string())); 
                    line1 = lines1.next();
                }
                _ => {
                    printer(Col2(val2.to_string())); 
                    line2 = lines2.next();
                }
            },
            (Some(val1), None) => {
                printer(Col1(val1.to_string())); 
                line1 = lines1.next();
            }
            (None, Some(val2)) => {
                printer(Col2(val2.to_string())); 
                line2 = lines2.next();
            }
            (None, None) => break,
        };
    }
    Ok(())
}
8. 总结

通过以上步骤,我们实现了一个 Rust 版本的 comm 工具 commr 。它可以比较两个文件的内容,找出共同行和独特行,并根据用户的配置输出结果。以下是整个处理流程的 mermaid 流程图:

graph TD;
    A[开始] --> B[解析参数];
    B --> C[验证和打开文件];
    C --> D[创建行迭代器];
    D --> E[读取第一行];
    E --> F{是否有行};
    F -- 是 --> G{比较行};
    G -- 相等 --> H[打印共同行];
    G -- 第一行小 --> I[打印第一行];
    G -- 第二行小 --> J[打印第二行];
    H --> K[读取下一行];
    I --> K;
    J --> K;
    K --> F;
    F -- 否 --> L[结束];
9. 进一步探索
  • GNU 输出支持 :可以修改程序以支持 GNU comm 的输出和选项。
  • 性能优化 :可以考虑对程序进行性能优化,例如并行处理文件。
  • 功能扩展 :可以添加更多功能,如输出统计信息等。

Rust 实现文件比较工具:commr 详解

10. 常见问题及解决方案

在使用 commr 工具的过程中,可能会遇到一些常见问题,下面为大家列举并提供相应的解决方案。
| 问题描述 | 解决方案 |
| — | — |
| 输入文件未排序导致结果异常 | 确保输入的文件按字典序排序,可以使用 sort 命令对文件进行排序,例如 sort file1.txt > sorted_file1.txt |
| 同时将两个文件指定为 STDIN 报错 | 避免同时将两个输入文件指定为 - ,只能有一个输入为 STDIN |
| 输出格式不符合预期 | 检查是否正确设置了 -1 -2 -3 等抑制列输出的选项,以及输出分隔符 -d 的设置 |

11. 代码优化建议

为了让 commr 工具更加高效和健壮,我们可以对代码进行一些优化。
- 错误处理优化 :在 open 函数中,当文件打开失败时,错误信息已经包含了文件名,这有助于快速定位问题。但可以进一步考虑添加更多的错误类型处理,例如权限不足等情况。

fn open(filename: &str) -> MyResult<Box<dyn BufRead>> {
    match filename {
        "-" => Ok(Box::new(BufReader::new(io::stdin()))),
        _ => {
            let file = File::open(filename);
            match file {
                Ok(f) => Ok(Box::new(BufReader::new(f))),
                Err(e) => {
                    if e.kind() == std::io::ErrorKind::PermissionDenied {
                        return Err(From::from(format!("{}: Permission denied", filename)));
                    }
                    Err(From::from(format!("{}: {}", filename, e)))
                }
            }
        }
    }
}
  • 内存管理优化 :在处理大文件时,逐行读取和处理可以有效减少内存占用。当前代码已经采用了逐行读取的方式,但可以考虑在处理过程中及时释放不再使用的内存。
  • 代码结构优化 :可以将一些功能模块进一步拆分,例如将文件比较逻辑、输出格式化逻辑等封装成独立的函数,提高代码的可读性和可维护性。
12. 实际应用案例

commr 工具在实际应用中有很多场景,下面为大家介绍几个具体的案例。
- 旅游城市分析 :假设有两个文件,一个记录了某乐队上一次巡演的城市列表,另一个记录了当前巡演的城市列表。可以使用 commr 工具找出两次巡演都去过的城市、只在第一次巡演去过的城市以及只在第二次巡演去过的城市。

# 找出两次巡演都去过的城市
comm -12 <(sort cities1.txt) <(sort cities2.txt)
# 找出只在第一次巡演去过的城市
comm -23 <(sort cities1.txt) <(sort cities2.txt)
# 找出只在第二次巡演去过的城市
comm -13 <(sort cities1.txt) <(sort cities2.txt)
  • 生物信息学应用 :在生物信息学中,给定一个蛋白质序列文件,经过分析后将相似的序列聚类。可以使用 commr 工具比较聚类后的蛋白质和原始列表,找出未聚类的蛋白质,进一步分析这些蛋白质的独特性。
13. 与其他工具的比较

commr 工具与其他类似的文件比较工具有一些异同点,下面为大家进行对比。
| 工具名称 | 特点 | 适用场景 |
| — | — | — |
| commr | 用 Rust 实现,可自定义输出分隔符,支持大小写不敏感比较 | 需要灵活控制输出格式和比较方式的场景 |
| comm (BSD 版) | 标准的文件比较工具,输出格式固定 | 对输出格式要求不高,只需要基本的文件比较功能 |
| comm (GNU 版) | 功能丰富,有更多的选项,如检查排序、输出摘要等 | 需要高级功能和更多选项的场景 |

14. 总结回顾

通过前面的介绍,我们详细了解了 commr 工具的实现过程,包括参数定义、文件验证和打开、文件处理、输出格式化等方面。下面是整个实现过程的步骤列表:
1. 创建 commr 项目,添加依赖。
2. 定义 Config 结构体和 get_args 函数来解析参数。
3. 实现 open 函数来验证和打开输入文件。
4. 处理文件,逐行比较并输出结果。
5. 定义 Column 枚举和 printer 闭包来控制输出格式。

15. 未来展望

随着技术的不断发展, commr 工具也有很大的发展空间。未来可以从以下几个方面进行改进和扩展:
- 跨平台兼容性 :进一步优化代码,确保在不同的操作系统上都能稳定运行。
- 用户界面优化 :可以考虑添加图形用户界面(GUI),让用户更方便地使用该工具。
- 集成更多功能 :例如与其他数据处理工具集成,实现更复杂的数据分析任务。

以下是一个更详细的处理流程 mermaid 流程图,包含了错误处理和优化后的步骤:

graph TD;
    A[开始] --> B[解析参数];
    B --> C{参数是否合法};
    C -- 是 --> D[验证和打开文件];
    C -- 否 --> E[输出错误信息并退出];
    D --> F{文件打开是否成功};
    F -- 是 --> G[创建行迭代器];
    F -- 否 --> E;
    G --> H[读取第一行];
    H --> I{是否有行};
    I -- 是 --> J{比较行};
    J -- 相等 --> K[打印共同行];
    J -- 第一行小 --> L[打印第一行];
    J -- 第二行小 --> M[打印第二行];
    K --> N[读取下一行];
    L --> N;
    M --> N;
    N --> I;
    I -- 否 --> O[结束];

通过以上的介绍,相信大家对 commr 工具有了更深入的了解,希望大家在实际应用中能够充分发挥其作用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值