14、深入探究 `cutr` 工具:Rust 实现的 `cut` 命令

深入探究 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 改进点
  1. 范围必须同时指定起始和结束值(包含)。
  2. 输出列应按用户指定的顺序排列。
  3. 范围可以包含重复值。
  4. 解析分隔文本文件时应尊重转义分隔符。
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 工具,例如支持更多的文件格式、提高性能等。同时,也可以根据用户的需求,添加更多的功能和选项。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值