你必须按所想去生活,否则只能按生活去想。 --王小波
大家好,我是柒八九。
作为一个前端
/Rust
/AI
知识博主,之前的文章中,大部分篇幅都是关于前端的知识分享,而对Rust
和AI
的内容只是做了几篇内容梳理和介绍。
而,我们今后的重心也会逐渐偏移,势必能达到前端
/Rust
/AI
三足鼎立的局面。
这里也和很多精神股东做一次简短的汇报,之前答应大家多出一些Rust
相关的文章,由于工作和个人事务侵占大部分学习和总结的时间,所以迟迟没有兑现承诺。也很感谢大部分老粉能不离不弃,在这里先叩谢大家了。
你们的支持也是我输入内容的精神支柱,同时也很感谢有些远在天涯海角的朋友,不停的给出建议和改进意见,Last but not least
,由于有些技术能力的有限,在一些表达方式和技术深度方向上,有很多瑕疵。也希望以后大家,互相学习,共同进步。
好了,估计大家不想听我在这里一个人聒噪了,那么就进入我们今天的主题。
前言
在上一篇致所有渴望学习Rust的人的信中我们介绍了Rust
可以在命令行工具上也大有建树。
现在就是我们兑现承诺的时候了。
Rust
是一种静态编译
的、快速的语言,具有出色的工具支持和迅速增长的生态系统。这使它非常适合编写命令行应用程序。
通过编写具有简单CLI
的程序,对于那些初学者来说是一个很好的练习,也是我们需要循序渐进的一个过程。毕竟,大家刚开始接触一个新的语言都是从Hello World
的入手的,但是这种Demo
级别的程序,可以说是闭门造车,没有任何的实际价值。并且这种程序是难登大雅之堂的。
所以,我们今天来通过一个简单的CLI
来巩固之前的内容,并且写出的东西也可以在公司应用场景中有用武之地。
所以说选择很重要,我们不要成为别人口中说的你之所以穷,是因为你不够努力的人。
我们在讲解代码中,有一些基础语法会一带而过,也就是说,已经默认大家已经有Rust
基础了。如果,你是一个Rust
初学者,我们也提供了Rust学习笔记系列,可以快速掌握基础语法。当然,里面的有一些内容也会做一些简单的梳理和讲解。这个就因人而异了,看大家实际情况吧。
由于篇幅的原因,我们打算写三篇文章(
上/中/下
),来介绍如何用Rust
来编写属于自己的命令行工具。 今天是第一篇文章,我们主要的目的是用Rust
写出一个可用的命令行工具。属于本地应用级别,现在先不要嗤之以鼻,我们后面的2篇文章,会逐步优化这个项目,然后达到最后发版供别人使用的级别。
你能所学到的知识点
- 前置知识点
- 项目设置
- 解析命令行参数
- 解析文件内容
- 更人性化的错误报告
- 信息输出处理
- 代码展示 (这个狠重要) 👈 徐志胜语音包
好了,天不早了,干点正事哇。
1. 前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
grep 简介
grep
是一个常用的命令行工具,用于在文本文件中搜索指定的文本模式
并返回匹配的行
。其名称来源于 global regular expression print
(全局正则表达式打印),它最初是在UNIX
操作系统中开发的,现在已经成为大多数Unix-like
系统(包括Linux
)的标准工具之一。grep
的主要功能是查找文件中包含特定文本的行,并将这些行打印到标准输出(通常是终端)上。
以下是 grep
命令的基本语法:
grep [选项] 模式 [文件...]
- 选项:可以是一些控制搜索行为的可选标志,例如
-i
(忽略大小写)、-r
(递归搜索目录)、-l
(仅显示包含匹配项的文件名)等。 - 模式:要搜索的文本模式,通常使用正则表达式来指定。
- 文件:要搜索的文件列表。如果不指定文件,则
grep
将从标准输入中读取数据。
一些常见的 grep
用法示例:
- 在文件中搜索特定字符串(不区分大小写):
grep -i "search_text" file.txt
- 在多个文件中递归搜索特定字符串并显示包含匹配项的文件名:
grep "pattern.*text" file.txt
- 使用正则表达式搜索匹配模式:
grep -c "pattern" file.txt
- 统计匹配的行数:
grep -c "pattern" file.txt
grep
是一个强大的文本搜索工具
,可以在各种情况下用于过滤、查找和处理文本数据。它的灵活性和正则表达式支持使得它在命令行中非常有用。
让我们编写一个小型的类似grep
的工具。给它起一个霸气侧漏的名称,那就叫它 - f789
吧。
我们可以在我们本地,创建一个文件夹,作为项目的工作目录。(这就看个人喜好,自行决断了)
最终,我们希望能够像这样运行我们的工具:
// 创建一个text.txt文件,并向其写入指定内容 echo "front:789" > text.txt echo "province:山西" >> text.txt echo "rust: hello" >> text.txt $ f789 rust test.txt rust: hello $ f789 --help // 提供一些帮助选项
本文中
rustc
采用的是1.72.0 (5680fa18f 2023-08-23)
的版本。并且在Cargo.toml
文件的[package]
部分中设置edition = "2021"
。
如果,版本不对会有一些库的兼容性问题,所以最好大家在运行代码前,做一下代码配置和相关的处理。具体的配置和升级可以参考Rust环境配置和入门指南或者官网.
在使用对应命令升级之前,这里有一个小的提示,如果你在
Mac
中使用brew
安装过Rust
,你最好检测一下对应的版本信息。可以使用rustc --version
命令,会返回指定版本信息。例如:rustc 1.68.2 (9eb3afe9e 2023-03-27) (built from a source tarball)
。
但是,
(built from a source tarball)
这一部分表示Rust
编译器不是通过二进制发布版安装的,而是从Rust
源代码中编译生成的。这通常是因为我们手动构建Rust
或从源代码仓库中获取Rust
的最新版本。这种情况的话,在使用rustup update
进行版本更新的时候,会有问题。所以我推荐安装官方的二进制发布版。(也就是官网的处理方式)
2. 项目设置
如果你尚未安装Rust
,可以参考我们之前的文章Rust环境配置和入门指南。然后,打开一个终端并导航到我们想要放置应用程序代码的目录。
首先,在存储编程项目的目录中运行以下命令:cargo new f789
。如果我们查看新创建的f789
目录,我们将会找到一个典型的Rust
项目设置:
我们用erdtree进行页面结构展示。当然,我们也可以用tree
命令。一切的理所应当都是命运的暗中撮合。因为erdtree
也是Rust
写的。
- 一个
Cargo.toml
文件,其中包含我们项目的元数据,包括我们使用的依赖/外部库列表。 - 一个
src/main.rs
文件,它是我们二进制文件的入口点。
如果我们可以在f789
目录中执行cargo run
并获得一个Hello World
,那么我们已经设置好了。
项目运行
$ cargo new f789 Created binary (application) `f789` package $ cd f789/ $ cargo run Compiling f789 v0.1.0 (项目存储路径) Finished dev [unoptimized + debuginfo] target(s) in 0.70s Running `target/debug/f789` Hello, world!
3. 解析命令行参数
一般的CLI
都支持参数的输入:例如tree -a -L 2
或者我们之前的erd -i -I -L 2 -y inverted
。
我们也想让我们的CLI
具有这个功能:
$ f789 front test.txt
我们期望我们的程序查看test.txt
并打印出包含front
的行。但是我们如何获取这两个值呢?
程序名称后面的文本通常被称为命令行参数
或命令行标志
(特别是当它们看起来像--
这样时)。
在操作系统内部通常将它们表示为字符串列表 - 简而言之,它们由空格分隔。
有许多方法可以探查和识别这些参数,以及如何将它们解析成更容易处理的形式。我们还需要告诉使用我们程序的用户需要提供哪些参数以及它们期望的格式是什么。
获得参数
标准库
中包含了函数std::env::args()
,它提供了给定参数的迭代器。第一项(索引为0)是我们程序被调用的名称(例如,f789
),其后的项是用户在后面写的内容。
通过这种方式获取原始参数非常容易(在文件src/main.rs
中,在fn main() {
之后):
let pattern = std::env::args().nth(1).expect("未提供模式"); let path = std::env::args().nth(2).expect("未提供路径");
这里,pattern
将包含用户输入的第一个参数,path
将包含用户输入的第二个参数。如果用户没有提供这些参数,程序将会报错并显示相应的错误消息。
将 CLI 参数自定义数据类型
与将CLI
参数视为一堆文本相比,将其视为表示程序输入的自定义数据类型
通常更有帮助。
看看 f789 front test.txt
:有两个参数,首先是模式
(要查找的字符串),然后是路径
(要查找的文件)。
此外还有其它需要注意的点?首先,它们都是必需的。我们还没有讨论默认值,因此我们期望用户始终提供两个值。此外,我们还可以谈谈它们的类型:模式应该是一个字符串
,而第二个参数应该是文件的路径
。
在Rust
中,通常以处理的数据为中心来构建程序,因此以这种方式看待CLI
参数非常合适。让我们做一层数据抽象(在文件src/main.rs
中,在fn main() {
之前):
struct Cli { pattern: String, path: std::path::PathBuf, }
这定义了一个新的结构体(struct
),它有两个字段来存储数据:pattern
和path
。
注意:
PathBuf
类似于String
,但用于跨平台的文件系统路径。
现在,我们需要将我们的程序接收到的实际参数转换为这种形式。一种选项是手动解析操作系统获取的字符串列表并自己构建结构。代码可能如下所示:
let pattern = std::env::args().nth(1).expect("未提供模式"); let path = std::env::args().nth(2).expect("未提供路径"); let args = Cli { pattern: pattern, path: std::path::PathBuf::from(path), };
这种方法是可行的,但不够方便。上面的方式无法满足,用户天马行空
的创造力。例如:遇到类似--pattern="front"
或 --pattern "front"
和 --help
的参数形式上面的代码就捉襟见肘
了。
也就是说,上面的代码不够优雅。