【跟小嘉学 Rust 编程】十二、构建一个命令行程序

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、如何接受命令行参数
    • 1.1、创建项目
    • 1.2、需求介绍
    • 1.3、读取参数值
    • 1.4、将参数值保存进变量
  • 二、读取文件(使用 fs 模块读取文件)
  • 三、模块化与错误处理
    • 3.1、代码中存在的问题
    • 3.2、二进制项目的关注分离
    • 3.3、提取参数解析器
    • 3.4、使用结构来组织配置变量
    • 3.5、创建 Config 的构造函数
    • 3.6、修复错误处理
      • 3.6.1、改善错误提示
      • 3.6.2、业务逻辑处理:run方法
      • 3.6.3、run 函数返回 Result 错误
    • 3.7、将代码拆分
      • 3.7.1、lib.rs
      • 3.7.2、main.rs
  • 四、测试驱动开发(TDD)
    • 4.1、search 方法编写和测试
    • 4.2、在 run 函数中使用 search 函数
  • 五、处理环境变量
  • 六、标准输出和标准错误
    • 6.1、标准输出:stdout
    • 6.2、标准错误:stderr
  • 总结

前言

本章是一个目前所学的很多技能的应用,以及标准库的探索,我们讲构建一个命令行程序工具来练习现在已经学习过的一些Rust的技能。我们将构建自己的版本的命令行工具:grep(Globally search a Regular Expression and print)。

主要教材参考 《The Rust Programming Language》


一、如何接受命令行参数

1.1、创建项目

$ cargo new minigrepCreated binary (application) `minigrep` project
$ cd minigrep

1.2、需求介绍

minigrep 能够接受两个命令行参数:文件名和要搜索的字符串。也就是说我们希望使用cargo run的时候,可以使用如下的方式。

cargo run searchstring example-filename.txt

在 Crates.io 上会有一些现场的库帮助我们接受命令行参数(clap)。不过我们现阶段使用标准库。

1.3、读取参数值

为了能够接受命令行参数的值,我们需要使用 rust 标准库提供的函数。该函数返回一个命令行参数的迭代器(iiterator),迭代器我们将会在下一章详细讲解。我们只需要知道在迭代器上有一个方法 collect 可以将其转换为一个集合。

use std::env;fn main() {let args: Vec<String> = env::args().collect();println!("{:?}", args);
}

需要注意 args 函数 在其任何参数包含 无效Unicode 字符时会panic。 如果你需要接受包含无效Unicode字符的参数,使用 std::env::args_os 代替。该函数返回 OsString值而不是 String 值。

Vector 的第一个参数是二进制文件的名称。

1.4、将参数值保存进变量

use std::env;fn main() {let args: Vec<String> = env::args().collect();let query = &args[1];let filename = &args[2];println!("Searching for {}", query);println!("In file {}", filename);
}

二、读取文件(使用 fs 模块读取文件)

use std::{env, fs};fn main() {let args: Vec<String> = env::args().collect();let query = &args[1];let filename = &args[2];println!("Searching for {}", query);println!("In file {}", filename);let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");println!("With text:\n{}", contents);}

fs::read_to_string(filename) 方法打开文件,返回包含内容的Result<String>

三、模块化与错误处理

我们上述代码 main 函数有着多个职责,通常函数只负责一个功能会更加简洁并且易于维护。在开发的时候重构是一个最佳时间,重构少量代码要容易的多。

3.1、代码中存在的问题

我们最初的代码存在下面四个问题:

  • 1、 main 现在进行了两个功能:解析参数并且打开文件。但是当函数承担了更多责任,会更加难易推导,难以测试,并且难以在不破坏其他部分的情况下做出修改。
  • 2、query 和 flename 是程序中过的配置i变了,而 contents 则用来执行程序逻辑。当变量越来越多的时候便会难以追踪分析每个变量的目的,最好能够讲配置变量组织进一个结构。这样就能够使他们的目的更加明确;
  • 3、如果打开文件失败 ,我们使用 expect 来打印错误信息,不过这种错误信息并不明确,读取文件失败的原因有很多种:例如文件不存在,或者没有打开文件的权限等,无论那种情况,这并没有给予使用者具体的信息
  • 4、我们不停的使用 expect 来处理不同的错误,如果用户没有指定足够的的参数来运行程序,他们会从 rust 中得到 一个 index out of bounds 错误,而这并不能明确解释问题。如果所有的错误处理都位于一处,这样将来的维护者需要在修改错误处理逻辑时只需要考虑这一处代码。

3.2、二进制项目的关注分离

main 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main 函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:

  • 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
  • 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
  • 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。

经过这些过程之后保留在 main 函数中的责任应该被限制为:

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他的配置
  • 调用 lib.rs 中的 run 函数
  • 如果 run 返回错误,则处理这个错误

这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试他们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序

3.3、提取参数解析器

use std::{env, fs};fn main() {let args: Vec<String> = env::args().collect();let (query,filename) = parse_config(&args);println!("Searching for {}", query);println!("In file {}", filename);let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");println!("With text:\n{}", contents);}fn parse_config(args: &[String]) -> (&str, &str) {let query = &args[1];let filename = &args[2];(query, filename)
}

3.4、使用结构来组织配置变量

use std::{env, fs};fn main() {let args: Vec<String> = env::args().collect();let config = parse_config(&args);println!("Searching for {}", config.query);println!("In file {}", config.filename);let contents = fs::read_to_string(config.filename).expect("Something went wrong reading the file");println!("With text:\n{}", contents);}struct Config {query: String,filename: String,
}fn parse_config(args: &[String]) -> Config {let query = args[1].clone();let filename = args[2].clone();Config { query, filename }
}

我们需要注意 我们定义的 Config 包含拥有所有权的String值,我们返回来引用 args 中的 String值的字符串切片 slice。 main函数的args变量是参数值的所有者并只允许 parse_config 方法借用他们。这意味着 Config 尝试获取args 中的值的所有权将违反 Rust的借用规则。

还有许多不同的方式可以处理 String 的数据,而最简单但有些不太高效的方式是调用这些值的 clone 方法。这会生成 Config 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。

由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 clone 来解决所有权问题。

在关于迭代器的章节中,我们将学习如何更加有效率的处理这种情况,不过现在复制字符串取得进展是没有问题的。因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。

3.5、创建 Config 的构造函数

use std::{env, fs};fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args);println!("Searching for {}", config.query);println!("In file {}", config.filename);let contents = fs::read_to_string(config.filename).expect("Something went wrong reading the file");println!("With text:\n{}", contents);}struct Config {query: String,filename: String,
}impl Config {fn new(args: &[String])-> Config{let query = args[1].clone();let filename = args[2].clone();Config { query, filename }}
} 

3.6、修复错误处理

3.6.1、改善错误提示

对于错误,我们可以使用 panic!,但是 panic!更趋向于程序上的问题,而不是使用上的问题,我们应该使用Result 枚举来处理错误。

use std::{env, fs, process};const ARGS_LENGTH:usize= 3;fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err|{println!("Problem parsing arguments:{}", err);process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.filename);let contents = fs::read_to_string(config.filename).expect("Something went wrong reading the file");println!("With text:\n{}", contents);}struct Config {query: String,filename: String,
}impl Config {fn new(args: &[String])-> Result<Config, &'static str>{if args.len() < ARGS_LENGTH  {return Err("not enough arguments")}let query = args[1].clone();let filename = args[2].clone();Ok( Config { query, filename })}
} 

3.6.2、业务逻辑处理:run方法

use std::{env, fs, process};const ARGS_LENGTH:usize= 3;fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err|{println!("Problem parsing arguments:{}", err);process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.filename);run(config);
}struct Config {query: String,filename: String,
}impl Config {fn new(args: &[String])-> Result<Config, &'static str>{if args.len() < ARGS_LENGTH  {return Err("not enough arguments")}let query = args[1].clone();let filename = args[2].clone();Ok( Config { query, filename })}
} fn run(config: Config) {let contents = fs::read_to_string(config.filename).expect("Something went wrong reading the file");println!("With text:\n{}", contents);
}

3.6.3、run 函数返回 Result 错误

use std::{env, fs, process, error::Error};const ARGS_LENGTH:usize= 3;fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err|{println!("Problem parsing arguments:{}", err);process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.filename);if let Err(e) = run(config) {println!(" Application error: {}", e);process::exit(1);}
}struct Config {query: String,filename: String,
}impl Config {fn new(args: &[String])-> Result<Config, &'static str>{if args.len() < ARGS_LENGTH  {return Err("not enough arguments")}let query = args[1].clone();let filename = args[2].clone();Ok( Config { query, filename })}
} fn run(config: Config) -> Result<(), Box<dyn Error>>{let contents = fs::read_to_string(config.filename)?;println!("With text:\n{}", contents);Ok(())
}

3.7、将代码拆分

3.7.1、lib.rs


use std::{fs, error::Error};const ARGS_LENGTH:usize= 3;pub struct Config {pub query: String,pub filename: String,
}impl Config {pub fn new(args: &[String])-> Result<Config, &'static str>{if args.len() < ARGS_LENGTH  {return Err("not enough arguments")}let query = args[1].clone();let filename = args[2].clone();Ok( Config { query, filename })}
} pub fn run(config: Config) -> Result<(), Box<dyn Error>>{let contents = fs::read_to_string(config.filename)?;println!("With text:\n{}", contents);Ok(())
}

3.7.2、main.rs

use std::{env, process};
use minigrep::Config;fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err|{println!("Problem parsing arguments:{}", err);process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.filename);if let Err(e) = minigrep::run(config) {println!(" Application error: {}", e);process::exit(1);}
}

四、测试驱动开发(TDD)

4.1、search 方法编写和测试

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut result = Vec::new();for line in contents.lines(){if line.contains(query) {result.push(line);}}println!("{:?}", result);result
}#[cfg(test)]
mod tests {use super::*;#[test]fn one_result() {let query = "duct";let contents = "\
Rust:
safe, fast, productive.
Pick, three.
Duct tape.";assert_eq!(vec!["safe, fast, productive."],search(query, contents));}
}

4.2、在 run 函数中使用 search 函数

pub fn run(config: Config) -> Result<(), Box<dyn Error>>{let contents = fs::read_to_string(config.filename)?;for line in search(&config.query, &contents) {println!("{}", line);}Ok(())
}

五、处理环境变量


use std::{fs, error::Error, env};const ARGS_LENGTH:usize= 3;pub struct Config {pub query: String,pub filename: String,pub case_sensitive: bool,
}impl Config {pub fn new(args: &[String])-> Result<Config, &'static str>{if args.len() < ARGS_LENGTH  {return Err("not enough arguments")}let query = args[1].clone();let filename = args[2].clone();let case_sensitive = env::var("CASE_INSENSITIVE").is_err();Ok( Config { query, filename , case_sensitive})}
} pub fn run(config: Config) -> Result<(), Box<dyn Error>>{let contents = fs::read_to_string(config.filename)?;for line in search(&config.query, &contents, config.case_sensitive) {println!("{}", line);}Ok(())
}pub fn search<'a>(query: &str, contents: &'a str, case_sensitive: bool) -> Vec<&'a str> {let mut result = Vec::new();if case_sensitive {let query_ignore_sensitive = query.to_lowercase();for line in contents.lines(){if line.to_lowercase().contains(&query_ignore_sensitive) {result.push(line);}}return result;} else {for line in contents.lines(){if line.contains(&query) {result.push(line);}}return result;}
}#[cfg(test)]
mod tests {use super::*;#[test]fn one_result() {let query = "duct";let contents = "\
Rust:
safe, fast, productive.
Pick, three.
Duct tape.";assert_eq!(vec!["safe, fast, productive."],search(query, contents));}
}

六、标准输出和标准错误

6.1、标准输出:stdout

println!() 宏就是把输出信息输出到标准输出

6.2、标准错误:stderr

eprintln!() 宏就是把输出信息输出到标准错误

use std::{env, process};use minigrep::Config;fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err|{eprintln!("Problem parsing arguments:{}", err);process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.filename);println!("case_sensitive: {}", config.case_sensitive);if let Err(e) = minigrep::run(config) {eprintln!(" Application error: {}", e);process::exit(1);}
}

总结

以上就是今天要讲的内容

  • 主要讲解了一个项目的编写过程

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

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

相关文章

【Pytroch】基于K邻近算法的数据分类预测(Excel可直接替换数据)

【Pytroch】基于K邻近算法的数据分类预测&#xff08;Excel可直接替换数据&#xff09; 1.模型原理2.数学公式3.文件结构4.Excel数据5.下载地址6.完整代码7.运行结果 1.模型原理 K最近邻&#xff08;K-Nearest Neighbors&#xff0c;简称KNN&#xff09;是一种简单但常用的机器…

Redis基础学习

目录 第一章、Redis数据库的下载和安装1.1&#xff09;nosql数据库和 Redis 介绍1.2&#xff09;Windows中下载安装Redis数据库1.3&#xff09;Linux中安装Redis数据库1.4&#xff09;Linux中启动redis1.5&#xff09;Linux中关闭redis 第二章、三种Redis客户端连接Redis数据库…

安全远控如何设置?揭秘ToDesk、TeamViewer 、向日葵安全远程防御大招

写在前面一、远程控制&#xff1a;安全性不可忽略二、远控软件安全设置实测◉ ToDesk◉ TeamViewer◉ 向日葵 三、远控安全的亮点功能四、个人总结与建议 写在前面 说到远程办公&#xff0c;相信大家都不陌生。远程工作是员工在家中或者其他非办公场所上班的一种工作模式&…

传输层协议

传输层协议 再谈端口号端口号范围划分认识知名端口号两个问题netstatpidof UDP协议UDP协议端格式UDP的特点面向数据报UDP的缓冲区UDP使用注意事项基于UDP的应用层协议 TCP协议TCP协议段格式确认应答(ACK)机制超时重传机制连接管理机制理解 CLOSE_WAIT 状态理解TIME_WAIT状态解决…

修改el-select和el-input样式;修改element-plus的下拉框el-select样式

修改el-select样式 .select_box{// 默认placeholder:deep .el-input__inner::placeholder {font-size: 14px;font-weight: 500;color: #3E534F;}// 默认框状态样式更改:deep .el-input__wrapper {height: 42px;background-color: rgba(0,0,0,0)!important;box-shadow: 0 0 0 …

OptaPlanner笔记6 N皇后

N 个皇后 问题描述 将n个皇后放在n大小的棋盘上&#xff0c;没有两个皇后可以互相攻击。 最常见的 n 个皇后谜题是八个皇后谜题&#xff0c;n 8&#xff1a; 约束&#xff1a; 使用 n 列和 n 行的棋盘。在棋盘上放置n个皇后。没有两个女王可以互相攻击。女王可以攻击同一水…

如何做好一名网络工程师?具体实践?

预防问题 – 资格与认证 在安装线缆或升级网络时测试线缆是预防问题的有效方式。对已安装布线进行测试的方法有两种。 资格测试确定布线是否有资格执行某些操作 — 换言之&#xff0c;支持特定网络速度或应用。尽管“通过”认证测试也表明按标准支持某一网络速度或应用的能力…

Redux - Redux在React函数式组件中的基本使用

文章目录 一&#xff0c;简介二&#xff0c;安装三&#xff0c;三大核心概念Store、Action、Reducer3.1 Store3.2 Reducer3.3 Action 四&#xff0c;开始函数式组件中使用4.1&#xff0c;引入store4.1&#xff0c;store.getState()方法4.3&#xff0c;store.dispatch()方法4.4&…

cookie和session的区别及原理

Cookie概念 在浏览某些 网站 时,这些网站会把 一些数据存在 客户端 , 用于使用网站 等跟踪用户,实现用户自定义 功能. 是否设置过期时间: 如果不设置 过期时间,则表示这个 Cookie生命周期为 浏览器会话期间 , 只要关闭浏览器,cookie就消失了. 这个生命期为浏览会话期的cookie,就…

其他行业跳槽转入计算机领域简单看法

其他行业跳槽转入计算机领域简单看法 本人选择从以下几个方向谈谈自己的想法和观点。 如何规划才能实现转码 自我评估和目标设定&#xff1a;首先&#xff0c;你需要评估自己的技能和兴趣&#xff0c;确定你希望在计算机领域从事的具体职位或领域。这有助于你更好地规划学习路…

深入了解 Rancher Desktop 设置

Rancher Desktop 设置的全面概述 Rancher Desktop 拥有方便、强大的功能&#xff0c;是最佳的开发者工具之一&#xff0c;也是在本地构建和部署 Kubernetes 的最快捷方式。 本文将介绍 Rancher Desktop 的功能和特性&#xff0c;以及 Rancher Desktop 作为容器管理平台和本地…

人工智能原理(2)

目录 一、知识与知识表示 1、知识 2、知识表示 3、知识表示方法 二、谓词逻辑表示法 1、命题逻辑 2、谓词逻辑 三、产生式表达法 1、知识的表示方法 2、产生式系统组成 3、推理方式 4、产生式表示法特点 四、语义网络 1、概念及结构 2、语义网络的基本语义联系 …

zookeeper案例

目录 案例一&#xff1a;服务器动态上下线 服务端&#xff1a; &#xff08;1&#xff09;先获取zookeeper连接 &#xff08;2&#xff09;注册服务器到zookeeper集群&#xff1a; &#xff08;3&#xff09;业务逻辑&#xff08;睡眠&#xff09;&#xff1a; 服务端代码…

Java+Excel+POI+testNG基于数据驱动做一个简单的接口测试【杭州多测师_王sir】

一、创建一个apicases.xlsx放入到eclipse的resource里面&#xff0c;然后refresh刷新一下 二、在pom.xml文件中加入poi和testng的mvn repository、然后在eclipse的对应目录下放入features和plugins&#xff0c;重启eclipse就可以看到testNG了 <!--poi excel解析 --><d…

运维监控学习笔记3

DELL的IPMI页面的登录&#xff1a; 风扇的状态&#xff1a; 电源温度&#xff1a;超过70度就告警&#xff1a; 日志信息&#xff1a; 可以看到更换过磁盘。 iDRAC的设置 虚拟控制台&#xff1a;启动远程控制台&#xff1a; 可以进行远程控制。 机房工程师帮我们接远程控制&…

【云原生】kubernetes中容器的资源限制

目录 1 metrics-server 2 指定内存请求和限制 3 指定 CPU 请求和限制 资源限制 在k8s中对于容器资源限制主要分为以下两类: 内存资源限制: 内存请求&#xff08;request&#xff09;和内存限制&#xff08;limit&#xff09;分配给一个容器。 我们保障容器拥有它请求数量的…

【云原生】K8S集群

目录 一、调度约束1.1 POT的创建过程1.1调度过程 二、指定节点调度2.1 通过标签选择节点 三、亲和性3.1requiredDuringSchedulingIgnoredDuringExecution&#xff1a;硬策略3.1 preferredDuringSchedulingIgnoredDuringExecution&#xff1a;软策略3.3Pod亲和性与反亲和性3.4使…

(2)原神角色数据分析-2

功能一&#xff1a; 得到某个属性的全部角色&#xff0c;将其封装在class中 """各元素角色信息&#xff1a;一对多""" from pandas import DataFrame, Series import pandas as pd import numpy as npclass FindType:# 自动执行&#xff0c;将…

山东布谷科技直播平台搭建游戏开发技术分享:数据存储的重要意义

在市场上的热门的直播平台中&#xff0c;有很多小程序为用户提供各种各样的功能&#xff0c;这其中就有很多游戏小程序&#xff0c;当今社会独生子女众多&#xff0c;很多作为独生子女的用户都会去选择一个能够社交互动的APP来填补内心的空虚&#xff0c;而直播平台的实时互动的…

SAP 选择屏幕组件名描述翻译时字符长度不够问题处理

问题&#xff1a;有时候我们在开发report程序的时候&#xff0c;要求程序显示支持中英文&#xff0c;如果程序是在中文环境下开发的时候&#xff0c;需要进行翻译处理&#xff0c;但是我们发现选择屏幕上的组件的描述支持的默认长度是30位&#xff0c;如果超过该如何处理呢 解…