【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 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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

sora生成高质量视频的原理

Sora是怎样生成视频的&#xff1f; 写在前面 Sora 是 OpenAI 在日前发布的超强视频生成 AI&#xff0c;旨在探索 AI 如何在理解真实世界运动和交互方面做得更好Sora目前无灰度体验 面临挑战 Sora面对的挑战就像是需要处理和理解来自世界各地、不同设备拍摄的数以百万计的图…

分布式扫描bean问题

今天我突然想到&#xff0c;为什么现在项目上会有一个 spring.factories 文件&#xff0c;原来它是用来批量扫描类&#xff0c;然后加到容器中的。 前几天我查了一下这个文件&#xff0c;发现这个文件是springboot运行时&#xff0c;会查询这个文件&#xff0c;然后把里面配置的…

SpringBoot配置文件日志

目录 一、SpringBoot配置文件的作用 二、SpringBoot配置文件的分类 1、application.properties 2、application.yml 3、application.yaml 三、使用配置文件实例--验证码 1、使用Kaptcha插件生成验证码 2、网页需求分析 3、前端页面 4、发送请求 5、服务器作出响应 …

AD24-铺铜使用方法说明

一、局部铺铜及网络添加 1&#xff09;按空格键进行切换 2&#xff09;按Backspac进行撤回 3&#xff09;铜皮网络添加 再点击铜皮选中的区域&#xff0c;即可完成网络添加 4&#xff09;完成铺铜 5&#xff09;出现红色框情况处理 按以下进行设置 重新铺铜即可 法二&#xff1…

Filezilla 银河麒麟桌面操作系统V10(sp1)与Windows主机数据传输问题

银河麒麟桌面操作系统V10&#xff08;sp1&#xff09;与Windows主机数据传输问题 1. 关闭Windows主机的防火墙和KylinOS V10的防火墙 如果不知道怎么关闭的参考这两篇文章&#xff1a; https://blog.csdn.net/m0_70885101/article/details/127271517 https://blog.csdn.net/w…

三防平板丨平板终端丨三防平板电脑丨建筑工地应用

随着建筑工程越来越复杂和规模越来越大&#xff0c;工地管理和协调变得越来越复杂。在这个过程中&#xff0c;工业设备的作用越来越重要&#xff0c;而三防平板作为一种实用的工业设备&#xff0c;在工地上的应用越来越广泛。本文将介绍三防平板在工地使用中的优势和应用。 一…

typescript 泛型详解

typescript 泛型 泛型是可以在保证类型安全前提下&#xff0c;让函数等与多种类型一起工作&#xff0c;从而实现复用&#xff0c;常用于: 函数、接口、class中。 需求:创建一个id 函数&#xff0c;传入什么数据就返回该数据本身(也就是说&#xff0c;参数和返回值类型相同)。 …

DP读书:社区文档(小白向)解读——iSulad 轻量级容器引擎功能介绍以及代码架构解析

10min带你快速了解iSulad 容器技术方案 功能介绍以及代码架构解析iSulad是啥iSulad怎么用:先看大佬咋说——maintainer李峰iSulad 轻量级容器引擎功能介绍以及代码架构解析iSulad提问iSulad_SIGiSulad的仓库&#xff1a; 主仓库嘿嘿 仓库链接&#xff1a;[https://gitee.com/op…

Java实现假日旅社管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统介绍2.2 QA 问答 三、系统展示四、核心代码4.1 查询民宿4.2 新增民宿评论4.3 查询民宿新闻4.4 新建民宿预订单4.5 查询我的民宿预订单 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的假日旅社…

JavaWeb学习|JSON与AJAX

学习材料声明 所有知识点都来自互联网&#xff0c;进行总结和梳理&#xff0c;侵权必删。 引用来源&#xff1a;尚硅谷最新版JavaWeb全套教程,java web零基础入门完整版 JSON JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机…

两分钟秒懂Android控件设置

导言 本文从整体到局部,从常用布局(layout)到控件的设置,层层递进,一定要从前往后依次看 目录 1.常用布局方式 1.1 ConstraintLayout 约束布局 1.2 LinearLayout 线性布局 1.3 TableLayout 表格布局 1.4 GridLayout 瀑布布局 2.关于控件属性整合 2.1 放置位置 2.2 如…

cubeIDE之串口空闲中断注意事项

1&#xff1a;在配置完成后的main函数中不可使能接收中断 // __HAL_UART_ENABLE_IT(&huart4, UART_IT_RXNE);//接收中断__HAL_UART_ENABLE_IT(&huart4, UART_IT_IDLE);//空闲中断HAL_UART_Receive_DMA(&huart4,rx4_buffer, 255);//使能MDA接收HAL_UART_Transmit(&…

Linux系统中HTTP隧道的搭建与配置步骤:穿越网络的“魔法隧道”

在Linux系统中搭建HTTP隧道&#xff0c;就像是开启了一条穿越网络的“魔法隧道”。这条隧道能让你的数据在网络中自由穿梭&#xff0c;无论是远程办公还是数据同步&#xff0c;都能变得轻松自在。下面&#xff0c;就让我们一起探索如何搭建这条神奇的“魔法隧道”吧&#xff01…

2024-2-20-IO进程线程作业

1> 源代码&#xff1a; #include <myhead.h>int main(int argc, const char *argv[]) {pid_t pid -1;FILE *src NULL;FILE *dest NULL;if ((src fopen("./base.txt", "r")) NULL){perror("fopen error");return -1;}fseek(src, …

阿赵UE学习笔记——15、灯光的移动性概念和构建光照信息

阿赵UE学习笔记目录   大家好&#xff0c;我是阿赵。   继续学习虚幻引擎&#xff0c;这次来学习一下UE里面灯光的移动性概念和构建光照信息。 1、灯光移动性 打开一个带有灯光的场景 在大纲面板里面找到其中一个灯光&#xff1a; 会发现灯光的细节面板里面&#xff0c;…

Windows 自带的 Linux 子系统(WSL)安装与使用

WSL官网安装教程&#xff1a; https://learn.microsoft.com/zh-cn/windows/wsl/install Windows 自带的Linux子系统&#xff0c;比用VM什么的香太多了。可以自己看官方教程&#xff0c;也可以以下步骤完成。 如果中间遇到我没遇到的问题百度&#xff0c;可以在评论区评论&#…

【Python】【VS Code】VS Code中python.json和setting.json文件配置说明

目录 1. python.json配置 2. setting.json配置 3. 解决中文乱码 4. 实现效果 1. python.json配置 python.json 获取步骤&#xff1a;文件 -> 首选项 -> 配置用户代码片段 -> python 此为VS Code的头文件设置&#xff0c;复制以下内容到 python.json {"HEADER…

【字符串题目讲解】一文理解 Manacher Algoirth(马拉车算法)——以洛谷 P3805 和 P5446 为例

M a n a c h e r A l g o r i t h m \mathrm{Manacher\ Algorithm} Manacher Algorithm Manacher 算法主要是解决怎样的问题呢&#xff0c;其实是求解最长的回文串&#xff0c;但是只能找到长度为奇数的回文串&#xff0c;不过可以通过转化使得能够求解任意长度的回文串。 例…

【9】知识存储

一、图数据库neo4j Neo4j是一个高性能的,NOSQL图形数据库&#xff0c;它将结构化数据存储在网络上而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事务特性的Java持久化引擎。单节点的服务器可承载上亿级的节点和关系&#xff0c;单节点性能不够时也可进行分布式集群部…

基于bloomz-7b指令微调的中文医疗问诊大模型,实现智能问诊、医疗问答

基于bloomz-7b指令微调的中文医疗问诊大模型&#xff0c;实现智能问诊、医疗问答 码源见文末 1.项目简介 本项目开源了基于医疗指令微调的中文医疗问诊模型&#xff1a;明医 (MING)。目前模型的主要功能如下&#xff1a; 医疗问答&#xff1a;对医疗问题进行解答&#xff0…