编程挑战:文件处理与查找工具实现
一、文件处理程序的实现与优化
在编程过程中,我们常常会遇到处理文件内容的需求,比如统计重复行的数量。下面我们来详细探讨如何实现这样一个程序,并对其进行优化。
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
工具,使其更加实用和强大。
超级会员免费看

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



