深入探究
cutr
工具:Rust 实现的
cut
命令
1.
cut
命令基础
cut
命令是一个常用的文本处理工具,用于从文件的每一行中提取指定部分并输出到标准输出。它有 BSD 和 GNU 两个主要版本,下面分别介绍其基本用法。
1.1 BSD 版本的
cut
-
名称
:
cut用于从文件的每一行中截取选定部分。 - 语法 :
-
cut -b list [-n] [file ...] -
cut -c list [file ...] -
cut -f list [-d delim] [-s] [file ...] - 选项说明 :
-
-b list:指定字节位置。 -
-c list:指定字符位置。 -
-d delim:使用delim作为字段分隔符,默认为制表符。 -
-f list:指定字段,输入中字段由分隔符分隔,输出字段由单个分隔符分隔。 -
-n:不拆分多字节字符。 -
-s:抑制没有字段分隔符的行。
1.2 GNU 版本的
cut
-
名称
:
cut用于从文件的每一行中移除部分内容。 -
语法
:
cut OPTION... [FILE]... - 选项说明 :
-
-b, --bytes=LIST:仅选择这些字节。 -
-c, --characters=LIST:仅选择这些字符。 -
-d, --delimiter=DELIM:使用DELIM代替制表符作为字段分隔符。 -
-f, --fields=LIST:仅选择这些字段;除非指定-s选项,否则也打印不包含分隔符的行。 -
-n:与-b一起使用时,不拆分多字节字符。 -
--complement:补全选定的字节、字符或字段集合。 -
-s, --only-delimited:不打印不包含分隔符的行。 -
--output-delimiter=STRING:使用STRING作为输出分隔符,默认为输入分隔符。 -
--help:显示帮助信息并退出。 -
--version:输出版本信息并退出。
2.
cut
命令的使用示例
以下是一些使用
cut
命令的示例,使用的文件位于
08_cutr/tests/inputs
目录下。
2.1 处理固定宽度文本文件
$ cd 08_cutr/tests/inputs
$ cat books.txt
Author Year Title
Émile Zola 1865 La Confession de Claude
Samuel Beckett 1952 Waiting for Godot
Jules Verne 1870 20,000 Leagues Under the Sea
# 提取前 20 个字符(作者列)
$ cut -c 1-20 books.txt
Author
Émile Zola
Samuel Beckett
Jules Verne
# 提取第 21 到 25 个字符(年份列)
$ cut -c 21-25 books.txt
Year
1865
1952
1870
# 提取第 26 到 70 个字符(标题列)
$ cut -c 26-70 books.txt
Title
La Confession de Claude
Waiting for Godot
20,000 Leagues Under the Sea
2.2 处理分隔符分隔的文件
# 处理制表符分隔的文件
$ cat books.tsv
Author Year Title
Émile Zola 1865 La Confession de Claude
Samuel Beckett 1952 Waiting for Godot
Jules Verne 1870 20,000 Leagues Under the Sea
# 选择第二列和第三列
$ cut -f 2,3 books.tsv
Year Title
1865 La Confession de Claude
1952 Waiting for Godot
1870 20,000 Leagues Under the Sea
# 处理逗号分隔的文件
$ cat books.csv
Author,Year,Title
Émile Zola,1865,La Confession de Claude
Samuel Beckett,1952,Waiting for Godot
Jules Verne,1870,"20,000 Leagues Under the Sea"
# 选择第二列和第一列
$ cut -d , -f 2,1 books.csv
Author,Year
Émile Zola,1865
Samuel Beckett,1952
Jules Verne,1870
3.
cutr
挑战程序
cutr
是一个用 Rust 实现的
cut
命令,它在原
cut
命令的基础上有一些改进。
3.1 改进点
- 范围必须同时指定起始和结束值(包含)。
- 输出列应按用户指定的顺序排列。
- 范围可以包含重复值。
- 解析分隔文本文件时应尊重转义分隔符。
3.2 开始项目
首先,创建一个新的 Rust 项目:
cargo new cutr
cd cutr
cp -r 08_cutr/tests .
然后,在
Cargo.toml
中添加依赖:
[dependencies]
clap = "2.33"
csv = "1"
regex = "1"
[dev-dependencies]
assert_cmd = "1"
predicates = "1"
rand = "0.8"
运行测试:
cargo test
4. 定义参数
以下是
src/main.rs
的推荐结构:
fn main() {
if let Err(e) = cutr::get_args().and_then(cutr::run) {
eprintln!("{}", e);
std::process::exit(1);
}
}
src/lib.rs
的部分代码如下:
use crate::Extract::*;
use clap::{App, Arg};
use std::error::Error;
type MyResult<T> = Result<T, Box<dyn Error>>;
type PositionList = Vec<usize>;
#[derive(Debug)]
pub enum Extract {
Fields(PositionList),
Bytes(PositionList),
Chars(PositionList),
}
#[derive(Debug)]
pub struct Config {
files: Vec<String>,
delimiter: u8,
extract: Extract,
}
get_args
函数的开始部分:
pub fn get_args() -> MyResult<Config> {
let matches = App::new("cutr")
.version("0.1.0")
.author("Ken Youens-Clark <kyclark@gmail.com>")
.about("Rust cut")
// 这里添加参数
.get_matches();
Ok(Config {
files: ...
delimiter: ...
fields: ...
bytes: ...
chars: ...
})
}
run
函数:
pub fn run(config: Config) -> MyResult<()> {
println!("{:#?}", &config);
Ok(())
}
5. 解析位置列表
parse_pos
函数用于解析位置列表,以下是其实现:
fn parse_pos(range: &str) -> MyResult<PositionList> {
let mut fields: Vec<usize> = vec![];
let range_re = Regex::new(r"(\d+)?-(\d+)?").unwrap();
for val in range.split(',') {
if let Some(cap) = range_re.captures(val) {
let n1: &usize = &cap[1].parse()?;
let n2: &usize = &cap[2].parse()?;
if n1 < n2 {
for n in *n1..=*n2 {
fields.push(n);
}
} else {
return Err(From::from(format!(
"First number in range ({}) \
must be lower than second number ({})",
n1, n2
)));
}
} else {
match val.parse() {
Ok(n) if n > 0 => fields.push(n),
_ => {
return Err(From::from(format!(
"illegal list value: \"{}\"",
val
)))
}
}
}
}
// 减去 1 以调整字段索引
Ok(fields.into_iter().map(|i| i - 1).collect())
}
6. 总结
通过以上步骤,我们了解了
cut
命令的基本用法,以及如何使用 Rust 实现一个改进版的
cut
命令
cutr
。在实现过程中,我们需要注意参数的解析、位置列表的处理以及错误处理等方面。
以下是一个简单的流程图,展示了
parse_pos
函数的处理流程:
graph TD;
A[开始] --> B[分割输入字符串];
B --> C{是否匹配正则表达式};
C -- 是 --> D[解析两个数字];
D -- 第一个数字小于第二个数字 --> E[添加范围内的数字到列表];
D -- 第一个数字不小于第二个数字 --> F[返回错误];
C -- 否 --> G{是否能解析为正整数};
G -- 是 --> H[添加数字到列表];
G -- 否 --> I[返回错误];
E --> J[调整列表中的数字];
H --> J;
J --> K[返回结果];
通过这个流程图,我们可以更清晰地理解
parse_pos
函数的工作原理。在实际开发中,我们可以根据这个流程来编写代码,确保代码的正确性和健壮性。
深入探究
cutr
工具:Rust 实现的
cut
命令
7. 完整
get_args
函数实现
在前面我们给出了
get_args
函数的开始部分,现在来完成它。以下是完整的
get_args
函数代码:
pub fn get_args() -> MyResult<Config> {
let matches = App::new("cutr")
.version("0.1.0")
.author("Ken Youens-Clark <kyclark@gmail.com>")
.about("Rust cut")
.arg(
Arg::with_name("files")
.value_name("FILE")
.help("Input file(s)")
.required(true)
.default_value("-")
.min_values(1),
)
.arg(
Arg::with_name("delimiter")
.value_name("DELIMITER")
.help("Field delimiter")
.short("d")
.long("delim")
.default_value("\t"),
)
.arg(
Arg::with_name("fields")
.value_name("FIELDS")
.help("Selected fields")
.short("f")
.long("fields")
.conflicts_with_all(&["chars", "bytes"]),
)
.arg(
Arg::with_name("bytes")
.value_name("BYTES")
.help("Selected bytes")
.short("b")
.long("bytes")
.conflicts_with_all(&["fields", "chars"]),
)
.arg(
Arg::with_name("chars")
.value_name("CHARS")
.help("Selected characters")
.short("c")
.long("chars")
.conflicts_with_all(&["fields", "bytes"]),
)
.get_matches();
let delimiter = matches.value_of("delimiter").unwrap_or("\t");
let delim_bytes = delimiter.as_bytes();
if delim_bytes.len() > 1 {
return Err(From::from(format!(
"--delim \"{}\" must be a single byte",
delimiter
)));
}
let fields = matches.value_of("fields").map(parse_pos).transpose()?;
let bytes = matches.value_of("bytes").map(parse_pos).transpose()?;
let chars = matches.value_of("chars").map(parse_pos).transpose()?;
let extract = match (fields, bytes, chars) {
(Some(f), None, None) => Extract::Fields(f),
(None, Some(b), None) => Extract::Bytes(b),
(None, None, Some(c)) => Extract::Chars(c),
_ => return Err(From::from("Must specify one of -f, -b, or -c")),
};
Ok(Config {
files: matches.values_of_lossy("files").unwrap(),
delimiter: delim_bytes[0],
extract,
})
}
这个函数完成了参数的解析和验证工作,具体步骤如下:
1.
定义参数
:使用
clap
库定义了文件、分隔符、字段、字节和字符等参数,并设置了它们之间的冲突关系。
2.
验证分隔符
:将分隔符转换为字节,并验证其长度是否为 1。
3.
解析位置列表
:使用
parse_pos
函数解析字段、字节和字符的位置列表。
4.
确定提取类型
:根据用户指定的参数,确定提取的类型(字段、字节或字符)。
5.
返回配置
:返回包含文件、分隔符和提取类型的配置对象。
8. 错误处理和测试
在开发
cutr
工具时,错误处理是非常重要的。以下是一些常见的错误情况及处理方式:
| 错误情况 | 处理方式 |
|---|---|
无效的范围值(如
0
、
a
等)
|
返回
illegal list value
错误
|
范围起始值大于结束值(如
3-2
)
|
返回
First number in range (...) must be lower than second number (...)
错误
|
| 分隔符长度大于 1 |
返回
--delim "..." must be a single byte
错误
|
未指定
-f
、
-b
或
-c
中的任何一个
|
返回
Must specify one of -f, -b, or -c
错误
|
在
src/lib.rs
中已经提供了
test_parse_pos
测试函数,用于测试
parse_pos
函数的正确性。以下是测试代码:
#[cfg(test)]
mod tests {
use super::parse_pos;
#[test]
fn test_parse_pos() {
assert!(parse_pos("").is_err());
let res = parse_pos("0");
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "illegal list value: \"0\"",);
let res = parse_pos("a");
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "illegal list value: \"a\"",);
let res = parse_pos("1,a");
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), "illegal list value: \"a\"",);
let res = parse_pos("2-1");
assert!(res.is_err());
assert_eq!(
res.unwrap_err().to_string(),
"First number in range (2) must be lower than second number (1)"
);
let res = parse_pos("1");
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec![0]);
let res = parse_pos("1,3");
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec![0, 2]);
let res = parse_pos("1-3");
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec![0, 1, 2]);
let res = parse_pos("1,7,3-5");
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec![0, 6, 2, 3, 4]);
}
}
运行
cargo test
命令可以执行这些测试,确保代码的正确性。
9. 实际使用示例
以下是一些使用
cutr
工具的实际示例:
9.1 处理 TSV 文件
cargo run -- -f 2-3 tests/inputs/movies1.tsv
这个命令将选择
tests/inputs/movies1.tsv
文件中的第二列和第三列。输出结果如下:
Config {
files: [
"tests/inputs/movies1.tsv",
],
delimiter: 9,
extract: Fields(
[
1,
2,
],
),
}
9.2 处理 CSV 文件
cargo run -- -f 1 -d , tests/inputs/movies1.csv
这个命令将选择
tests/inputs/movies1.csv
文件中的第一列,并使用逗号作为分隔符。输出结果如下:
Config {
files: [
"tests/inputs/movies1.csv",
],
delimiter: 44,
extract: Fields(
[
0,
],
),
}
10. 总结和展望
通过以上内容,我们详细介绍了
cutr
工具的实现过程,包括
cut
命令的基础用法、
cutr
工具的改进点、参数解析、位置列表解析、错误处理和测试等方面。
cutr
工具在原
cut
命令的基础上,提供了更灵活的范围指定和输出顺序控制,同时支持对转义分隔符的解析。
以下是一个简单的流程图,展示了
cutr
工具的整体处理流程:
graph TD;
A[开始] --> B[解析参数];
B --> C{参数是否有效};
C -- 是 --> D[解析位置列表];
D --> E[确定提取类型];
E --> F[读取文件];
F --> G[提取指定部分];
G --> H[输出结果];
C -- 否 --> I[输出错误信息];
在未来的开发中,我们可以进一步优化
cutr
工具,例如支持更多的文件格式、提高性能等。同时,也可以根据用户的需求,添加更多的功能和选项。
超级会员免费看
31

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



