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

文章目录

  • 一、解析命令行参数
    • 1.1 简单参数
    • 1.2 数据类型解析-手动解析
    • 1.3 用 clap 库解析
    • 1.4 收尾
  • 二、实现 grep 命令行
    • 2.1 读取文件,过滤关键字
    • 2.2 错误处理
      • 2.2.1 Result 类型
      • 2.2.2 UNwraping
      • 2.2.3 不需要 panic
      • 2.2.4 ? 问号符号
      • 2.2.5 提供错误上下文-自定义 CustomError struct
      • 2.2.6 anyhow 库
      • 2.2.7 Wrapping up 收尾工作
    • 2.3 输出日志和进度条
      • 2.3.1 println!
      • 2.3.2 打印错误
      • 2.3.3 打印的性能
      • 2.3.4 indicatif 显示进度条
      • 2.3.5 日志
    • 2.4 Test
      • 2.4.1 单测
      • 2.4.2 让代码可测试
      • 2.4.3 将代码拆分为 library 和 binary targets
      • 2.4.4 创建临时测试文件
    • 2.5 package 和 distributing
      • 2.5.1 cargo publish
      • 2.5.2 用 cargo install 从 crates.io 安装 binary
      • 2.5.3 distributing binaries
      • 2.5.4 在 CI build binary release
    • 2.5.5 开源示例
  • 三、高级话题
    • 3.1 信号处理 Signal Handling
      • 3.1.1 处理其他 signal 类型
      • 3.1.2 用 channel
      • 3.1.3 用 futures 和 streams
    • 3.2 使用配置文件
    • 3.3 exit code
    • 3.4 人类可读
    • 3.5 机器可读:pipe
  • 四、相关 crates

一、解析命令行参数

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 sourceid: String,/// the start timestamp of the sourcesrc_start_ts: i64,/// the end timestamp of the sourcesrc_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 argumentsUsage: 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 sourceOptions:-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 forpattern: String,/// the path to the file to readpath: 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 forpattern: String,/// the path to the file to readpath: 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>
whereP: 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); // 自定义了 CustomErrorfn main() -> Result<(), CustomError> { // 将 main 的返回值变为了 CustomErrorlet path = "test.txt";let content = std::fs::read_to_string(path).map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?; // 自行错误转换,从 std::io::Error 到 CustomErrorprintln!("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 patternpattern: String,/// path to searchpath: 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 ... oktest 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 ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sfn 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 patternpattern: String,/// path to searchpath: 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 ... FAILEDfailures:---- 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

  • Command-line interface
  • Configuration
  • Database interfaces
  • Encoding
  • Filesystem
  • HTTP Client
  • Operating systems

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/261106.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java导出动态下拉框excel模板

1.原始模板 2.导出模板,下拉框为数据库中得到动态数据 public void downloadTemplate(HttpServletResponse response) throws IOException {// 所有部门List<String, String> departments expertManageMapper.selectAllDepartment();//所有职位List<String, String&g…

打码半年,开源一款自定义大屏设计软件!

hi&#xff0c;大家好&#xff0c;我是Tduck马马。 最近我们开源了一款大屏软件-TReport&#xff0c;与大家分享。 TReport是一款基于Vue3技术栈的数据可视化系统&#xff0c;支持静态、动态api等数据源&#xff1b;可用于数据可视化分析、报表分析、海报设计使用。 提供自定…

定制你的【Spring Boot Starter】,加速开发效率

摘要&#xff1a; 本文将介绍如何创建一个自定义的 Spring Boot Starter&#xff0c;让您可以封装常用功能和配置&#xff0c;并在多个 Spring Boot 项目中共享和重用。 1. 简介 Spring Boot Starter 是 Spring Boot 框架中的一种特殊的依赖项&#xff0c;它用于快速启动和配置…

计算机网络-广域通信网

1.广域网概念和分类 什么是广域网&#xff1f; 广域网是指长距离跨地区的各种局域网、计算机、终端互联在一起&#xff0c;组成一个资源共享的通信网络。 广域网分为传统广域网和现代广域网。 传 统 广 域 网公共交换电话网PSTN公共数据网X.25帧中继网FR综合业务数据网ISDN…

Linux 内存top命令详解

通过top命令可以监控当前机器的内存实时使用情况&#xff0c;该命令的参数解释如下&#xff1a; 第一行 15:30:14 —— 当前系统时间 up 1167 days, 5:02 —— 系统已经运行的时长&#xff0c;格式为时:分 1 users ——当前有1个用户登录系统 load average: 0.00, 0.01, 0.05…

时间获取,文件属性和权限的获取——C语言——day06

今天主要内容是时间获取以及文件属性和权限的获取 时间获取 1.time 1.time time_t time(time_t *tloc); 功能:返回1970-1-1到现在的秒数&#xff08;格林威治时间&#xff09; 参数:tloc:存放秒数空间首地址 返回值:成功返回秒数失败返回-12.localtime 2.localtimestruct t…

开发一款招聘小程序需要具备哪些功能?

随着时代的发展&#xff0c;找工作的方式也在不断变得简单&#xff0c;去劳务市场、人才市场的方式早就已经过时了&#xff0c;现在大多数年轻人都是直接通过手机来找工作。图片 找工作类的平台不但能扩大企业的招聘渠道&#xff0c;还能节省招聘的成本&#xff0c;方便求职者进…

Linux-时间接口-005

学习重点&#xff1a; 1.函数接口 2.【ls-l】命令的实现1【time】 1.1函数原型 【time_t time(time_t *tloc);】1.2函数功能 返回1970-1-1到现在的秒数&#xff08;格林威治时间&#xff09;1.3函数参数 1.3.1【tloc】 存放秒数空间首地址 存放的秒数&#xff1a;如果【t…

Java Web(六)--XML

介绍 官网&#xff1a;XML 教程 为什么需要&#xff1a; 需求 1 : 两个程序间进行数据通信&#xff1f;需求 2 : 给一台服务器&#xff0c;做一个配置文件&#xff0c;当服务器程序启动时&#xff0c;去读取它应当监听的端口号、还有连接数据库的用户名和密码。spring 中的…

数据结构--红黑树详解

什么是红黑树 红黑树(Red Black Tree)是一种自平衡二叉查找树。它是在 1972 年由 Rudolf Bayer 发明的,当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在 1978 年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。 由于其自平衡的特性,保证…

NBlog个人博客部署维护过程记录 -- 后端springboot + 前端vue

项目是fork的Naccl大佬NBlog项目&#xff0c;页面做的相当漂亮&#xff0c;所以选择了这个。可以参考2.3的效果图 惭愧&#xff0c;工作两年了也没个自己的博客系统&#xff0c;趁着过年时间&#xff0c;开始搭建一下. NBlog原项目的github链接&#xff1a;Naccl/NBlog: &#…

展示用HTML编写的个人简历信息

展示用HTML编写的个人简历信息 相关代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document…

网贷大数据查询多了对征信有影响吗?

网贷大数据在日常的金融借贷中起到很重要的风控作用&#xff0c;不少银行已经将大数据检测作为重要的风控环节。很多人在申贷之前都会提前了解自己的大数据信用情况&#xff0c;那网贷大数据查询多了对征信有影响吗?本文带你一起去看看。 首先要说结论&#xff1a;那就是查询网…

【AI视野·今日Robot 机器人论文速览 第七十八期】Wed, 17 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Wed, 17 Jan 2024 Totally 49 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Safe Mission-Level Path Planning for Exploration of Lunar Shadowed Regions by a Solar-Powered Rover Authors Olivier L…

24-k8s的附件组件-Metrics-server组件与hpa资源pod水平伸缩

一、概述 Metrics-Server组件目的&#xff1a;获取集群中pod、节点等负载信息&#xff1b; hpa资源目的&#xff1a;通过metrics-server获取的pod负载信息&#xff0c;自动伸缩创建pod&#xff1b; 参考链接&#xff1a; 资源指标管道 | Kubernetes https://github.com/kuberne…

SpringMVC第一天

SpringMVC简介 1.导入spring-mvc坐标 <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.3.7</version></dependency> 2.在web.xml里配置DispatcherServlet前端控制器 …

dubbo源码中设计模式——注册中心中工厂模式的应用

工厂模式的介绍 工厂模式提供了一种创建对象的方式&#xff0c;而无需指定要创建的具体类。 工厂模式属于创建型模式&#xff0c;它在创建对象时提供了一种封装机制&#xff0c;将实际创建对象的代码与使用代码分离。 应用场景&#xff1a;定义一个创建对象的接口&#xff0…

【AI学习】LangChain学习

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

Spring Boot项目怎么对System.setProperty(key, value)设置的属性进行读取加解密

一、前言 之前我写过一篇文章使用SM4国密加密算法对Spring Boot项目数据库连接信息以及yaml文件配置属性进行加密配置&#xff08;读取时自动解密&#xff09;&#xff0c;对Spring Boot项目的属性读取时进行加解密&#xff0c;但是没有说明对System.setProperty(key, value)设…

5 Nacos本地启动配置

1、修改启动配置 修改nacos-console模块的application.properties,具体如下: 其中,url参数详见链接: