【rust】7、命令行程序:std::env、clap 解析、anyhow 错误处理、indicatif 进度条库

一、解析命令行参数

1.1 简单参数

std::env::args() 提供了迭代器,下标从 0 开始

fn main() {
    let id = std::env::args().nth(1).expect("no id given");
    let src_start_ts = std::env::args().nth(2).expect("no src_start_ts given");
    let src_end_ts = std::env::args().nth(3).expect("no src_end_ts given");
    println!(
        "id: {}, src_start_ts: {}, src_end_ts: {}",
        id, src_start_ts, src_end_ts
    );
}

// cargo r a b c d
id: a, src_start_ts: b, src_end_ts: c

这样解析的参数都是 String 的,并没有数据类型

1.2 数据类型解析-手动解析

可以自定义数据类型

例如 grrs foobar test.txt 有两个参数,第一个参数 pattern 是一个 String,第二个参数 path 是一个文件路径。

示例如下,首先定义参数为 struct:

struct Cli {
    pattern: String,
    path: std::path::PathBuf,
}

然后手动解析到 struct 中:

struct Cli {
    id: String,
    src_start_ts: i64,
    src_end_ts: i64,
}

fn main() {
    let id = std::env::args().nth(1).expect("no id given");
    let src_start_ts = std::env::args().nth(2).expect("no src_start_ts given");
    let src_end_ts = std::env::args().nth(3).expect("no src_end_ts given");

    let args = Cli {
        id,
        src_start_ts: src_start_ts.parse().expect("src_start_ts not a number"),
        src_end_ts: src_end_ts.parse().expect("src_end_ts not a number"),
    };
    println!(
        "id: {}, src_start_ts: {}, src_end_ts: {}",
        args.id, args.src_start_ts, args.src_end_ts
    );
}

// cargo r a b c d
thread 'main' panicked at src/main.rs:14:44:
src_start_ts not a number: ParseIntError { kind: InvalidDigit }

// cargo r a 11 22 33
id: a, src_start_ts: 11, src_end_ts: 22

这样确实工作了,但是很麻烦

1.3 用 clap 库解析

最流行的库是 https://docs.rs/clap/,它包括子命令、自动补全、help 信息。

首先运行 cargo add clap --features derive,caogo 会自动帮我们在 Cargo.toml 中添加依赖 clap = { version = "4.5.1", features = ["derive"] }"

use clap::Parser;

#[derive(Parser)]
struct Cli {
    id: String,
    src_start_ts: i64,
    src_end_ts: i64,
}

fn main() {
    let args = Cli::parse();
    println!(
        "id: {:?}, src_start_ts: {:?}, src_end_ts: {:?}",
        args.id, args.src_start_ts, args.src_end_ts
    );
}

// cargo r a 11 22 33
error: unexpected argument '33' found
Usage: pd <ID> <SRC_START_TS> <SRC_END_TS>

// cargo r a 11 22
id: "a", src_start_ts: 11, src_end_ts: 22

clap 知道该 expect 什么 fields,以及他们的格式

1.4 收尾

用 /// 添加注释,会被 clap 库识别,并打印到 help 信息中

use clap::Parser;

/// parse the command line arguments
#[derive(Parser)]
struct Cli {
    /// the id of the source
    id: String,
    /// the start timestamp of the source
    src_start_ts: i64,
    /// the end timestamp of the source
    src_end_ts: i64,
}

fn main() {
    let args = Cli::parse();
    println!(
        "id: {:?}, src_start_ts: {:?}, src_end_ts: {:?}",
        args.id, args.src_start_ts, args.src_end_ts
    );
}

// cargo r -- --help
parse the command line arguments

Usage: pd <ID> <SRC_START_TS> <SRC_END_TS>

Arguments:
  <ID>            the id of the source
  <SRC_START_TS>  the start timestamp of the source
  <SRC_END_TS>    the end timestamp of the source

Options:
  -h, --help     Print help
  -V, --version  Print version

二、实现 grep 命令行

2.1 读取文件,过滤关键字

use clap::Parser;

/// search for a pattern in a file and display the lines that contain it
#[derive(Parser)]
struct Cli {
    /// the pattern to look for
    pattern: String,
    /// the path to the file to read
    path: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path).expect("could not read file");
    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }
}

// Cargo.toml 如下:
[package]
name = "pd"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.1", features = ["derive"] }

// 运行 cargo r version Cargo.toml 输出如下,成功过滤了文字
version = "0.1.0"
clap = { version = "4.5.1", features = ["derive"] }

read_to_string() 会一次性将全部文件读入内存,也可以用 BufReader 替代,如下:

use std::{fs::File, io::BufRead, io::BufReader};

use clap::Parser;

/// search for a pattern in a file and display the lines that contain it
#[derive(Parser)]
struct Cli {
    /// the pattern to look for
    pattern: String,
    /// the path to the file to read
    path: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();
    let f = File::open(&args.path).expect("could not open file");
    let reader = BufReader::new(f);
    reader.lines().for_each(|line| {
        if let Ok(line) = line {
            if line.contains(&args.pattern) {
                println!("{}", line);
            }
        }
    });
}

// 运行 cargo r version Cargo.toml 输出如下,成功过滤了文字 (与上文相同)
version = "0.1.0"
clap = { version = "4.5.1", features = ["derive"] }

2.2 错误处理

目前只能由 clap 框架处理错误,而无法自定义错误处理。因为 Rust 的 Result Enum 中由 Ok 和 Err 两种枚举,所以处理错误很方便。

2.2.1 Result 类型

read_to_string 函数并不仅仅返回一个 String,而是返回一个 Result,其中包含 String 和 std::io::Error。

std::fs
pub fn read_to_string<P>(path: P) -> io::Result<String>
where
    P: AsRef<Path>,

// 示例如下:
use std::fs;
use std::net::SocketAddr;

fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
	let foo: SocketAddr = fs::read_to_string("address.txt")?.parse()?;
	Ok(())
}

错误处理的示意如下:

fn main() {
    let result = std::fs::read_to_string("test.txt");
    match result {
        Ok(content) => {
            println!("File content: {}", content)
        }
        Err(error) => {
            println!("occur an error: {}", error)
        }
    }
}

// cargo r (当test.txt 存在且内容为 abc 时)
File content: abc

// cargo r (当test.txt 不存在时)
occur an error: No such file or directory (os error 2)

2.2.2 Unwraping

现在可以读取文件内容,但是在 match block 之后却无法做任何事。因此,需要处理 error,挑战是每个 match 的分支都需要返回某种东西。但是有巧妙的技巧可以解决这一点。即把 match 的返回值赋值给变量。

fn main() {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => content,
        Err(error) => {
            panic!("cannot deal with {}, just exit here", error)
        }
    };
    println!("file content: {}", content);
}

// cargo r
file content: 192.168.2.1

如上例,let content 中的 content 是 String 类型,如果 match 返回 error,则 String 将不存在。但因为此时程序已被 panic,也是可以接受的。 即需要 test.txt 必须存在,否则就 panic

和如下简便的写法是等价的:

fn main() {
    let content = std::fs::read_to_string("test.txt").unwrap();
}

2.2.3 不需要 panic

当然,在 match 的 Err 分支 panic! 并不是唯一的办法,还可以用 return。但需要改变 main() 函数的返回值

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => content,
        Err(error) => return Err(error.into()),
    };
    println!("File content: {}", content);
    Ok(())
}

// cargo r(当 test.txt 存在,且内容为 abc 时)
File content: abc

// cargo r(当 test.txt 不存在时)
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" } // 直接从 match 的 Err 分支 的 return 语句返回了 main 函数,使 main 结束了

因为返回值是 Result!,所以在 match 的第二个分支 通过 return Err(error) 返回。main 函数的最后一行是默认返回值。

2.2.4 ? 问号符号

就像用 .unwrap() 可以匹配 match 的 panic! 一样,? 也可以(是.unwrap() 的缩写)。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("test.txt")?;
    println!("File content: {}", content);
    Ok(())
}

这里还发生了一些事情,不需要理解就可以使用它。例如,我们主函数中的错误类型是Box。但我们在上面已经看到,read_to_string() 返回一个std::io::Error。这能行得通是因为?扩展为转换错误类型的代码。

Box 也是一个有趣的类型。它是一个Box,可以包含 implements Error trait 的任何类型。这意味着基本所有 errors 都可以被放入 Box 中。所以我们才可以用 ? 做 std::io::Error 到 Box> 的类型转换。

2.2.5 提供错误上下文-自定义 CustomError struct

? 可以工作,但并不是最佳实践。比如当 test.txt 并不存在时,用 std::fs::read_to_string("test.txt")? 会得到 Error: Os { code: 2, kind: NotFound, message: "No such file or directory" 的错误,错误并不明显,因为并不知道具体哪个文件没找到。

有很多种解决办法:

比如自定义 error type,用它构建 custom error message:

#[derive(Debug)]
struct CustomError(String); // 自定义了 CustomError

fn main() -> Result<(), CustomError> { // 将 main 的返回值变为了 CustomError
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?; // 自行错误转换,从 std::io::Error 到 CustomError
    println!("File content: {}", content);
    Ok(())
}

这种模式比较常见,虽然它有问题:它并不存储原始的 error,只是存储了 string 的解释。

2.2.6 anyhow 库

https://docs.rs/anyhow 库有巧妙的解决方案,很像 CustomError type,它的 Context trait 可以添加描述,并且还保持了原始的 error,因此我们可以得到 从 root cause 开始的 error message chain。

首先 cargo add anyhow,然后完整的示例如下:

use anyhow::{Context, Result};
fn main() -> Result<()> {
    let path = "test.txt";
    let content =
        std::fs::read_to_string(path).with_context(|| format!("could not read file `{}`", path))?; // with_context 是 anyhow 库提供的方法,其中我们指定了 path,这样用户可以知道错误的上下文
    println!("File content: {}", content);
    Ok(())
}

// cargo r(当 test.txt 存在,且内容为 abc 时)
File content: abc

// cargo r(当 test.txt 不存在时)
Error: could not read file `test.txt` // 因为指明了 path,所以错误很明晰

Caused by:
    No such file or directory (os error 2)

2.2.7 Wrapping up 收尾工作

完整代码如下:

use anyhow::{Context, Result};
use clap::Parser;

/// my cli
#[derive(Parser)]
struct Cli {
    /// my pattern
    pattern: String,
    /// path to search
    path: std::path::PathBuf,
}

fn main() -> Result<()> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path)
        .with_context(|| format!("could not read file {:?}", &args.path))?;
    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line)
        }
    }
    Ok(())
}

// cargo r let src/main.r
let args = Cli::parse();
let content = std::fs::read_to_string(&args.path)

2.3 输出日志和进度条

2.3.1 println!

println!() 中 {} 占位符可以表示实现了 Display 的类型如数字、字符串,而 {:?} 可以表示其他实现了 Debug trait 的类型。示例如下:

let xs = vec![1, 2, 3];
println!("The list is: {:?}", xs);

// cargo r
The list is: [1, 2, 3]

2.3.2 打印错误

错误尽量打印到 stderr,方便其他程序或 pipe 收集。(普通信息通过 println! 打印到 stdout,错误信息通过 eprintln! 打印到 stderr)

println!("This is information");
eprintln!("This is an error!");

PS:如果想控制台打印颜色的话,直接打印会有问题,我们要用 ansi_term 库。

2.3.3 打印的性能

println! 是很慢的,如果循环调用很容易成为性能瓶颈。

有两种方案,这两种方案可以组合使用:

首先,可以减少 flush 到 terminal 的次数。默认每次 println! 都会 flush,我们可以用 BufWriter 包装 stdout,这样可以 buffer 8KB,也可以通过 .flush() 手动 flush()。

#![allow(unused)]
use std::io::{self, Write};

fn main() {
    let stdout = io::stdout();
    let mut handle = io::BufWriter::new(stdout);
    writeln!(handle, "foo: {}", 42);
}

// cargo r
foo: 42

其次,可以获取 stdout 或 stderr 的 lock,并用 writeln! 打印。这样阻止了系统反复 lock 和 unlock。

#![allow(unused)]
use std::io::{self, Write};

fn main() {
    let stdout = io::stdout();
    let mut handle = stdout.lock();
    writeln!(handle, "foo: {}", 42);
}

// cargo r
foo: 42

2.3.4 indicatif 显示进度条

用 https://crates.io/crates/indicatif 库

use std::thread;
use std::time::Duration;

fn main() {
    let pb = indicatif::ProgressBar::new(100);
    for i in 0..100 {
        thread::sleep(Duration::from_secs(1));
        pb.println(format!("[+] finished #{}", i));
        pb.inc(1)
    }
    pb.finish_with_message("done");
}

// cargo r
[+] finished #11
[+] finished #12
[+] finished #13
[+] finished #14
[+] finished #15
[+] finished #16
█████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 17/100

// 最终
████████████████████████████████████████████████████████████████████████████████ 100/100

2.3.5 日志

需要 https://crates.io/crates/log (它包括 log level 的定义) 和一个 adapter that actually writes the log outout somewhere useful。可以写日志到 terminal、syslog 或 一个 log server。

写 cli 工具,最方便的 adapter 是 https://crates.io/crates/env_logger(它的名称含 env 是因为,它可以通过环境变量控制想写到哪儿),它会在日志前打印 timestamp 和 module 名。

示例如下:

use log::{info, warn};

fn main() {
    env_logger::init();
    info!("starting up");
    warn!("oops, nothing implemented!");
}

// cargo r// env rust_LOG=info cargo r 或 rust_LOG=info cargo r
[2024-02-20T04:38:43Z INFO  grrs] starting up
[2024-02-20T04:38:43Z WARN  grrs] oops, nothing implemented!

经验表明,为了方便实用,可以用 --verbose 参数控制是否打印详细日志。https://crates.io/crates/clap-verbosity-flag 可以很方便的实现此功能。

2.4 Test

养成习惯,先写 README 再实现,用 TDD 方法实现(测试驱动开发)。

2.4.1 单测

通过 #[test] 可以执行单测

fn answer() -> i32 {
    42
}

#[test]
fn check_answer_validity() {
    assert_eq!(answer(), 42);
}

// cargo t
running 1 test
test check_answer_validity ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

2.4.2 让代码可测试

#![allow(unused)]
fn main() {
fn find_matches(content: &str, pattern: &str) {
    for line in content.lines() {
        if line.contains(pattern) {
            println!("{}", line);
        }
    }
}
}

#[test]
fn find_a_match() {
    find_matches("lorem ipsum\ndolor sit amet", "lorem");
    assert_eq!( // uhhhh

虽然可以抽取出 find_matches() 函数,但它直接输出到 stdout,而不是 return 值,不方便测试。

可通过 std::io::Write trait 捕获输出。trait 类似于其他语言的接口,可以抽象不同对象的行为。示例如下:

fn find_matches(content: &str, pattern: &str, mut writer: impl std::io::Write) { // impl std::io::Write 表示任何实现了 std::io::Write 的东西
    for line in content.lines() {
        if line.contains(pattern) {
            writeln!(writer, "{}", line);
        }
    }
}

#[test]
fn find_a_match() {
    let mut result = Vec::new();
    find_matches("lorem ipsum\ndolor sit amet", "lorem", &mut result);
    assert_eq!(result, b"lorem ipsum\n");
}

// cargo t
running 1 test
test find_a_match ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

fn main() -> Result<()> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path)
        .with_context(|| format!("could not read file `{}`", args.path.display()))?;

    find_matches(&content, &args.pattern, &mut std::io::stdout());

    Ok(())
}

// 注意:我们也可以让这个函数返回一个String,但这会改变它的行为。它不是直接写入终端,而是将所有内容收集到一个字符串中,并在最后一次性转储所有结果。

2.4.3 将代码拆分为 library 和 binary targets

目前代码全都在 src/main.rs文件中。这意味着我们当前的项目只生成一个二进制文件。但我们也可以将代码作为库提供,如下所示:

  1. 将 find_matches() 放入 src/lib.rs
  2. 在 fn find_matches() 前添加 pub 关键字。
  3. 移除 src/main.rs 中的 find_matches()
  4. 在 fn main() 中通过 grrs::find_matches() 调用。即使用 library 里的方法。

可以把特定逻辑写一个 lib,就像调用第三方 lib 一样。

注意:按照惯例,Cargo将在测试目录中查找集成测试。同样,它将在工作台/中寻找基准,在Examples/中寻找范例。这些约定还扩展到您的主要源代码:库有一个src/lib.ars文件,主二进制文件是src/main.rs,或者,如果有多个二进制文件,Cargo希望它们位于src/bin/.rs中。遵循这些约定将使习惯于阅读rust代码的人更容易发现您的代码库。

目前程序可以正常工作,但我们可以考虑可能发生的异常情况:

  • 文件不存在的行为?
  • 没有匹配到字符串的行为?
  • 忘记传入一些参数时,程序是否要退出?

cargo add assert_cmd predicates 是常用的测试库。

完整示例如下:

use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::process::Command;

#[test]
fn file_doesnt_exist() -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::cargo_bin("pd")?;
    cmd.arg("foobar").arg("test/file/doesnt/exist");
    cmd.assert()
        .failure()
        .stderr(predicate::str::contains("could not read file"));
    Ok(())
}

use anyhow::{Context, Result};
use clap::Parser;

/// my cli
#[derive(Parser)]
struct Cli {
    /// my pattern
    pattern: String,
    /// path to search
    path: std::path::PathBuf,
}

fn main() -> Result<()> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path)
        .with_context(|| format!("could not read file {:?}", &args.path))?;
    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line)
        }
    }
    Ok(())
}

// cargo t
running 1 test
test file_doesnt_exist ... FAILED

failures:

---- file_doesnt_exist stdout ----
thread 'file_doesnt_exist' panicked at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5:
Unexpected success
 "foobar" "test/file/doesnt/exist"`

2.4.4 创建临时测试文件

下面是一个新的测试用例(你可以写在另一个下面),它首先创建一个临时文件(一个“命名”的文件,这样我们就可以得到它的路径),用一些文本填充它,然后运行我们的程序来看看我们是否得到正确的输出。当文件超出作用域时(在函数结束时),实际的临时文件将被自动删除。

cargo add assert_fs

use assert_fs::prelude::*;

#[test]
fn find_content_in_file() -> Result<(), Box<dyn std::error::Error>> {
    let file = assert_fs::NamedTempFile::new("sample.txt")?; // 产生临时文件
    file.write_str("A test\nActual content\nMore content\nAnother test")?; // 写入临时文件

    let mut cmd = Command::cargo_bin("grrs")?;
    cmd.arg("test").arg(file.path());
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("A test\nAnother test"));

    Ok(())
}

2.5 package 和 distributing

2.5.1 cargo publish

将一个 crate 发布到 crates.io 非常简单:在crates.io上创建一个帐户(授权 GitHub 账户)。在本地电脑上用 cargo 登录。为此,需要在 https://crates.io/me 页创建一个新token,然后 cargo login 。每个电脑只需要执行一次。可以在 https://doc.rust-lang.org/1.39.0/cargo/reference/publishing.html 找到更详细的资料。

现在已经可以 publish 了,但记得检查 Cargo.toml 确保包含足够的信息。在 https://doc.rust-lang.org/1.39.0/cargo/reference/manifest.html 可以找到全部信息。如下是一个常见的示例:

[package]
name = "grrs"
version = "0.1.0"
authors = ["Your Name <your@email.com>"]
license = "MIT OR Apache-2.0"
description = "A tool to search files"
readme = "README.md"
homepage = "https://github.com/you/grrs"
repository = "https://github.com/you/grrs"
keywords = ["cli", "search", "demo"]
categories = ["command-line-utilities"]

2.5.2 用 cargo install 从 crates.io 安装 binary

cargo install 会下载、编译(用 release mode)、拷贝到 ~/.cargo/bin。也可以指定 git 做源。详见 cargo install --help

cargo install 很方便但也有如下缺点:因为它总是从头开始编译您的源代码,所以您的工具的用户将需要在他们的计算机上安装您的项目所需的rust、Cargo和所有其他系统依赖项。编译大型rust代码库也可能需要一些时间。

最好用它来分发面向其他 rust developer 的工具。例如用来安装 cargo-tree、cargo-outdated 这些工具。

2.5.3 distributing binaries

rust 会静态编译所有依赖的库。当您在包含名为 grrs 的 binary project上运行 cargo build 时,最终将得到一个名为 grrs 的 binary(二进制文件)。

  • 如果运行 cargo build,它将是 target/debug/grrs
  • 如果运行 cargo build --release 时,它将是 target/release/grrs。除非你用了一个必须依赖外部库的库(如使用 system version 的 openssl),否则这个 binary 是直接可以运行开箱即用的。

2.5.4 在 CI build binary release

如果您的工具是开源的并托管在GitHub上,那么很容易建立一个像Travis CI这样的免费CI(持续集成)服务。(还有其他服务也可以在其他平台上使用,但Travis非常受欢迎。) 。这基本上是在每次将更改推送到存储库时,在虚拟机中运行设置命令。这些命令和运行它们的机器类型是可配置的。例如:装有rust和一些常见构建工具的机器上运行cargo test命令。如果失败了,就说明最近的更改中存在问题。

我们还可以用它来构建二进制文件并将它们上传到GitHub!实际上,如果我们运行 cargo build --release 并将二进制文件上传到某个地方,我们应该已经设置好了,对吗?不完全是。我们仍然需要确保我们构建的二进制文件与尽可能多的系统兼容。例如,在Linux上,我们可以不针对当前系统进行编译,而是针对x86_64-UNKNOWN-LINUX-MUSL目标进行编译,使其不依赖于默认系统库。在MacOS上,我们可以将MacOSX_DEPLOYMENT_TARGET设置为10.7,以仅依赖10.7版及更早版本中的系统功能。

2.5.5 开源示例

https://github.com/BurntSushi/ripgrep 是一个 rust 实现的 grep/ack/ag,

三、高级话题

3.1 信号处理 Signal Handling

https://crates.io/crates/ctrlc 可以处理 ctrl+c,支持跨平台。

use std::{thread, time::Duration};

fn main() {
    ctrlc::set_handler(move || {
        println!("received Ctrl+C!");
    })
    .expect("Error setting Ctrl-C handler");

    // Following code does the actual work, and can be interrupted by pressing
    // Ctrl-C. As an example: Let's wait a few seconds.
    thread::sleep(Duration::from_secs(20));
}

在实际的程序中,一个好的做法是在信号处理程序中设置一个变量,然后在程序的各个地方进行检查。例如,你可以在信号处理程序中设置一个Arc<AtomicBool>(一个可以在多个线程之间共享的布尔变量),在 loops 中或者等待线程时,定期检查其值,并在其变为true时跳出循环。

3.1.1 处理其他 signal 类型

ctrlc 只能处理 Ctrl+C signal,如果想处理其他信号,可以参考 https://crates.io/crates/signal-hook,设计文档为 https://vorner.github.io/2018/06/28/signal-hook.html

use signal_hook::{consts::SIGINT, iterator::Signals};
use std::{error::Error, thread, time::Duration};

fn main() -> Result<(), Box<dyn Error>> {
    let mut signals = Signals::new(&[SIGINT])?;

    thread::spawn(move || {
        for sig in signals.forever() {
            println!("Received signal {:?}", sig);
        }
    });

    // Following code does the actual work, and can be interrupted by pressing
    // Ctrl-C. As an example: Let's wait a few seconds.
    thread::sleep(Duration::from_secs(2));

    Ok(())
}

3.1.2 用 channel

您可以使用通道,而不是设置变量并让程序的其他部分检查它:您创建一个通道,信号处理程序在接收信号时向该通道发送值。在您的应用程序代码中,您将此通道和其他通道用作线程之间的同步点。使用 https://crates.io/crates/crossbeam-channel,示例如下:

use std::time::Duration;
use crossbeam_channel::{bounded, tick, Receiver, select};
use anyhow::Result;

// 创建一个控制通道,用于接收ctrl+c信号
fn ctrl_channel() -> Result<Receiver<()>, ctrlc::Error> {
    // 创建一个有限容量的通道,用于发送ctrl+c事件
    let (sender, receiver) = bounded(100);

    // 设置ctrl+c信号处理器,在接收到ctrl+c信号时发送事件到通道
    ctrlc::set_handler(move || {
        let _ = sender.send(());
    })?;

    Ok(receiver)
}

fn main() -> Result<()> {
    // 获取ctrl+c事件的接收器
    let ctrl_c_events = ctrl_channel()?;
    // 创建一个定时器,每隔1秒发送一个事件
    let ticks = tick(Duration::from_secs(1));

    loop {
        select! {
            // 当收到定时器的事件时,执行以下代码块
            recv(ticks) -> _ => {
                println!("working!");
            }
            // 当收到ctrl+c事件时,执行以下代码块
            recv(ctrl_c_events) -> _ => {
                println!();
                println!("Goodbye!");
                break;
            }
        }
    }

    Ok(())
}

3.1.3 用 futures 和 streams

https://tokio.rs/ 适合异步、事件驱动。可以 enable signal-hook’s tokio-support feature。从而在 signal-hook crate 的 Signals 类型上调用 into_async() 方法,以便获取 futures::Streams 类型。

3.2 使用配置文件

https://docs.rs/confy/0.3.1/confy/。指定配置文件的路径,在 struct 上设置 Serialize, Deserialize,就可以工作了。

#[derive(Debug, Serialize, Deserialize)]
struct MyConfig {
    name: String,
    comfy: bool,
    foo: i64,
}

fn main() -> Result<(), io::Error> {
    let cfg: MyConfig = confy::load("my_app")?;
    println!("{:#?}", cfg);
    Ok(())
}

3.3 exit code

程序成功时,应 exit 0,否则应介于 0 到 255 之间。有一些 BSD 平台下退出码的通用定义,这个库实现了它 https://crates.io/crates/exitcode。

fn main() {
    // ...actual work...
    match result {
        Ok(_) => {
            println!("Done!");
            std::process::exit(exitcode::OK);
        }
        Err(CustomError::CantReadConfig(e)) => {
            eprintln!("Error: {}", e);
            std::process::exit(exitcode::CONFIG);
        }
        Err(e) => {
            eprintln!("Error: {}", e);
            std::process::exit(exitcode::DATAERR);
        }
    }
}

3.4 人类可读

默认的 panic 日志如下:

thread 'main' panicked at 'Hello, world!', src/main.rs:2:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

可以用 https://crates.io/crates/human-panic 让错误日志更让人可读,如下:

use human_panic::setup_panic;
fn main() {
   setup_panic!();

   panic!("Hello world")
}

// cargo r
Well, this is embarrassing.

foo had a problem and crashed. To help us diagnose the problem you can send us a crash report.

We have generated a report file at "/var/folders/n3/dkk459k908lcmkzwcmq0tcv00000gn/T/report-738e1bec-5585-47a4-8158-f1f7227f0168.toml". Submit an issue or email with the subject of "foo Crash Report" and include the report as an attachment.

- Authors: Your Name <your.name@example.com>

We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.

Thank you kindly!

3.5 机器可读:pipe

But what if we wanted to count the number of words piped into the program? Rust programs can read data passed in via stdin with the Stdin struct which you can obtain via the stdin function from the standard library. Similar to reading the lines of a file, it can read the lines from stdin.

Here’s a program that counts the words of what’s piped in via stdin

use clap::{CommandFactory, Parser};
use is_terminal::IsTerminal as _;
use std::{
    fs::File,
    io::{stdin, BufRead, BufReader},
    path::PathBuf,
};

/// Count the number of lines in a file or stdin
#[derive(Parser)]
#[command(arg_required_else_help = true)]
struct Cli {
    /// The path to the file to read, use - to read from stdin (must not be a tty)
    file: PathBuf,
}

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

    let word_count;
    let mut file = args.file;

    if file == PathBuf::from("-") {
        if stdin().is_terminal() {
            Cli::command().print_help().unwrap();
            ::std::process::exit(2);
        }

        file = PathBuf::from("<stdin>");
        word_count = words_in_buf_reader(BufReader::new(stdin().lock()));
    } else {
        word_count = words_in_buf_reader(BufReader::new(File::open(&file).unwrap()));
    }

    println!("Words from {}: {}", file.to_string_lossy(), word_count)
}

fn words_in_buf_reader<R: BufRead>(buf_reader: R) -> usize {
    let mut count = 0;
    for line in buf_reader.lines() {
        count += line.unwrap().split(' ').count()
    }
    count
}

四、相关 crates

  • anyhow - provides anyhow::Error for easy error handling
  • assert_cmd - simplifies integration testing of CLIs
  • assert_fs - Setup input files and test output files
  • clap-verbosity-flag - adds a --verbose flag to clap CLIs
  • clap - command line argument parser
  • confy - boilerplate-free configuration management
  • crossbeam-channel - provides multi-producer multi-consumer channels for message passing
  • ctrlc - easy ctrl-c handler
  • env_logger - implements a logger configurable via environment variables
  • exitcode - system exit code constants
  • human-panic - panic message handler
  • indicatif - progress bars and spinners
  • is-terminal - detected whether application is running in a tty
  • log - provides logging abstracted over implementation
  • predicates - implements boolean-valued predicate functions
  • proptest - property testing framework
  • serde_json - serialize/deserialize to JSON
  • signal-hook - handles UNIX signals
  • tokio - asynchronous runtime
  • wasm-pack - tool for building WebAssembly

在 lib.rs 可以看到各种 crates

4.1 clap

4.1.1 解析参数

4.1.1.1 数组
 #[clap(num_args=1.., value_delimiter=',')]
    /// ids, 如'1, a, b, c'
    ids: Vec<String>,

参考 https://stackoverflow.com/questions/74936109/how-to-use-clap-to-take-create-a-vector

clap 接收数组
You can use num_args to specify a range for the number of argument occurences and values allowed like so:
use clap::Parser;

#[derive(Parser)]
pub struct Args {
    #[clap(short, long, value_parser, num_args = 1.., value_delimiter = ' ')]
    pub files: Vec<String>,
  }

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

    println!("files: {:?}", args.files);
}

This will allow both
cargo run -- --files hello world
and
cargo run -- --files hello --files world

Specifying the value_delimiter is not strictly neccessary here, but I just wanted to point out that you can use different characters as delimiters like this.
If you also want to allow empty arrays beeing passed, you can change the num_args attribute like so num_args = 0...
4.1.1.2 时间戳 timestamp

自定义解析类型, 如解析 timestimp
https://stackoverflow.com/questions/72313616/using-claps-deriveparser-how-can-i-accept-a-stdtimeduration

  • 37
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值