Command Line Rust

Command Line Rust
书中配套代码:kyclark/command-line-rust
前言
本书作者在前言中分享了他学习编程的经验,强调了实践的重要性。作者认为,仅仅阅读参考书籍是不足以掌握一门编程语言的,需要通过实际编写程序来应用所学知识。作者还提到,学习如何学习一门语言是程序员最重要的技能。Rust 语言在语法上与 C 语言类似,例如 for 循环、分号结尾的语句和用花括号表示的代码块。Rust 通过 borrow checker 来保证内存安全,同时不会牺牲性能。Rust 程序可以编译成原生二进制代码,其运行速度通常可以与 C 或 C++ 编写的程序相媲美。Rust 从函数式语言如 Haskell 中借鉴了一些概念,例如,变量默认是不可变的,函数是一等公民,可以使用枚举和 sum 类型表示函数可以返回 Ok
或 Err
。书中还提供了代码示例下载链接和技术问题反馈邮箱。作者感谢了 Rust 社区的贡献,以及家人和朋友的支持。
第一章:真相或后果
本章介绍了 Rust 编程的基础知识,并以经典的 Hello, world!
程序为例,展示了如何创建和运行一个 Rust 项目。
首先,介绍了如何组织 Rust 项目目录,使用 Cargo 创建和运行项目。还演示了如何编写和运行集成测试,以验证程序的正确性。例如,可以通过 Command::cargo_bin("hello").unwrap()
创建一个命令,并使用 cmd.assert().success().stdout("Hello, world!\n")
来断言命令成功执行且标准输出为 “Hello, world!\n”。如果修改 src/main.rs
中的输出,测试将会失败,并显示实际输出与期望输出之间的差异。
此外,还介绍了如何使用 std::process::exit
函数显式退出并返回一个代码,以及 std::process::abort
函数,用于以非零错误代码退出。
本章还解释了 Result
类型,它是一种可以成功或失败的方式,具有 Ok
和 Err
两种变体
第二章:回声测试
本章介绍了如何使用 Rust 创建一个类似于 echo
的程序,它可以将参数打印到标准输出。本章演示了如何使用 clap
crate 来解析命令行参数,例如,如何创建一个名为 text
的位置参数,以及一个名为 omit_newline
的 flag 参数。 使用 values_of_lossy
获取参数值,并使用 is_present
判断 flag 是否存在。代码示例:
use clap::{App, Arg};
fn main() {
let matches = App::new("echo")
.arg(Arg::with_name("text").multiple(true).required(true))
.arg(Arg::with_name("omit_newline").short("n"))
.get_matches();
let text = matches.values_of_lossy("text").unwrap();
let omit_newline = matches.is_present("omit_newline");
print!("{}{}", text.join(" "), if omit_newline { "" } else { "\n" });
}
本章还介绍了如何编写测试用例,例如使用 assert_eq!
来验证程序的输出是否与预期一致。书中使用了 Result
类型来处理测试中的错误,其中 Ok
表示成功,Err
表示错误。 使用 Box
指示错误将存在于堆上的指针中,dyn
表示对 std::error::Error
trait 的方法调用是动态分发的。
第三章:走猫步
本章讲解如何使用 Rust 实现一个类似 cat
的程序,它可以将多个文件的内容连接到一个文件中。本章介绍了如何使用 std::error::Error
trait 来表示错误值,并创建自定义的 MyResult
类型。run
函数接受一个 Config
参数,该参数包含文件列表和用于行编号的选项。本章还演示了如何使用 clap
crate 定义命令行参数,例如使用 Arg::with_name
函数来定义输入文件参数和行号选项。还使用了 const
关键字来定义全局常量。
本章还介绍了如何使用 Iterator::enumerate
方法获取元素的索引和值,以及如何使用 Box
指向文件句柄以读取标准输入或常规文件。
#[derive(Debug, Parser)]
#[command(author, version, about)]
/// Rust version of `cat`
struct Args {
/// Input file(s)
#[arg(value_name = "FILE", default_value = "-")]
files: Vec<String>,
/// Number lines
#[arg(
short('n'),
long("number"),
conflicts_with("number_nonblank_lines")
)]
number_lines: bool,
/// Number non-blank lines
#[arg(short('b'), long("number-nonblank"))]
number_nonblank_lines: bool,
}
第四章:头痛
本章介绍了如何使用 Rust 实现一个类似 head
的程序,它可以显示文件的前几行或前几个字节。本章首先介绍了如何编写单元测试来检查 parse_positive_int
函数,该函数用于将字符串解析为正整数。使用了 unimplemented!
macro 来标记尚未实现的功能,以及 panic!
macro 来终止程序。书中还使用了 match
表达式来进行模式匹配。本章还演示了如何使用 App::new
和 Arg::with_name
定义命令行参数,包括 lines
和 bytes
参数,以及如何使用 conflicts_with
来指定参数之间的冲突关系。代码示例:
#[derive(Parser, Debug)]
#[command(author, version, about)]
/// Rust version of `head`
struct Args {
/// Input file(s)
#[arg(default_value = "-", value_name = "FILE")]
files: Vec<String>,
/// Number of lines
#[arg(
short('n'),
long,
default_value = "10",
value_name = "LINES",
value_parser = clap::value_parser!(u64).range(1..)
)]
lines: u64,
/// Number of bytes
#[arg(
short('c'),
long,
value_name = "BYTES",
conflicts_with("lines"),
value_parser = clap::value_parser!(u64).range(1..)
)]
bytes: Option<u64>,
}
书中介绍了如何使用下划线 _
来表示不使用的变量,如何在类型注解中使用下划线来让编译器推断类型,以及如何使用turbofish ::<>
操作符来显式指定类型。本章还介绍了如何使用 BufRead::read_line
来读取文件行,以及如何使用 take
方法来限制选择的元素数量。
第五章:对妈妈说的话
本章介绍了如何使用 Rust 实现一个类似 wc
的程序,它可以统计文件或标准输入的行数、字数和字节数。本章介绍了如何使用 if let
表达式来处理 Result
类型,以及如何使用 std::process::exit
来终止程序并返回错误代码。定义了 Config
结构体来表示命令行参数。还演示了如何使用 App::new
和 Arg::with_name
定义命令行参数,包括 files
,words
,bytes
,chars
和 lines
参数,以及如何使用 multiple
和 default_value
来指定参数的属性。
本章还介绍了如何使用 Iterator::all
方法来检查所有值是否都为 false,以及如何使用闭包来作为参数传递给其他函数。代码示例:
if [lines, words, bytes, chars].iter().all(|v| !v) {
lines = true;
words = true;
bytes = true;
}
本章还介绍了如何使用 BufRead
trait 来读取文件,以及如何使用 impl BufRead
来指定函数参数的 trait bound。还介绍了如何使用 std::io::Cursor
来创建用于测试的虚拟文件句柄。
第六章:独一无二
本章介绍了如何使用 Rust 实现一个类似 uniq
的程序,它可以查找文件或标准输入中的唯一行。 本章讨论了如何写入文件或标准输出,如何使用闭包捕获变量,以及如何应用DRY(不要重复自己)原则。本章还介绍了如何使用 Write
trait 和 write!
和 writeln!
macros,以及如何使用临时文件。 使用 match
表达式来处理输入文件是标准输入还是文件。 本章还使用了 loop
表达式来进行循环读取。
本章还介绍了闭包和函数的区别,以及如何使用闭包来捕获变量。还介绍了如何使用 Box<dyn Write>
来表示可写入类型,并根据命令行参数来决定是写入文件还是标准输出。代码示例:
let mut print = |num: u64, text: &str| -> Result<()> {
if num > 0 {
if args.count {
write!(out_file, "{num:>4} {text}")?;
} else {
write!(out_file, "{text}")?;
}
};
Ok(())
};
let mut line = String::new();
let mut previous = String::new();
let mut count: u64 = 0;
loop {
let bytes = file.read_line(&mut line)?;
if bytes == 0 {
break;
}
if line.trim_end() != previous.trim_end() {
print(count, &previous)?;
previous = line.clone();
count = 0;
}
count += 1;
line.clear();
}
print(count, &previous)?;
第七章:见者有份
本章介绍了如何使用 Rust 实现一个类似 find
的程序,它可以递归搜索目录并查找符合特定条件的文件。本章介绍了如何使用 enum
类型来表示不同的条目类型,例如目录、文件和链接。 还定义了 Config
结构体来表示命令行参数,包括搜索路径,名称模式和条目类型。本章还介绍了如何在文件 glob 和正则表达式中使用点号 (.) 和星号 (*) 的不同含义。本章使用了 Regex::new
函数来创建正则表达式,并介绍了**^
和 $
** 的作用,分别用于匹配字符串的开头和结尾。
本章还介绍了如何使用 WalkDir
来递归搜索目录结构,并使用 DirEntry
值来查找文件,目录和链接。 还演示了如何使用链式调用,如 any
,filter
,map
和 filter_map
来处理迭代器。 代码示例:
for path in &args.paths {
let entries = WalkDir::new(path)
.into_iter()
.filter_map(|e| match e {
Err(e) => {
eprintln!("{e}");
None
}
Ok(entry) => Some(entry),
})
.filter(type_filter)
.filter(name_filter)
.map(|entry| entry.path().display().to_string())
.collect::<Vec<_>>();
println!("{}", entries.join("\n"));
}
第八章:削发
本章介绍了如何使用 Rust 实现一个类似 cut
的程序,它可以从文件或标准输入中提取文本。 本章介绍了如何使用单元测试来验证 parse_pos
函数,该函数用于解析文本位置,并支持逗号分隔的列表以及范围。定义了多个测试用例来涵盖不同的情况,包括错误和正确的情况,还使用了 assert!
和 assert_eq!
来验证测试结果。 本章还介绍了如何使用正则表达式来匹配数字和范围,并使用 collect
方法来将结果收集到向量中。
本章介绍了如何使用 Arg::conflicts_with_all
来指定参数之间的冲突关系,例如 fields
, bytes
和 chars
参数。还使用了 Vec::first
方法来选择向量的第一个元素。 本章还提供了如何实现 extract_chars
和 extract_bytes
函数的示例,用于提取字符串中的字符和字节。还介绍了如何使用 csv
crate 来解析和创建分隔文本。
for filename in &args.files {
match open(filename) {
Err(err) => eprintln!("{filename}: {err}"),
Ok(file) => match &extract {
Extract::Fields(field_pos) => {
let mut reader = ReaderBuilder::new()
.delimiter(delimiter)
.has_headers(false)
.from_reader(file);
let mut wtr = WriterBuilder::new()
.delimiter(delimiter)
.from_writer(io::stdout());
for record in reader.records() {
wtr.write_record(extract_fields(
&record?, field_pos,
))?;
}
}
Extract::Bytes(byte_pos) => {
for line in file.lines() {
println!("{}", extract_bytes(&line?, byte_pos));
}
}
Extract::Chars(char_pos) => {
for line in file.lines() {
println!("{}", extract_chars(&line?, char_pos));
}
}
},
}
}
第九章:抓手
本章介绍了如何使用 Rust 实现一个类似 grep
的程序,它可以查找匹配给定正则表达式的输入行。本章介绍了如何使用 RegexBuilder
来创建正则表达式,并使用 case_insensitive
方法来进行大小写不敏感的匹配。还演示了如何使用 Result::map_err
来创建错误消息,并介绍了 grep
的正则表达式语法, 包括基本正则表达式和扩展正则表达式。还介绍了如何使用 WalkDir
来递归查找文件,与第七章类似。
本章还介绍了如何使用**BitXor
** 按位异或运算符来替换更复杂的逻辑 AND 和 OR 组合。代码示例:
if (pattern.is_match(&line) && !invert_match) || (!pattern.is_match(&line) && invert_match) {
matches.push(line.clone());
}
// 等价于
if pattern.is_match(&line) ^ !invert_match {
matches.push(line.clone());
}
第十章:天下为公
本章介绍了如何使用 Rust 实现一个类似 comm
的程序,它可以读取两个文件并报告两个文件中共有和独有的行。 本章介绍了 Config
结构体,其中包含两个输入文件名,以及控制输出列和比较敏感度的布尔值。还演示了如何使用 App::new
和 Arg::with_name
定义命令行参数,并使用 required
来指定参数是必需的。 代码示例:
pub fn get_args() -> MyResult<Config> {
let matches = App::new("commr")
.arg(Arg::with_name("file1").required(true))
.arg(Arg::with_name("file2").required(true))
.arg(Arg::with_name("suppress_col1").short("1"))
.arg(Arg::with_name("suppress_col2").short("2"))
.arg(Arg::with_name("suppress_col3").short("3"))
.arg(Arg::with_name("insensitive").short("i"))
.arg(Arg::with_name("delimiter").short("d").default_value("\t"))
.get_matches();
// ...
}
本章还使用了 if !config.suppress_col1
,即双重否定在理解上更困难,建议使用更积极的命名方法,例如 do_something
。
第十一章:尾随其后
本章介绍了如何使用 Rust 实现一个类似 tail
的程序,它可以显示文件的最后几行或最后几个字节。本章介绍了如何使用 enum
类型来表示不同的取值,例如 PlusZero
和 TakeNum
,以及如何使用 i64
类型来存储正数和负数。 本章使用了正则表达式来匹配数字和正负号,并使用了 map_or
方法来获取捕获组的值,以及 format!
方法来格式化字符串。 代码示例:
fn run(args: Args) -> Result<()> {
let lines = parse_num(args.lines)
.map_err(|e| anyhow!("illegal line count -- {e}"))?;
let bytes = args
.bytes
.map(parse_num)
.transpose()
.map_err(|e| anyhow!("illegal byte count -- {e}"))?;
let num_files = args.files.len();
for (file_num, filename) in args.files.iter().enumerate() {
match File::open(filename) {
Err(err) => eprintln!("{filename}: {err}"),
Ok(file) => {
if !args.quiet && num_files > 1 {
println!(
"{}==> {filename} <==",
if file_num > 0 { "\n" } else { "" },
);
}
let (total_lines, total_bytes) = count_lines_bytes(filename)?;
let file = BufReader::new(file);
if let Some(num_bytes) = &bytes {
print_bytes(file, num_bytes, total_bytes)?;
} else {
print_lines(file, &lines, total_lines)?;
}
}
}
}
Ok(())
}
本章还介绍了如何使用 i64::wrapping_neg
来计算负数,并介绍了泛型函数和 trait bounds 的用法,例如 Read + Seek
。 还使用了 OnceCell
来创建一个静态正则表达式。
第十二章:天选之子
本章介绍了如何使用 Rust 实现一个类似 fortune
的程序,它可以从文本数据库中随机选择引语或琐事。本章介绍了如何使用 strfile
程序来创建索引文件,以及如何使用 head
命令来查看文件的结构。 本章介绍了如何使用 RegexBuilder
创建正则表达式,并定义了 Config
结构体,包括资源列表,模式和随机种子。 还介绍了如何使用 map
和 transpose
方法来处理 Option
和 Result
类型,以及如何使用 unwrap
方法来获取 Option
或 Result
的值。
本章还介绍了如何使用 SliceRandom
trait 来随机选择元素,并使用了 SeedableRng
trait 来创建可重现的随机数生成器。代码示例:
use rand::prelude::SliceRandom;
use rand::{rngs::StdRng, SeedableRng};
fn pick_fortune(fortunes: &[Fortune], seed: Option<u64>) -> Option<String> {
let mut rng = match seed {
Some(seed) => StdRng::seed_from_u64(seed),
_ => StdRng::from_entropy(),
};
fortunes.choose(&mut rng).map(|f| f.text.clone())
}
第十三章:岁月如歌
本章介绍了如何使用 Rust 实现一个类似 cal
的程序,它可以显示日历。 本章使用了 chrono
crate 来处理日期和时间,包括 NaiveDate
类型。 本章还介绍了如何使用 App::conflicts_with_all
来指定参数之间的冲突关系,例如 month
和 year
参数,以及 show_current_year
参数。 本章还使用了闭包来判断某天是否是今天,以及如何使用 Style::reverse
来高亮显示今天的日期。还使用了 format!
宏来格式化输出。
fn run(args: Args) -> Result<()> {
let today = Local::now().date_naive();
let mut month = args.month.map(parse_month).transpose()?;
let mut year = args.year;
if args.show_current_year {
month = None;
year = Some(today.year());
} else if month.is_none() && year.is_none() {
month = Some(today.month());
year = Some(today.year());
}
let year = year.unwrap_or(today.year());
match month {
Some(month) => {
let lines = format_month(year, month, true, today);
println!("{}", lines.join("\n"));
}
None => {
println!("{year:>32}");
let months: Vec<_> = (1..=12)
.map(|month| format_month(year, month, false, today))
.collect();
for (i, chunk) in months.chunks(3).enumerate() {
if let [m1, m2, m3] = chunk {
for lines in izip!(m1, m2, m3) {
println!("{}{}{}", lines.0, lines.1, lines.2);
}
if i < 3 {
println!();
}
}
}
}
}
Ok(())
}
第十四章:条分缕析
本章介绍了如何使用 Rust 实现一个类似 ls
的程序,它可以显示文件和目录的信息。本章介绍了如何使用 enum
类型来表示用户、组和其他所有者,以及如何使用位掩码来检查文件权限。还介绍了如何使用 metadata::mode
来获取文件的权限,并介绍了八进制表示法。 本章还介绍了如何使用 Owner
枚举来表示所有者,并使用 impl
块定义了 masks
方法来获取权限掩码。 代码示例:
fn run(args: Args) -> Result<()> {
let paths = find_files(&args.paths, args.show_hidden)?;
if args.long {
println!("{}", format_output(&paths)?);
} else {
for path in paths {
println!("{}", path.display());
}
}
Ok(())
}
本章介绍了如何使用三斜杠 ///
来创建文档注释,以及如何使用 cargo doc
来生成代码文档。还介绍了如何使用 tabular::Table
crate 来创建表格输出。
尾声
作者在尾声中鼓励读者继续学习和实践,并尝试使用不同的编程语言来编写和重写这些程序。作者还希望读者通过编写优秀的软件来让世界变得更美好。