当前位置: 首页>资讯 >

当前视讯!rust 使用第三方库构建mini命令行工具

来源: 博客园 | 时间: 2023-06-18 05:53:35 |

这是上一篇 rust 学习 - 构建 mini 命令行工具的续作,扩展增加一些 crate 库。这些基础库在以后的编程工作中会常用到,他们作为基架存在于项目中,解决项目中的某个问题。

项目示例还是以上一篇的工程为基础做调整修改ifun-grep 仓库地址

怎么去使用已发布的 crate 库

在开发ifun-grep项目时,运行项目命令为cargo run -- hboot hello.txt,测试项目的逻辑正确。在发布到crates.io要如何使用呢,


(资料图)

在项目中使用

作为项目的一个功能函数,逻辑事务调用。在crates.io 中找到需要的库

安装已经发布的示例库ifun-grep. 通过cargo add添加依赖项

这里我们有一个测试示例项目rust-web,这是在另一篇rust 基础中创建的示例项目。

$> cargo add ifun-grep

安装成功后,可以在在项目的Cargo.toml看到依赖

[dependencies]ifun-grep = "0.1.0"

main.rs导入库使用,这个库包括了一个结构体Config,三个方法find\find_insensitive\run

use ifun_grep;fn main(){    let search = String::from("let");    let config = ifun_grep::Config {        search,        file_path: String::from("hello.txt"),        ignore_case: true,    };    let result = ifun_grep::run(config);    println!("{}", result.is_ok());}

执行cargo run,可以看到输出了false。因为文件hello.txt不存在,在上一篇文中我们把错误处理统一放到了main.rs文件中处理的。而我们这边作为一个 lib 库,直接调用的run函数,所以这边没有任何的错误输出,只提示我们没有执行成功。

我们可以在项目目录下新建一个测试文件hello.txt

Let life be beautiful like summer flowers.The world has kissed my soul with its pain.Eyes are raining for her.you also miss the stars.

再次运行,可以看到打印的输出内容。Let life be beautiful like summer flowers.

可以通过cargo remove ifun-grepCargo.toml移除依赖

作为脚本命令执行

可以看到作为功能性函数调用时,只能手动去初始化函数调用。不能像执行命令一样,传递参数调用,也就不能执行main.rs中的处理逻辑以及错误打印。

通过cargo install安装二进制可执行文件的库

$> cargo install ifun-grep

安装完成后,就可以在全局环境中使用命令ifun-grep了。

通过cargo uninstall ifun-grep移除。

开发时如何测试使用

开发时只能cargo run去执行main.rs文件,不能直接使用ifun-grep命令

可以通过cargo build构建编译,在target/debug下生成二进制文件

这样可以通过相对目录地址访问可执行文件执行命令

$> target/debug/ifun-grep Let hello.txt

如果我们的代码 存储在 github 或者 gitee 上,就可以将编译包压缩发布版本,这样需要的人不需要 cargo 就可以下载安装。

构建发布版本

$> cargo build --release

我的代码仓库在 giteeifun-grep 基础版本发布

下载压缩包后,需要把可执行文件配置到系统环境中,全局可用。也可以不用配置,直接使用文件路径地址执行命令。

还需要考虑一个问题,就是系统的兼容性,mac、windows、linux 等等,想要发布一个兼容的库,可能还需要针对性构建编译包并发布

这里演示的是 mac 系统下载发布包后,通过路径访问执行命令

clap库解析 cli 参数

clap库包含了对子命令、shell 完成和更好的帮助信息。

安装,参数--features表示启动特定功能,

$> cargo add clap --features derive

clap除了提供基础的功能之外,还可以通过--features开启特定功能。derive启动自定义派生功能,可以通过过程宏处理参数。

src/main.rs中使用

// use std::{env, process};use clap::Parser;use ifun_grep::{Config};fn main() {    // let args: Vec = env::args().collect();    let config = Config::parse();    // let config = Config::build(&args).unwrap_or_else(|err| {    //     // println!("error occurred parseing args:{err}");    //     eprintln!("error occurred parseing args:{err}");    //     process::exit(1);    // });}

结构体 Config派生了一个内部函数parse,可以直接解析参数生成实例 config

还需要修改src/lib.rs,使得结构体 Config用拥有这种能力

use clap::Parser;#[derive(Parser, Debug)]pub struct Config {    #[arg(long)]    pub search: String,    #[arg(long)]    pub file_path: String,    #[arg(long)]    pub ignore_case: bool,}

首先不再使用std::env去解析 cli 参数,也不需要调用Config的 build 方法去实例化创建 config。

通过clap::Parser的过程式宏 Parser去解析 cli 参数,并返回结构体Config的实例 config

执行命令

$> cargo run

报错了,如图,首先这个错误信息很友好,告诉我们必填的参数信息

增加参数配置,调用命令执行

$> cargo run --  --search Let --file-path hello.txt

可以看到结果成功了,对比之前调用方式cargo run -- Let hello.txt,多了一个参数名称定义--search

#[arg(long)]参数宏是用来定义参数接受的标志,arg还有许多其他的功能

移除掉#[arg(long)],执行命令 cargo run

use clap::Parser;#[derive(Parser, Debug)]pub struct Config {    pub search: String,    pub file_path: String,    pub ignore_case: bool,}

报错了,thread "main" panicked at "Argument "ignore_case is positional, it must take a value,意思是 ignore_case 必须要有一个值,ignore_case是一个布尔值,隐式启动了#[arg(action = ArgAction::SetTrue)],所以需要设置接受标志

布尔值只需要通过设置标志,而不需要设置值,--ignore_case就表示 true

use clap::Parser;pub struct Config {    pub search: String,    pub file_path: String,    #[arg(short, long)]    pub ignore_case: bool,}

再次执行命令cargo run,

还需要必填的两个参数,此时不需要--

$> cargo run --  Let hello.txt

要想开启大小写不敏感,则需要增加--ignore-case

$> cargo run --  let hello.txt --ignore-case

需要注意的是结构体定义的下划线ignore_case,在 clap 接受参数的标志为--ignore-case

增加命令的描述信息

通常 cli 的命令都有一个--help功能,这可以基本说明这个脚本是干嘛的,以及怎么去使用

而这些 clap 正好有。测试一下,代码修改后需执行cargo build

$> target/debug/ifun-grep --help

可以看到对于ifun-grep一个基本的使用方式,包括Usage、Arguments、Options。还展示了对于结构体Config的注释说明、例子。通过简写的-h可以让描述更加紧凑一点。

clap 通过#[command()]可以从Cargo.toml获取到一些基础信息,生成 Command 实例

#[derive(Parser)]#[command(author, version, about, long_about = None)]pub struct Config {    pub search: String,    pub file_path: String,    #[arg(short, long)]    pub ignore_case: bool,}

编译后,执行--help

也可以自定义这些字段的值。

#[derive(Parser)]#[command(name = "ifun-grep")]#[command(author = "hboot ")]#[command(version = "0.2.0")]#[command(about="A simple fake grep",long_about=None)]pub struct Config {    pub search: String,    pub file_path: String,    #[arg(short, long)]    pub ignore_case: bool,}

通过执行ifun-grep -V可以查看设置的name、version信息

定义参数非必须

通过Option定义字段数据类型,使得这个字段非必须

#[derive(Parser)]pub struct Config {    name: Option,}

通过--help查看参数是,必须的Arguments:参数是使用尖括号的;而非必须的是中括号[name].

如果某个参数可以接受多个,则通过集合定义类型

#[derive(Parser)]pub struct Config {    name: Vec,}

在命令执行多余的参数会解析到字段 name 中。隐式的启动了#[arg(action = ArgAction::Set)],处理多个值。

使用标志命名参数

在之前的实例中,已经使用了#[arg(short, long)],它用来标识参数名称,它可以:

  • 意图表达更明确
  • 不用在意参数的顺序
  • 使参数变的可选
#[derive(Parser)]pub struct Config {    #[arg(short, long)]    pub search: String,    #[arg(short, long)]    pub file_path: String,    #[arg(short, long)]    pub ignore_case: bool,}

可以通过--help查看变化,所有的参数都变成了Options

子命令

在执行ifun-grep时,携带子命令执行。通过#[derive(Subcommand)]标志属性,子命令也可以有自己的版本、作者信息、参数等等

use clap::{Args, Parser, Subcommand};#[derive(Parser)]pub struct Config {    #[arg(short, long)]    pub search: String,    #[arg(short, long)]    pub file_path: String,    #[arg(short, long)]    pub ignore_case: bool,    #[command(subcommand)]    pub command: Commands,}#[derive(Subcommand)]pub enum Commands {    Add(AddArgs),}#[derive(Args)]pub struct AddArgs {    name: Option,}

默认值

可以通过#[arg(default_value_t)]定义默认值,定义字段file_path默认值

#[derive(Parser)]pub struct Config {    #[arg(short, long)]    pub search: String,    #[arg(short, long, default_value = "hello.txt")]    pub file_path: String,    #[arg(short, long)]    pub ignore_case: bool,}

调用命令执行时,可以不在设置该字段

$> target/debug/ifun-grep -s Let

命令执行是查询成功的.

其他的功能比如:数据校验、自定义值解析逻辑、自定义校验等等。

anyhow处理错误

提供了一种错误类型anyhow::Error. 处理出现的错误。

之前处理文件读取的逻辑,使用了?

pub fn run(config: Config) -> Result<(), Box> {    let content = fs::read_to_string(config.file_path)?;    Ok(())}

当文件不存在时,会打印出错误信息something error:No such file or directory (os error 2).但不知道具体哪个文件不存在。

可以通过自定义错误类型IfunError,来构建自己的错误信息

#[derive(Debug)]pub struct IfunError(String);pub fn run(config: Config) -> Result<(), IfunError> {    let file_path = config.file_path.clone();    let content = fs::read_to_string(config.file_path)        .map_err(|err| IfunError(format!("could not read file {} - {}", file_path, err)))?;    // ...    Ok(())}

再次执行访问不存在的文件,报错信息为something error:IfunError("could not read file 1.txt - No such file or directory (os error 2)")

anyhow正好做了事情,可以通过 anyhow 的特征context可以附加错误内容信息,也保留了原始错误。

安装 crate anyhow

$> cargo add anyhow

调整处理读取文件的函数run,在src/lib.rs文件中修改:

use anyhow::{Context, Result};pub fn run(config: Config) -> Result<()> {    let file_path = config.file_path.clone();    let content = fs::read_to_string(config.file_path)        .with_context(|| format!("could not read file {}", file_path))?;    // ...    Ok(())}

还需要修改src/main.rs文件,将错误输出方式改为{:?}

fn main() {    // ...    if let Err(e) = run(config) {        // println!("something error:{e}");        eprintln!("something error:{:?}", e);        process::exit(1);    }}

再次执行命令,可以看到错误更加的友好。

使用anyhow!()宏,输出错误信息

src/main.rs,读取文件之前增加错误输出

fn main(){    // ...    println!("{}", anyhow!("anyhow error {}", "running"));    //...}

使用bail!()宏,中断执行

调用执行返回错误,中断程序执行

src/main.rs,读取文件之前增加错误输出

use anyhow::{anyhow, bail};fn main() -> Result<(), anyhow::Error> {    // ...    println!("{}", anyhow!("anyhow error {}", "running"));    bail!("permission denied for accessing {}", config.file_path)    //...}

调用bail!时,返回值必须是Result<(), anyhow::Error>类型

bail!同等于return Err(anyhow!())

跟踪错误栈

打印出错误信息,我们可以知道发生了错误,想知道是哪个文件、那行代码发生的错误,则需要开启错误栈追踪。

这是一个特性功能,需要指定特性启用

$> cargo add anyhow --features backtrace

然后通过设置环境变量,

  • RUST_BACKTRACE=1panics和 error 都有错误栈输出
  • RUST_LIB_BACKTRACE=1仅打开错误输出
  • RUST_BACKTRACE=1RUST_LIB_BACKTRACE=0仅 panic 时

在执行命令时,设置环境变量,打开错误输出时的错误追踪

$> RUST_LIB_BACKTRACE=1 cargo run -- -s let -f 1.txt

thiserror自定义自己的错误类型

anyhow不同,thiserror可以用来自定义错误类型。

通过过程式宏#[derive(Error)],它是由 std::error::Error派生而来。

$> cargo add thiserror

定义一个文件不能存在的错误类型,并用于读取文件时的逻辑

use thiserror::Error;#[derive(Error, Debug)]pub enum IfunError {    #[error("the file is"t exist")]    FileNotExist(#[from] std::io::Error),}pub fn run(config: Config) -> Result<(), IfunError> {    let content = fs::read_to_string(config.file_path)?;    // ...    Ok(())}

执行命令,访问不存在的文件。错误信息输出会被自定义的类型包裹:

ansi_term更好的打印输出

ansi_term控制台上的打印输出,包括字体样式、格式化。

安装

$> cargo add ansi_term

包括对文本的字体颜色、背景色、是否加粗、是否闪烁等等。

通过ansi_term::Colour控制字体样式

我们将ifun-grep的 参数打印使用颜色标记输出

use ansi_term::Colour::{Green, Yellow};fn main(){    //...    println!(        "will search {} in {}",        Green.paint(&config.search),        Yellow.paint(&config.file_path)    );}

执行命令cargo run -- -s Let -f hello.txt

加粗bold()、加下划线underline()、背景色on()

use ansi_term::Colour::{Green, Yellow};fn main(){    //...    println!(        "will search {} in {}",        Green.bold().paint(&config.search),        Yellow.underline().paint(&config.file_path)    );}

给程序查询出的行数据加背景色、闪烁

use ansi_term::Colour::{Red, Yellow};pub fn run(config: Config) -> Result<(), anyhow::Error> {    //...    for line in result {        println!("{}", Red.on(Yellow).blink().paint(line));    }    Ok(())}

通过ansi_term::Style控制样式

Colour是一个枚举类型,专门针对颜色样式处理;Style是结构体类型,是字体样式的集合。

设置字体颜色,结构体需要实例化一个实例对象,然后再调用对应的方法。

use ansi_term::Colour::{Green, Yellow};use ansi_term::Style;fn main(){    //...    println!(        "will search {} in {}",        Green.bold().paint(&config.search),        // Yellow.underline().paint(&config.file_path)        Style::new().fg(Yellow).paint(&config.file_path)    );}

颜色扩展ansi_term::Colour::Fixed

除了内置枚举的颜色,还可以通过色码值设置颜色。0-255

use ansi_term::Colour::Fixed;Fixed(154).paint("other color");

也可以通过ansi_term::Colour::RGB,设置三个不同的值

use ansi_term::Colour::RGB;RGB(154, 56, 178).paint("other color");

此外还有内置ANSIStrings类型,可以通过to_string()方法转换为String;

支持格式化输出\[u8]字节字符串,对于不知道编码的文本输出很有用。会生成ANSIByteString类型,通过write_to方法写入输出流中。

Green.paint("ansi_term".as_bytes()).write_to(&mut std::io::stdout()).unwrap();

indicatif展示进度条

处理任务时,显示任务的执行进度。会让人感觉良好,更有耐心等待执行完毕

$> cargo add indicatif

手动创建一个进度条,为了看到进度条的进度效果,可以使用std::thred线程休眠一段时间。

use indicatif::ProgressBar;use std::{thread, time};fn main(){    let bar = ProgressBar::new(100);    let ten_millis = time::Duration::from_millis(10);    for _ in 0..100 {        bar.inc(1);        thread::sleep(ten_millis);        // ...    }    bar.finish();}

通过ProgressBar类型创建了一个进度条的实例对象,然后通过实例bar.inc()逐步增加进度。完成后调用bar.finish()表示进度完成,并保留显示进度信息。

也支持多进度条的MultiProgress

log日志记录

一个程序运行时期的日志打印,非常重要,这对于运行监测喝解决有问题都有很到的帮助。

$> cargo add log

通常可以将日志按照登记划分,比如错误、警告、信息等。还需要一个日志输出的适配器 env_logger,可以将日志写入终端、日志服务器等。

$> cargo add env_logger

美化输出,将接受到的参数作为信息info!()输出,将产生的错误使用error!()输出

env_logger默认输出日志到终端,

use log;fn main() {    env_logger::init();    // ...    log::info!(        "will search {} in {}",        Green.bold().paint(&config.search),        Style::new().fg(Yellow).paint(&config.file_path)    );    // ...    if let Err(e) = run(config) {        log::error!("something error:{:?}", e);        process::exit(1);    }}

必须在程序之前初始化完毕日志环境变量配置。默认只展示error错误类型的日志

执行cargo run -- -s Let -f 1.txt命令访问不存在的文件,可以看到只有 error 错误输出打印。

通过设置变量RUST_LOG=info,查看

$> RUST_LOG=info cargo run -- -s Let -f 1.txt

初始化指定信息类型

在执行命令前加上RUST_LOG=info很麻烦,有遗忘的可能,可以通过初始化env_logger::init()调用时,设定一个默认值

use env_logger::Env;fn mian(){    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();    // ...}

通过终端设置的变量优先级比默认值高,可以通过执行时设置变量覆盖默认值。

自定义输出模板

可以看到默认的输出打印包括了时间、类型以及模块名。可以通过改变模板自定义输出格式

use std::io::Write;fn main(){    env_logger::builder()        .format(|buf, record| writeln!(buf, "{} - {}", record.level(), record.args()))        .init();}

输出格式改变为信息类型 - 信息。使用默认的挺好,现在好多编辑器的日志输出都是这种格式。

测试

之前的单元测试示例都是和逻辑代码放在一起的,并用#[test]注释。可以将这些测试放在tests目录中

新建tests/lib.rs用于存放单元测试用例。

use ifun_grep::{find, find_insensitive};#[test]fn case_sensitive() {    let search = "rust";    let content = "\nice. rustI"m hboot.hello world.Rust";    assert_eq!(vec!["nice. rust"], find(search, content));}#[test]fn case_insensitive() {    let search = "rust";    let content = "\nice. rustI"m hboot.hello world.Rust";    assert_eq!(        vec!["nice. rust", "Rust"],        find_insensitive(search, content)    );}

通过借助第三饭库来使得测试更容易,

assert_cmd

可以处理结果进行断言;也可以测试调用命令进行测试。一起配合使用的还有predicates用来断言布尔值类型结果值

因为测试示例只在开发阶段需要,则在安装时加参数--dev

$> cargo add assert_cmd predicates --dev

新增一个处理文件不存在的的测试示例。日志打印输出时会包含有could not read file字符串。

use assert_cmd::prelude::*;use ifun_grep::{find, find_insensitive};use predicates::prelude::*;use std::{error::Error, process::Command};#[test]fn file_doesnt_exist() -> Result<(), Box> {    let mut cmd = Command::cargo_bin("ifun-grep")?;    cmd.arg("-s let").arg("-f 1.txt");    cmd.assert()        .failure()        .stderr(predicate::str::contains("could not read file"));    Ok(())}

通过运行cargo test,测试示例是运行成功的。

assert_fs用于测试文件系统的断言

刚才测试了文件不存在的错误输出,还需要增加文件存在的测试,并写入内容。

$> cargo add assert_fs --dev

生成要测试的文件;断言测试生成的文件。tests/lib.rs增加测试用例

use assert_cmd::prelude::*;use assert_fs::prelude::*;use ifun_grep::{find, find_insensitive};use predicates::prelude::*;use std::{error::Error, process::Command};#[test]fn file_content_exist() -> Result<(), Box> {    let file = assert_fs::NamedTempFile::new("1.txt")?;    file.write_str("hello world \n Rust-web \n good luck for you!")?;    let mut cmd = Command::cargo_bin("ifun-grep")?;    cmd.arg("-s good").arg("-f").arg(file.path());    cmd.assert()        .success()        .stderr(predicate::str::contains("good luck for you!"));    Ok(())}

这样书写的单元测试用例更能直接、明了。和实际使用ifun-grep时同样的命令操作,而不是使用开发时运行cargo run

关键词:

 

热文推荐

当前视讯!rust 使用第三方库构建mini命令行工具

这是上一篇[rust学习-构建mini命令行工具](https: www cnblogs com dr

2023-06-18

德赫亚或在今夏离队 曼联盯上米兰双雄主力门将

北京时间6月17日消息,据英国媒体《镜报》报道,西班牙门将德赫亚将于

2023-06-18

全球资讯:柄集乡_关于柄集乡概略

1、邴集乡地处界首市北部,距城区20公里,是全市两个“双培双带”试点

2023-06-18

0726是哪里的区号查询 0726是哪里的区号 观天下

1、目前国内、国际长途电话都没有0726这样的区号。2、如果您真的接到显

2023-06-18

徕卡M11是我永远不会买的最漂亮的相机_当前热议

徕卡M11是这家标志性摄影公司最新推出的全画幅数字旁轴相机,将60兆像

2023-06-17

2023款奔驰C级PHEV解析,不光颜值高,行驶品质同样出色 今日精选

作为比较热门的豪华品牌,奔驰在燃油车市场的实力毋庸置疑,近些年来,

2023-06-17

全球热点评!《鲛人》作者:jeans 鲛人之禁脔

1、资源已经上传如果有什么问题的话1追问我2百度Hi我-----满意请采纳为

2023-06-17

定点生产_关于定点生产介绍

1、定点生产是指生产的主管部门同有关部门协商。2、指定某一个或若干个

2023-06-17

怎么刷recovery_怎么刷recovery|资讯

恢复笔刷教程,恢复模式笔刷你知道恢复模式刷机的步骤吗?其实方法很简

2023-06-17

阳谷二中李长兴(阳谷二中)

1、老大您好,谷山中学是中学。2、阳谷2中是个高中不过距离不远他们都

2023-06-17

固定式消防炮的定义及作用_固定消防炮和高空消防炮区别在哪儿相关介绍简介

可以了解GB50338-2003《固定消防炮灭火系统设计规范》和《大空间智能型

2023-06-17

汪氏宗亲网 汪氏宗亲网 安定胡进

一、“汪氏宗亲网”简介“汪氏宗亲网”是一个以汪氏家族为主体的全球宗

2023-06-17

时隔5年,陈雨菲决赛再战马林!国羽决赛已有2席,雅思组合遇东渡

雅思组合赢国羽德比,赛季4进决赛雅思组合决赛对手是渡边勇大 东野有纱

2023-06-17

三星 Galaxy Tab S9 系列平板海报曝光

IT之家6月17日消息,国外科技媒体TheTechOutlook在最新报道中,分享了

2023-06-17

全球动态:大趋势中抓住创业机会?网易棋牌代理热招中

伴随着游戏市场的快速发展,游戏代理逐渐成为创业的风口项目。但是,要

2023-06-17

热身赛-德国狂轰26脚0-1爆冷不敌波兰 遭遇三场不胜_全球热推荐

德国0-1不敌波兰热身赛三场不胜

2023-06-17

房地产又香了?防水建材龙头进京“抢地”

尽管东方雨虹对外宣称“不涉及房地产开发业务”,但这家“防水一哥”却

2023-06-17

智己LS6申报信息曝光,定位中型SUV,年内上市,对标蔚来ES6?-环球新要闻

日前,智己LS6(图片)的申报信息在网络上曝光,新车是智己品牌的第三款

2023-06-17

天蝎座和水瓶座合不合适谈恋爱(天蝎座和水瓶座合不合) 世界快资讯

1、就太阳主星所在是最不合的就个人经验分析不合处:1,天蝎的自以为是

2023-06-17

焦点滚动:​奥秘“趣无限” 沌口幼儿园开展科学小实验活动

​奥秘“趣无限”沌口幼儿园开展科学小实验活动---为进一步培养孩子们

2023-06-17