使用rust编写高质量命令行程序

前言

最近打算干点人事,继续入门rust。开始看pingcap提供的rust教程,Building Blocks 1引用了一篇文章写的挺好,翻译一下以备查阅。

使用rust编写高质量命令行程序

命令行界面(CLI)程序在终端上运行,这意味着没有图形界面(GUI)。

其实我们每天都在使用CLI,比如lspstop 等。还有一个awesome-cli-apps收集了很多优秀的CLI程序,值得一看。我推荐exa,用rust编写的现代版ls

命令行程序

通常,命令行程序看起来像这样:

1
$ ./program_name [arguments] [flags] [options]

一般我们会使用-h--help命令以查看该命令的帮助信息。

cargo为例:

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
28
29
30
31
32
33
34
35
$ cargo -h
Rust's package manager

USAGE:
cargo [OPTIONS] [SUBCOMMAND]

OPTIONS:
-V, --version Print version info and exit
--list List installed commands
--explain <CODE> Run `rustc --explain CODE`
-v, --verbose Use verbose output (-vv very verbose/build.rs output)
-q, --quiet No output printed to stdout
--color <WHEN> Coloring: auto, always, never
--frozen Require Cargo.lock and cache are up to date
--locked Require Cargo.lock is up to date
-Z <FLAG>... Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
-h, --help Prints help information

Some common cargo commands are (see all commands with --list):
build Compile the current project
check Analyze the current project and report errors, but don't build object files
clean Remove the target directory
doc Build this project's and its dependencies' documentation
new Create a new cargo project
init Create a new cargo project in an existing directory
run Build and execute src/main.rs
test Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this project to the registry
install Install a Rust binary
uninstall Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.

一目了然,如此我们就知道如何使用cargo了。

创建项目

让我们开始构建一个新的命令行程序吧!

我在这里将项目命名为meow

1
2
$ cargo new meow
$ cd meow

参数

正如上面看到CLI的样子,CLI应有一些参数。

添加参数最简单的方法是:

1
2
3
4
5
6
7
// main.rs
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}
1
2
$./meow a1 a2 a3
["meow", "a1", "a2", "a3"]

如此,程序即可打印参数。

不过,我摸实际使用到的CLI程序更会加复杂:

1
2
$ ./foo -g -e a1 a3 a4
$ ./foo a1 -e -l --path=~/test/123

可见这种简单的实现使用起来很不方便,因为:

  • 参数可能有默认值
  • 标识会交换位置
  • 选项会交换位置
  • arg1可能会绑定arg2

因此,需要一个库来帮助我们轻松完成这项工作。

Clap

Clap是一个全面、高效的Rust命令行参数解析器,用法如下:

YAML接口已废弃

参见Deprecate Yaml API #3087,现行的clap版本已废弃YAML API,下面的例子需要使用2版本进行编译:

首先,创建一个cli.yml文件来设置参数。看起来像:

cli.yml
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
28
name: myapp
version: "1.0"
author: Kevin K. <kbknapp@gmail.com>
about: Does awesome things
args:
- config:
short: c
long: config
value_name: FILE
help: Sets a custom config file
takes_value: true
- INPUT:
help: Sets the input file to use
required: true
index: 1
- verbose:
short: v
multiple: true
help: Sets the level of verbosity
subcommands:
- test:
about: controls testing features
version: "1.3"
author: Someone E. <someone_else@other.com>
args:
- debug:
short: d
help: print debug information

然后我们在main.rs中添加如下代码:

main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[macro_use]
extern crate clap;
use clap::App;

fn main() {
// The YAML file is found relative to the current file, similar to how modules are found
let yaml = load_yaml!("cli.yml");
let m = App::from_yaml(yaml).get_matches();

match m.value_of("argument1") {
// ...
}

// ...
}

clap将加载和解析yml配置,以使得我们可以在程序中使用这些参数。

上面cli.yml加上-h的程序运行结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ meow -h
My Super Program 1.0
Kevin K. <kbknapp@gmail.com>
Does awesome things

USAGE:
MyApp [FLAGS] [OPTIONS] <INPUT> [SUBCOMMAND]

FLAGS:
-h, --help Prints help information
-v Sets the level of verbosity
-V, --version Prints version information

OPTIONS:
-c, --config <FILE> Sets a custom config file

ARGS:
INPUT The input file to use

SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s)
test Controls testing features

现行版本示例

clap现行版本(3.0.14)官方示例:

添加现行版依赖:

Cargo.toml
1
2
[dependencies]
clap = { version = "3.0.14", features = ["derive"] }

使用Derive API,以在编写struct时能够直接通过属性访问Builder API

main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
/// Name of the person to greet
#[clap(short, long)]
name: String,

/// Number of times to greet
#[clap(short, long, default_value_t = 1)]
count: u8,
}

fn main() {
let args = Args::parse();

for _ in 0..args.count {
println!("Hello {}!", args.name)
}
}

观察输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ demo --help
clap [..]
Simple program to greet a person

USAGE:
demo[EXE] [OPTIONS] --name <NAME>

OPTIONS:
-c, --count <COUNT> Number of times to greet [default: 1]
-h, --help Print help information
-n, --name <NAME> Name of the person to greet
-V, --version Print version information

$ demo -n world
Hello world!

配置

CLI程序还需要配置。有些参数应当在运行前确定,并记录在配置文件中,如.env.config.setting

一个.env文件的例子:

1
2
3
4
5
PORT = 8000
PATH = "home/foo/bar"
MODE = "happy mode"
ZONE = 8
AREA = "Beijing"

你可以选择手动处理,如:

  • 读取文件.env
  • 先按\n拆分每行
  • 再按=拆分各项并将数据添加到HashMap

或使用一个现成的crate。

dotenv_codegen

dotenv_codegen是一个带有宏的简单.env配置解析器。

使用这个crate可以轻松处理.env

1
2
3
4
5
6
7
use dotenv_codegen::dotenv;

// ...

fn main() {
println!("{}", dotenv!("PORT"));
}

注:.env是在meow根目录下的配置文件,它在编译时就被dotenv!读取,因此编译后再修改配置文件不会改变二进制文件meow的输出。

环境变量

您可能还想调用系统中的环境变量,例如JAVA_HOME

1
2
3
4
5
6
7
8
9
use std::env;

let key = "HOME";
match env::var_os(key) {
Some(val) => println!("{}: {:?}", key, val),
None => println!("{} is not defined in the environment.", key)
}

// 打印环境变量`HOME`的值。

错误处理

最常见的:

1
panic!("this is panic");

这种方式过于简单:

  • 它会直接终止程序
  • 它在退出时没有错误代码
  • 它通常使用在小型脚本中

使用Result

Result可以传递错误而不终止程序。如果函数中断,它将返回带有错误类型的Error。于是我们可以根据类型来决定下一步做什么,比如“重试”或者“放弃”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum MyErr {
Reason1,
Reason2,
}
fn foo() -> Result<(), MyErr> {
match bar {
Some(_)=>{}
None => Err(MyErr::Reason1)
}
}
fn hoo() {
match foo() {
Ok(_) => reply(),
Err(e) => println!(e)
// 返回`e`其实还没有结束
// 应该继续使用`fmt`明确该错误信息
}
}

错误信息

如果我们希望为错误类型打印特定的错误信息,则需要为MyErr实现fmttrait,以使各类错误具有特定的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum MyErr {
Reason1(String),
Reason2(String, u32),
}
impl fmt::Display for MyErrError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MyErr::Reason1(ref s) =>
write!(f, "`{}` is the error", s),
MyErr::Reason2(ref s, ref num) =>
write!(f, "`{}` and `{}` are error", s, num),
}
}
}

使用:

1
2
Err(e) => println!("{}", e)
// `XXX` is the error

标准错误

系统中有标准输出和标准错误。

println!()是标准输出,eprintln!()是标准错误。

例如:

1
$ cargo run > output.txt

如此仅将标准输出重定向到文件output.txt中。

因此,如果我们不想在output.txt文件中写入错误消息,就可以使用eprintln!()将错误打印为标准错误。

退出码

在程序有问题时使用非零退出码,以通知其他程序“我出错了”。

1
2
3
4
5
use std::process;

fn main() {
process::exit(1);
}

总结

CLI程序可以胜任各种场景,而好的CLI程序需要良好的设计。CLI程序应该能解析参数和配置、读取环境变量、可以很好地处理错误、能够在标准输出和标准错误中输出相应的信息,并在执行失败时输出非零退出代码。

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×