【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.mzph.cn/news/692685.shtml

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

相关文章

山海鲸可视化软件:多场景下的数据呈现利器

在当今数据驱动的时代&#xff0c;数据可视化成为了企业和个人不可或缺的工具。作为一个老数据人&#xff0c;本文想借用自己常用山海鲸可视化软件&#xff0c;带大家了解在不同使用场景下数据可视化的应用。山海鲸可视化是一款可以免费编辑、本地化部署的产品&#xff0c;对数…

小程序域名可以使用免费的SSL证书吗?

对于小程序域名而言&#xff0c;选择何种类型的SSL证书主要取决于小程序域名的具体情况。如果小程序域名是单独的域名&#xff0c;那么可以选择最为常见的免费单域名证书&#xff1b;如果小程序是公司主域名的子域名&#xff0c;则可以选择免费的通配符证书&#xff0c;一张证书…

facebook群控如何做?静态住宅ip代理在多账号运营重的作用

在进行Facebook群控时&#xff0c;ip地址的管理是非常重要的&#xff0c;因为Facebook通常会检测ip地址的使用情况&#xff0c;如果发现有异常的使用行为&#xff0c;比如从同一个ip地址频繁进行登录、发布内容或者在短时间内进行大量的活动等等&#xff0c;就会视为垃圾邮件或…

【架构】GPU虚拟化

GPU虚拟化方法 远程调用API 半虚拟化和全虚拟化 硬件划分 局限&#xff1a;授权付费 Hardware Partition&#xff1a;Ampere 架构的 A100 GPU 所支持的 MIG&#xff0c;即是一种 Hardware Partition。它的问题是不灵活: 只有高端 GPU 支持&#xff1b;只支持 CUDA 计算&#x…

基于springboot校园志愿者管理系统源码和论文

随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;校园志愿者管理系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化&#xff0c;…

电脑恢复删除数据的原理和方法

在恢复数据的时候&#xff0c;很多人都会问&#xff0c;为什么删除的数据还能恢复&#xff1f;本篇和大家一起了解下硬盘上数据的存储方式&#xff0c;文件被删除的时候具体发生了什么&#xff0c;帮助大家理解数据恢复的基本原理。最后还会分享一个好用的数据恢复工具并附上图…

垂起固定翼无人机基础知识,垂起固定翼无人机应用前景,垂起固定翼无人机优缺点分析

无人机定义与类型 无人机&#xff0c;也称为无人驾驶飞行器&#xff0c;是一种无需人工直接操作的航空器。根据其用途、设计及技术特点&#xff0c;可以分为多种类型。垂起固定翼无人机是其中的一种&#xff0c;它具有垂直起降的能力并采用固定翼设计以提高飞行效率和稳定性。…

Excel SUMPRODUCT函数用法(成绩求和,分组排序)

SUMPRODUCT函数是Excel中功能比较强大的一个函数&#xff0c;可以实现sum,count等函数的功能&#xff0c;也可以实现一些基础函数无法直接实现的功能&#xff0c;常用来进行分类汇总&#xff0c;分组排序等 SUMPRODUCT 函数基础 SUMPRODUCT函数先计算多个数组的元素之间的乘积…

解决flask结合layui前端框架模板(laytpl 语法)与gin语法出现冲突的问题。

在模板中加了laytpl的写法 例如&#xff1a; gin框架渲染数据的语法也是{{ }} 例如&#xff1a; 如何两者都出现在html模板页&#xff0c;运行gin框架会识别为框架定义的变量&#xff0c;运行之后发现报如上错误&#xff0c;出现了错误是因为刚好gin的写法也是"{{ 变量 }…

【JavaEE】_CSS选择器

目录 1. 基本语法格式 2. 引入方式 2.1 内部样式 2.2 内联样式 2.3 外部样式 3. 基础选择器 3.1 标签选择器 3.2 类选择器 3.3 ID选择器 4. 复合选择器 4.1 后代选择器 4.2 子选择器 4.3 并集选择器 4.4 伪类选择器 1. 基本语法格式 选择器若干属性声明 2. 引入…

Leetcode日记 290. 单词规律 给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。 这里的 遵循 指完全匹配

Leetcode日记 290. 单词规律 给定一种规律 pattern 和一个字符串 s &#xff0c;判断 s 是否遵循相同的规律。 这里的 遵循 指完全匹配 解题思路制作不易&#xff0c;感谢三连&#xff0c;谢谢啦 给定一种规律 pattern 和一个字符串 s &#xff0c;判断 s 是否遵循相同的规律。…

WEB基础及http协议(Apache)

一、httpd安装组成 http服务基于C/S结构 1、常见http服务器程序 httpd apache&#xff0c;存在C10K&#xff08;10K connections&#xff09;问题nginx 解决C10K问题lighttpdIIS .asp 应用程序服务器tomcat .jsp 应用程序服务器jetty 开源的servlet容器&#xff0c;基于Java…

【Java代码审计】本地命令执行函数

1.本地命令执行函数 在服务器中时常会调用命令执行的代码&#xff0c;以完善或加强系统的功能需求&#xff0c;一旦这些调用命令执行的接口被攻击者恶意利用&#xff0c;就会导致服务器沦陷。 在Java中可用于执行系统命令的方式有API有&#xff1a;java.lang.Runtime、java.la…

基于微信小程序的比赛赛程管理系统设计与实现

在全面健身的倡导下通过各级赛事的举办完成体育人才的选拔&#xff0c;当由于缺乏信息化的管理手段而只能通过人工完成比赛报名、赛程制定及成绩记录等流程的管理&#xff0c;因此常常因意外而导致比赛赛程管理不善、成绩不理想等问题出现。为了帮助比赛组织者优化赛程管理流程…

微服务建构思想

微服务架构思想 微服务架构优点 1、易于开发和维护:一个微服务只会关注一个特定的业务功能。所以它业务清晰。代码量较少。开发和维护 单个微服务相对简单。而整个应用是由若干个微服务构建而成的。 2、单个微服务启动较快:单个微服务代码量较少&#xff0c;所以启动比较快。 …

CSS position属性sticky

在开发时&#xff0c;经常会碰到需要这样一种情况 —— 网站滚动到一定高度的时候&#xff0c;让一部分内容作为navbar&#xff0c;也就是置顶显示&#xff0c;我们一般会使用js监听scroll事件来实现&#xff0c;但是新增的css3属性position:sticky可以简单实现&#xff0c;省去…

【动态规划专栏】专题二:路径问题--------6.地下城游戏

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

Vue | (三)使用Vue脚手架(中)| 尚硅谷Vue2.0+Vue3.0全套教程

文章目录 &#x1f4da;Todo-list 案例&#x1f407;组件化编码流程&#xff08;通用&#xff09;&#x1f407;实现静态组件&#x1f407;展示动态数据&#x1f407;交互⭐️添加一个todo⭐️todo勾选实现⭐️删除功能实现⭐️底部统计功能实现⭐️底部全选功能实现⭐️底部一…

Docker从入门到上天系列第二篇:Docker与传统虚拟机对比

&#x1f609;&#x1f609; 欢迎加入我们的学习交流群呀&#xff01; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring、Security、Docker、Grpc、消息中间件、Rpc、SpringCloud等等很多应用和源码级…

代码随想录算法训练营day17||二叉树part04、110.平衡二叉树 、257. 二叉树的所有路径 、404.左叶子之和

注意&#xff1a;迭代法&#xff0c;可以先过&#xff0c;二刷有精力的时候 再去掌握迭代法。 110.平衡二叉树 &#xff08;优先掌握递归&#xff09; 再一次涉及到&#xff0c;什么是高度&#xff0c;什么是深度&#xff0c;可以巩固一下。 题目&#xff1a;给定一个二叉树&am…