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
工具有了更深入的了解,希望大家在实际应用中能够充分发挥其作用。
超级会员免费看
2025

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



