Talent Plan 之 Rust 网络编程(一):预备知识

引言

今天开始「Rust 网络编程实践」的第一部分中的预备知识学习,并最终能编写出一个简单的命令行工具。以下材料是今天需要阅读的:

接下来我们将依次完成上述材料学习,着重记录一些知识点,并在最后编写一个简单的命令行程序。

Cargo Manifest 文件格式

在使用 cargo 命令创建的项目中,都会存在一个 cargo.toml 文件,这也就是所谓的 manifest。一个简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
[package]
name = "app"
version = "0.1.0"
authors = ["0xE8551CCB <user@host>"]
edition = "2018"

[dependencies]
clap = {version = "2.33.0", features = ["yaml"]}
maplit = "1.0.2"
lazy_static = "1.4.0"
yaml-rust = "0.3.5"

cargo.toml 是由多个部分配置组成的,主要包括如下几个部分(详细定义请参考文档):

  • cargo-features: 实验性功能
  • [package]: 包定义(如名称、版本、作者、Rust 编译器版本等)
  • [[lib]]: 库的设置
  • [[bin]]: 二进制文件设置
  • [[example]]: 示例文件设置
  • [[test]]: 测试设置
  • [[bench]]: 基准测试设置
  • [dependencies]: 依赖库列表
  • [features]: 条件编译功能
  • [workspace]: 工作空间定义

Cargo 环境变量

环境变量可以用来和 rustc 以及 Cargo 进行通信,可以在代码中通过 env! 宏或者在构建脚本中设置环境变量,这样可以控制构建行为。相关环境变量比较多,笔者在这里不多赘述了,等用到时再来查阅。建议把 Cargo environment variables 简单阅读下,留个印象。

Rust 程序文档编写

Rust API Guidelines 列出了一些设计和编写 Rust API 的建议,主要是由 Rust Lib 小组在构建 Rust 生态,编写标准库和一些其它 crates 期间总结而来。当然,这些建议并非一定需要遵守,不过既然来自于官方,那还是非常值得像笔者这样的 Rust 小白学习的。

今天主要学习其中关于文档编写的一些建议,其它内容建议抽空阅读下。关于文档编写的一些大建议梳理如下(详细示例还请参见文档 Rust API Guidelines: Documentation):

  1. crate 级别的文档应该是全面详细的,且包含使用示例;
  2. 所有公开的模块、trait、struct、enum、函数、方法、宏以及类型定义都需要提供使用示例;
  3. 文档中的实例应该使用 ? 处理错误,而非 try! 或者 unwrap
  4. 函数的文档中应该包括:函数功能描述、返回的错误(# Errors)、Panic 情况(# Panics)以及一些安全使用的提示(# Safty,比如 std::ptr::read 这种 unsafe 函数);
  5. 文档中提到的类型应该关联到对应的文档(Link all the things);
  6. Cargo.toml 中应该包含所有常用的元信息:
    • authors
    • description
    • license
    • repository
    • readme
    • keywords
    • categories
  7. crate 要设置正确的 #![doc(html_root_url = "https://docs.rs/CRATE/MAJOR.MINOR.PATCH")],尤其要注意这里的版本号要和 Cargo.toml 中的 version 保持一致;
  8. 在发布日志(Release notes)中记录所有重大变更,尤其是不兼容变更需要清晰指出;
  9. 可以通过 #[doc(hidden)] 将一些没什么帮助的实现细节(如私有类型关联的方法实现等)从生成的文档中隐藏起来。

如何编写命令行(CLI)程序(Write a Good CLI Program)

Write a Good CLI Program 一文中讲解在使用 Rust 编写命令行程序的最佳实践,核心知识点包括:

  1. 使用 clap crates 编写可以接收复杂参数的命令行程序;
  2. 使用 .env 编写应用配置,使用 dotenv 读取环境变量;
  3. 错误处理:
    1. panic 会导致程序直接退出,退出没有错误码,适合在脚本中使用;
    2. 函数返回 Result,可以根据是否为 Error 来决定后续操作;
    3. 可以为自定义 Error 实现 fmt::Display trait,方便格式化输出错误消息。
  4. 使用 println!() 会打印到标准输出中,而使用 eprintln!() 则是标准错误;
  5. 使用 std::process::exit(1) 设置程序退出码(Exit Code)。

所谓的命令行程序(Command Line Interface, CLI)就是指在终端中运行的程序,没有图形界面。比如 ls, ps 等。在 awesome-cli-apps 中收集了很多比较优质的命令行程序。

题外话:作者安利了一个叫作 exa 命令行程序,使用 Rust 语言实现,可以替代 ls 命令。笔者试用了一下,体验不错。

exa

命令行程序长什么样

一般而言,命令行程序使用形式如下:

1
$./program [args] [flags] [options]

可以通过 -h--help 查看更多帮助信息。

exa-help

动手实践

大神 Linus Torvalds 曾言「Talk is cheap, show me the code」。那么,接下来就通过一个简单的示例把 Write a Good CLI Program 中提到的一些技巧实践一遍。

笔者曾经使用 Rust 实现过一个小工具 gic,它可以生成风格一致的 git commit message,还算实用。接下来实现一个简单版本 gitc 仅供参考,全部代码参见 talent-plan-projects/gitc

首先使用 cargo 创建一个命令行应用:

1
2
$ cargo new gitc
$ cd gitc

应用目录结构如下:

1
2
3
4
5
6
7
8
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── cli.yml
│ └── main.rs
└── target
└── debug

定义命令行配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<--cli.yml-->
name: gitc
version: "0.1.0"
author: 0xE8551CCB <noti@ifaceless.space>
about: Generate elegant and uniform commit message
args:
- message:
required: true
index: 1
takes_value: true
help: Use the given <message> as the commit message
- use-emoji-code-only:
long: use-emoji-code-only
help: Use emoji code instead of emoji

注册 emoji 列表

为了方便扩展,将 emoji 相关信息放置在全局的列表中,扩展只需要新增列表项即可。这里使用 layzy_static 宏,他可以定义需要在运行时初始化的静态变量,用法类似在其它语言中定义全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use lazy_static::lazy_static;

lazy_static! {
// 这里注册 git emoji 列表
static ref GITMOJI_LIST: Vec<HashMap<&'static str, &'static str>> = vec![
hashmap! {
"name" => "feat",
"emoji" => "✨",
"code" => ":feat:",
"description" => "✨ Introduce new features"
},
hashmap! {
"name" => "fix",
"emoji" => "🐛",
"code" => ":fix:",
"description" => "🐛 Fix bugs"
}
];
}

构建命令行 App

clap 是一个功能丰富、工作高效的 Rust 命令行解析器,借助它可以非常轻松地创建命令行应用。gitc 在创建时,一部分命令行配置来自静态的 YAML 文件,另一部分则是基于上述可扩展的 emoji 列表配置。

1
2
3
4
5
6
7
8
9
10
11
12
fn make_cli_app<'a, 'b>(yml: &'a Yaml) -> App<'a, 'b> {
let mut app = App::from_yaml(yml);
// 将 gitmoji 相关命令注册进来
for item in GITMOJI_LIST.iter() {
app = app.arg(
Arg::with_name(item.get("name").unwrap())
.help(item.get("description").unwrap())
.long(item.get("name").unwrap()),
);
}
app
}

理解命令行参数并生成执行配置

为了便于理解,下面抽象了一个 CommitConfig 结构体,用于将 git commit 有关的特殊配置通过命令行参数解析到这里。具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#[derive(Debug)]
struct CommitConfig {
message: String,
use_emoji_code_only: bool,
gitmoji: Option<Gitmoji>,
}

impl CommitConfig {
fn from_cli_arg_matches(m: &ArgMatches) -> Self {
let message = m.value_of("message").unwrap();
let use_emoji_code_only = m.is_present("use-emoji-code-only");
CommitConfig {
message: message.to_string(),
use_emoji_code_only: use_emoji_code_only,
gitmoji: Self::match_gitmoji(m),
}
}

fn match_gitmoji(m: &ArgMatches) -> Option<Gitmoji> {
for x in GITMOJI_LIST.iter() {
if m.is_present(x.get("name").unwrap()) {
return Some(Gitmoji::from_gitmoji_config(x));
}
}
None
}
}

执行 git commit 命令

由于需要通过 Shell 执行 git commit 命令,这里需要使用 std::process::Command 协助完成。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn do_git_commit(c: &CommitConfig) -> Result<(), std::io::Error> {
let mut msg = c.message.to_string();
// 构建 commit 消息...(省略消息构建代码)
let mut cmd = Command::new("git");
cmd.arg("commit").arg("-m").arg(msg);

// 执行命令,拿到输出结果
let output = cmd.output()?;
println!("{}", String::from_utf8_lossy(&output.stdout));
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
Ok(())
}

编写 main 函数

好啦,现在可以将上述过程串联起来,完成我们的 gitc 命令行工具:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let yml = load_yaml!("cli.yml");
let matches = make_cli_app(yml).get_matches();
let commit_config = CommitConfig::from_cli_arg_matches(&matches);
match do_git_commit(&commit_config) {
Ok(_) => (),
Err(e) => {
eprintln!("Error: {}", e);
process::exit(1);
}
}
}

完成后,使用 cargo build 可以生成可执行文件,运行效果如下:
gitc-help
gic-run

小结

本节预备知识主要学习了与 Cargo 有关的环境变量作用、Cargo.toml 中的元信息构成以及编写规范的 API 文档的方法,最后通过编写 talent-plan-projects/gitc 掌握了如何编写一个简单的命令行程序以及一些最佳实践。

参考

0%