rust学习 - 构建mini 命令行工具

这篇具有很好参考价值的文章主要介绍了rust学习 - 构建mini 命令行工具。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为构建命令行程序的最佳选择。

实现一个命令行搜索工具grep,可以在指定文件中搜索指定的字符串。想实现这个功能呢,可以按照以下逻辑流程处理:

  1. 获取输入文件路径、需要搜索的字符串
  2. 读取文件;
  3. 在文件内容中查找字符串所在的行
  4. 打印包含字符串所在的行信息

创建项目ifun-grep

$> cargo new ifun-grep

项目在运行时,可以获取到传递的参数。比如cargo run -- hboot hello.txt,在文件hello.txt查找字符串hboot

读取参数

首先要先获取到传入的参数。通过标准库std::env::args获取

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    dbg!(args);
}

collect()方法可以将传入的参数转换为一个集合。对于变量args必须注明集合类型。

rust学习 -  构建mini 命令行工具

参数的第一个值是二进制文件的名称。可以用于程序调试或者打印出文件路径,取出另外两个参数,保存进对应的变量。方便后续传参数使用。

let search = &args[1];
let file_path = &args[2];

println!("will search {} in {}", search, file_path)

读取文件

首先创建测试文件hello.txt,并写入一段文字。

独立寒秋,湘江北去,橘子洲头。

看万山红遍,层林尽染;漫江碧透,百舸争流。

鹰击长空,鱼翔浅底,万类霜天竞自由。

怅寥廓,问苍茫大地,谁主沉浮?

携来百侣曾游,忆往昔峥嵘岁月稠。

恰同学少年,风华正茂;书生意气,挥斥方遒。

指点江山,激扬文字,粪土当年万户侯。

曾记否,到中流击水,浪遏飞舟

读取文件,并打印出文件中的内容。

let content = fs::read_to_string(file_path).expect("you should permission to read the file");

println!("read the content:\n{content}")

通过fs模块的read_to_string方法读取文件内容。expect则用于处理读取文件时发生的错误的提示信息,这在下面的错误处理会有说明。

模块拆分与错误处理

现在所有的处理业务都放在src/main.rs中。取参和读取文件是两个不同功能的逻辑处理,当功能越来越复杂的时候,就应该关注分离。这在我们设计时可提前考虑好

main.rs只被用来处理程序的执行。其他需要处理的逻辑则可以放在srr/lib.rs中。

定义一个解析取参的函数parse_args,现在仍然定义在src/main.rs中。

fn parse_args(args: &Vec<String>) -> (&str, &str) {
    let search = &args[1];
    let file_path = &args[2];

    (search, file_path)
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let (search, file_path) = parse_args(&args);

    println!("will search {} in {}", search, file_path);
}

这样main函数不再处理哪个参数对应哪个变量。

我们可以将这一组相关的变量通过结构体定义相互关联起来。这样函数返回将不再使用元组,并且可以通过结构体实例可以访问到每一个属性。

struct Config {
    search: String,
    file_path: String,
}

fn parse_args(args: &Vec<String>) -> Config {
    let search = args[1].clone();
    let file_path = args[2].clone();

    Config { search, file_path }
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let config = parse_args(&args);

    println!("will search {} in {}", search, file_path);
}

在结构体中,实例化赋值需要拥有这些变量值的所有权。而变量args是所有权的拥有者,通过clone()方法拷贝一份数据。

可以看到parse_args返回来一个结构体 Config 的实例,可以通过定义结构体的内部方法来创建实例。

impl Config {
    fn new(args: &Vec<String>) -> Self {
        let search = args[1].clone();
        let file_path = args[2].clone();

        Config { search, file_path }
    }
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("will search {} in {}", search, file_path);
}

这样,就不需要parse_args函数了,通过结构体的内部方法实例化实例。

错误处理

如果我们执行cargo run时,不传递任何参数,则程序会报错。这样的提示对于用户并不友好。

首先可以通过判断参数需要的参数信息,说明错误信息。

impl Config {
    fn new(args: &Vec<String>) -> Self {
        if args.len() < 3 {
            panic!("至少传入2个参数")
        }
        // ...
    }
}

提示用户必须传入 2 个从参数,因为有一个默认的路径参数。所以判断不能少于3

除了直接提示错误信息并中断程序,也可以使用Result传递错误,让主函数做决定如何去处理。

impl Config {
    fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少传入2个参数");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { search, file_path })
    }
}

现在提供了一个方法build来处理这个逻辑,之前的new不用了(这里是语义话定义,new常常表示不会产生错误),当有错误时,不是直接终止程序,而是返回一个Err值。

src/main.rs中调用并处理结果。对于错误信息给用户输出有好的提示信息,并以非零错误process::exit(1)退出命令行。

use std::{env, fs, process};

fn main(){
    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("error occurred parseing args:{err}");
        process::exit(1);
    });

    // ...
}

unwrap_or_else可以进行自定义错误处理。这是一个闭包,它调用内部的匿名函数,并通过|err|传递的参数供内部使用。当返回Ok时,则返回内部的值。

提取读取文件的逻辑

参数的取参逻辑经由结构体内部方法处理。现在吧文件读取的逻辑提取出来,并采用传递错误的方式Result返回错误信息。

use std::error::Error;

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

    println!("read the content:\n{content}");

    Ok(())
}

使用了 trait 对象 Box<dyn Error>返回实现Errortrait 的类型,不用指定具体的错误类型。灵活性更高dyn表示动态的

接着可以在主函数中调用run()函数,并处理可能出现的错误。

fn main (){
    // ...

    if let Err(e) = run(config) {
        println!("something error:{e}");
        process::exit(1);
    }
}

拆分代码到库

以上定义了结构体,处理取参函数;拆离了读取文件逻辑。但是这些都是在src/main.rs中,有复杂逻辑时,这会让文件行数很多,看起来很让人头疼。

将这一部分拆离的放到其他文件中去。新建src/lib.rs,将这些定义移动到该文件中。

use std::error::Error;
use std::fs;

pub struct Config {
    pub search: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少传入2个参数");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { search, file_path })
    }
}

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

    println!("read the content:\n{content}");

    Ok(())
}

可以看到通过pub将这些结构体、函数都公有化。包括结构里的字段,这就是一个可以测试的公有 API 的 crate 库。

然后再src/main.rs需要导入

use ifun_grep::{run, Config};

通过use引入作用域。ifun-grep是项目名称,作为前缀。

增加测试

通过测试驱动开发的模式来逐渐增加逻辑。期望从给定的内容中查找出字符串,并打印出所在行。

src/lib.rs增加测试示例

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn on_result() {
        let search = "hboot";
        let content = "\
nice. rust
I'm hboot.
hello world.
";

        assert_eq!(vec!["I'm hboot."], find(search, content));
    }
}

搜索字符串hboot,它在文本的第二行。所以期待搜索输出结果为I'm hboot.

提供一个find函数,用于处理搜索逻辑,先不写搜索逻辑,返回一个空的结果值。

pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    vec![]
}

利用显示生命周期'a来表明参数content参数与返回值的生命周期相关联。它们存在的时间一样久

执行测试cargo test,理所应当的输出失败,结果返回了一个空的vec![],和预期不匹配。

增加搜索逻辑,按行执行过滤,包含指定的字符串,则存储在结果中。

pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    let mut result = vec![];
    for line in content.lines() {
        if line.contains(search) {
            // 符合,包含了指定字符串
            result.push(line);
        }
    }

    result
}

通过迭代器遍历给定文本内容lines().字符串判断是否包含contains()方法。将结果值放进result中,并返回。

测试用例测试没有问题,完善一下run函数,搜索出符合的内容并打印出来

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

    // println!("read the content:\n{content}");
    for line in find(&config.search, &content) {
        println!("{line}");
    }

    Ok(())
}

执行脚本cargo run -- 山 hello.txt,可以看到打印输出两行

rust学习 -  构建mini 命令行工具

增加环境变量

功能已经到到预期,可以搜索出想要包含字符串的文本段落。增加一个额外的功能大小写敏感处理环境变量,当然也可以通过再多传一个参数处理。

更改文本内容为应为

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.

先测试当前程序是否大小写敏感,文本中首个英文单词是大写的,按照小写搜索

$> cargo run -- let hello.txt

没有任何的打印输出,说明当前的搜索逻辑是大小写敏感的,通过传递变量来控制逻辑,修改测试用例,增加两个测试示例:大小写敏感和不敏感测试。

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn case_sensitive() {
        let search = "rust";
        let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";

        assert_eq!(vec!["nice. rust"], find(search, content));
    }

    #[test]
    fn case_insensitive() {
        let search = "rust";
        let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";

        assert_eq!(vec!["nice. rust", "Rust"], find_insensitive(search, content));
    }
}

原来的函数find大小写敏感,逻辑不变。增加一个大小写不敏感的函数find_insensitive,在处理搜索时,查询的字符和被搜索的文本行都转小写后,然后在执行查找。

pub fn find_insensitive<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    let mut result = vec![];
    // 搜索 字符串转小写
    let search = search.to_lowercase();

    for line in content.lines() {
        // 文本行内容转小写
        if line.to_lowercase().contains(&search) {
            // 符合,包含了指定字符串
            result.push(line);
        }
    }

    result
}

多了一个操作to_lowercase()将文本内容转成小写。to_lowercase()会新创建一个 String,contains()方法参数需要的是一个引用。

再次执行测试cargo teset.用例全部通过。逻辑写好了,需要通过增加一个配置来处理是否大小写敏感。

修改结构体定义ingore_case表示来忽略大小写。

pub struct Config {
    pub search: String,
    pub file_path: String,
    pub ignore_case: bool,
}

通过ingore_case字段判断是否调用哪个函数,修改run函数

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

    // println!("read the content:\n{content}");
    let mut result = vec![];
    if config.ignore_case {
        result = find_insensitive(&config.search, &content)
    } else {
        result = find(&config.search, &content)
    }
    for line in result {
        println!("{line}");
    }

    Ok(())
}

处理接受变量IGNORE_CASE,通过库std::env处理环境变量。

impl Config {
    pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少传入2个参数");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            search,
            file_path,
            ignore_case,
        })
    }
}

env::var()返回值为 Result 类型,通过它自己的方法is_ok()判断什么状态,如果设置值则返回 true;未设置则返回 false。

进行测试,不设置变量时,查询小写的let是查询不到的,因为首写的因为单词字母是大些的。

$> cargo run -- let hello.txt

rust学习 -  构建mini 命令行工具

通过设置环境变量,执行程序

$> IGNORE_CASE=1 cargo run -- let hello.txt

可以查到目标文本内容。

rust学习 -  构建mini 命令行工具

错误信息处理

我们所预先知道的错误信息都通过程序执行println!打印在控制台,这是一种标准输出.

对于出现错误信息,希望它即时打印输出,而对于程序执行的结果记录下来,保存到文件中,方便查看。

现在使用println!标准输出流重定向到文件中,它会将错误信息也保存到起来,且不会打印。

$> cargo run >output.txt

屏幕上没有任何输出,以为程序执行正常,其实文件中的内容是error occurred parseing args:至少传入2个参数

这就造成了一个问题,不管成功、失败,只有打开文件才能看到。错误输出使用标准错误展示用于错误信息,将错误打印的println!改为eprintln!

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        // println!("error occurred parseing args:{err}");
        eprintln!("error occurred parseing args:{err}");
        process::exit(1);
    });
    println!("will search {} in {}", config.search, config.file_path);

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

重新执行cargo run >output.txt,错误打印到控制台,而文件output.txt没有输出。

再执行,可以查到数据的命令cargo run -- Let hello.txt > output.txt,查看output.txt,可以看到预期的查找到的内容在文件中。

发布 crate 到Crate.io

crates.io 库,可以这里找找想要的功能库,也可以将自己的 crate 发布到这里。

Rust 的发布配置都有一套默认的、可定制的配置。

  • cargo build 采用的是 dev 配置构建程序
  • cargo build --release 是 release 配置,有更好的发布构建的配置

可以在文件Cargo.toml中通过[profile.*]修改设置默认值。

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

dev 构建和发布构建定义不同的优化等级。opt-level定义何种程度优化,0-3可配置值。dev 默认为 0,release 默认为 3.

如果想 dev 模式下需要一些优化,则可以更改为

[profile.dev]
opt-level = 1

增加文档注释

一个好的模块包,是有很好的文档说明,以方便其他人轻易上手。通过文档注释///已支持 markdown 格式化文本。

给每一个函数增加注释说明,这里只展示部分。

/// the struct `Config` defines command line params.
///
/// # Example
///
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
///     search,
///     file_path:String::from("hello.txt"),
///     ignore_case:false,
/// };
///
/// ```
pub struct Config {
    pub search: String,
    pub file_path: String,
    pub ignore_case: bool,
}

/// the fun is used to execute search
///
/// # example
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
///     search,
///     file_path:String::from("hello.txt"),
///     ignore_case:false,
/// };
///
/// let result = ifun_grep::run(config);
///
/// assert!(result.is_ok());
/// ```
///
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    let result;
    if config.ignore_case {
        result = find_insensitive(&config.search, &content);
    } else {
        result = find(&config.search, &content);
    }
    for line in result {
        println!("{line}");
    }

    Ok(())
}

在使用 vscode 时,注释文档上方会有一个执行操作 run doctest。可以单独执行当前写的测试示例是否可以通过执行。

也可以通过cargo test来测试所有的测试示例。不仅会执行mod test的测试示例,也会执行doc test的注释测试示例。

通过命令cargo doc --open来生成在线文档。

$> cargo doc --open

rust学习 -  构建mini 命令行工具

可以通过//!对当前文件进行注释说明,必须是在第一行。

//! ifun_grep is a string search library
//!
//! Supports case sensitive search.
//!

注册 crate.io 账户并发布

目前只能使用 github 账号进行授权登录。在个人账号信息中,API Tokens生成 token 授权操作。

$> cargo login 你的token

如果登录不成功,看下提示错误,我是加了参数--registry crates-io才成功的。

$> cargo login 你的token --registry crates-io

登录之后就可以发布了,通过Cargo.toml增加一些仓库元信息,比如仓库名、作者、开源协议、描述等等。

$> cargo publish

发布之前需要验证你登录的账号邮箱,不然发布不了。个人的元信息有几项是必填的,包括name\version\description\license

发布时,如果发布不成功,看错误提示,可能还需要加--registry crates-io

撤销某个版本

如果你发布的版本有很大的问题,可以撤销改版本。不能删除仓库,已发布的代码时永久存在的,只能通过撤销来阻止其他项目引用它。

$> cargo yank --vers 0.1.0

使得当前版本不可用。也可以恢复当前版本的使用文章来源地址https://www.toymoban.com/news/detail-476059.html

$> cargo yank --vers 0.1.0 --undo

到了这里,关于rust学习 - 构建mini 命令行工具的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Docker(镜像、容器、仓库)工具安装使用命令行选项及构建、共享和运行容器化应用程序

    👧个人主页:@小沈YO. 😚小编介绍:欢迎来到我的乱七八糟小星球🌝 🔑本章内容:Docker工具安装使用、命令行选项及构建、共享和运行容器化应用程序时的主要步骤 记得 评论📝 +点赞👍 +收藏😽 +关注💞哦~ 提示:以下是本篇文章正文内容,下面案例可供参考 以下是在常

    2024年02月05日
    浏览(62)
  • 【ollama】(3):在linux搭建环境中,安装golang开发的ollama工具,并且完成启动下载gemma:7b和qwen:1.8b运行速度飞快,支持http接口和命令行模式

    https://www.bilibili.com/video/BV19F4m1F7Rn/ 【ollama】(3):在linux搭建环境中,安装ollama工具,并且完成启动下载gemma:7b和qwen:1.8b运行速度飞快,支持http接口和命令行 https://ollama.com/ 项目使用golang+llama.cpp 项目进行开发的。 简化了模型的安装,非常的方便。 然后因为是docker 虚拟环境

    2024年04月13日
    浏览(40)
  • 【跟小嘉学 Rust 编程】十二、构建一个命令行程序

    【跟小嘉学 Rust 编程】一、Rust 编程基础 【跟小嘉学 Rust 编程】二、Rust 包管理工具使用 【跟小嘉学 Rust 编程】三、Rust 的基本程序概念 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念 【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据 【跟小嘉学 Rust 编程】六、枚举

    2024年02月13日
    浏览(39)
  • Rust之构建命令行程序(三):重构改进模块化和错误处理

    Windows 10 Rust 1.74.1   VS Code 1.85.1 这次创建了新的工程minigrep. 为了改进我们的程序,我们将修复与程序结构及其处理潜在错误的方式有关的四个问题。首先,我们的 main 函数现在执行两项任务:解析参数和读取文件。随着我们程序的增长, main 处理的独立任务的数量也会增加。随

    2024年01月18日
    浏览(43)
  • 写了个辅助学习vite的小工具(mini-vite)

      话不多说先贴上仓库地址 mini-vite 封装的方法太多,不知道从哪个方法看起 随便一个文件就是一两千行代码,看得头皮发麻 不知道该怎么去debug,到底应该在哪个方法里面debug 没关系,这些问题在我这都能解决 移除了vite仓库中的所有注释和其他对于所要学习的vite功能用不

    2024年02月08日
    浏览(41)
  • 使用Rust开发命令行工具

    生成二进制文件,将其扔到环境变量的path下即可~ 用rust打造实时天气命令行工具 [1] 找到合适的API 使用 该api [2] 如请求 api.openweathermap.org/data/2.5/weather?q=Beijingappid=your_key : 初始化项目coding 使用 cargo new rust_weather 初始化一个项目。 对于 cargo.toml 文件: 对于 src/main.rs 文件: 使用

    2024年02月11日
    浏览(38)
  • 论文笔记: 深度学习速度模型构建的层次迁移学习方法 (未完)

    摘要 : 分享对论文的理解, 原文见 Jérome Simon, Gabriel Fabien-Ouellet, Erwan Gloaguen, and Ishan Khurjekar, Hierarchical transfer learning for deep learning velocity model building, Geophysics, 2003, R79–R93. 这次的层次迁移应该指从 1D 到 2D 再到 3D. 深度学习具有使用最少的资源 (这里应该是计算资源, 特别是预测

    2024年02月10日
    浏览(43)
  • rust学习-构建服务器

    服务器会依次处理每一个请求,在完成第一个连接的处理之前不会处理第二个连接 404.html thread::spawn,期望获取一些一旦创建线程就应该执行的代码 但是对于线程池不适用,线程池是当在需要时才执行代码执行 (1)定义 Worker 结构体存放 id 和 JoinHandle() (2)修改 ThreadPool 存

    2024年02月15日
    浏览(35)
  • Rust程序语言设计 第十二章 一个 I/O 项目:构建一个命令行程序

    本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。 Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,

    2024年02月13日
    浏览(38)
  • dji mini4pro 图片拷贝到电脑速度

    win电脑  amd3600 m.2固态硬盘 dp快充数据线 直接主机使用dp线连接无人机 9成是raw格式图片  一小部分是视频和全景图 TF卡信息:         闪迪 128GB 129元          闪迪 128GB TF(MicroSD) 存储卡U3 C10 V30 A2 4K 至尊超极速移动版  \\\"TF卡至尊超极速\\\"         理论读取200MB/s 理论写入90MB

    2024年02月06日
    浏览(23)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包