一、前言
rust语言目前应用最广的方向之一就是命令行了,因为rust语言代码简练、内存安全、效率很高,用来开发命令行工具是非常合适的。
目前已经有相当多的命令行工具都在采用rust重写了。
二、认识命令行
命令行程序与图形化程序相对,简写分别为:CLI
与GUI
,这两个单词相信大家以后会经常看到的,其指代的分别是这两者,官方点的称谓分别为:命令行接口(command-line interface)、图形用户接口(Graphical User Interface)。
图形化程序应该是大家最早认识到的,比如QQ、微信等应用程序都是图形化界面。
而命令行则比较少见了,但只要你写过基本的C/C++程序、乃至本系列文章中的rust程序,其实也算是认识了,因为我们目前写的这些程序也都算是命令行程序。
唯一的不同之处在于,我们这些程序是点击即运行,而无法像那些看起来很专业的工具还有一大堆说明、参数等等。
而这就是本文的核心,带大家快速开发一个属于自己的命令行工具。
一个命令行工具说简单也简单,但说复杂有时也挺复杂的,比如就拿大家最熟悉的Windows系统为例,打开其终端,输入命令dir
,它就会列出当前目录下所有的文件:
这是它的默认行为,但其实它是可以带选项的,windows系统自带的命令选项是一/
开头,但主流的命令行工具都是以-
开头,这里先以windows系统的dir命令为例:
window系统自带的命令都有一个?
的选项,它会列举出该命令的使用文档,比如上图中就可以看到,除了?
外,还有相当多/
开头的选项,通过传入不同的选项就可以启用这个命令行工具的不同功能。
只要是在[]
中的选项,那就都是可选的,也就是可以不填。
但有些命令不行,比如创建目录的mkdir
命令,它需要“参数”,注意这里的用词,不再是“选项”了,而是“参数”。
参数便是不以/
或-
开头的数据:
比如上图中,直接使用mkdir
命令就不行了,通过查看其帮助文档,发现其命令后面跟着一个path
,它没有用/
开头,说明它是一个参数,并且它没有被[]
包括起来,说明它是必须要传入的数据。
虽然命令行工具可以有很大的自由度,你怎么搞都行,但如果想要让自己的命令行工具能够被广泛应用,那最好还是参考这些大家都统一遵守的原则,这样就可以为用户减少一些学习成本。
通过以上的观察我们可以发现,一个命令行工具一般是需要包含以下三部分的:
- 选项:通过
/
或者-
开头,用于启动命令行工具的不同功能 - 参数:一般直接跟着后面
- 帮助文档:一般通过固有的参数
/?
、-h
或--help
来输入当前工具的帮助文档。
不同部分都是直接用空格隔开的,但如果有些参数内部存在空格,比如路径,那你就需要用引号""
将该参数包裹起来。
而选项内又有所区别,分为短选项与长选项,比如前面提到的-h
与--help
一般就是实现同一个功能的两个选项。
比如linux系统中用的最多的ls
命令,当列举出它的帮助文档后,就会发现每个功能前面都会列举出短选项与长选项:
一般长选项是一个完整的单词,用--
开头,而短选型则是一个字母,用-
开头,且短选项之间可以互相拼接,比如ls -al
,这里的-al
其实就是-a
与-l
两者的拼接结合。
至此,我们就算是基本把命令行工具的一些大家统一遵守的规范给大家过了一遍,下面就可以开始正式用rust写命令行工具了。
三、代码实现
上面的规范很繁杂,这里先用最原生的代码让大家看一看如何不用任何第三方库开发一个简单的命令行工具。
至于上面复杂的选项、参数解析功能,咱就没必要自己来实现了,因为rust中已经有非常完善的库可以让我们使用了。
首先第一步也是最重要的一步便是获取命令行参数:
fn main() {
for arg in std::env::args() {
println!("{}", arg);
}
}
这个很简单,直接调用args
函数即可获取到,同时注意,与命令行有关的东西都放在了std
中的env
里面。
传入参数也是很简单的,直接写在cargo run
命令之后即可,同时注意第一个参数都是可执行的路径位置。
然后通过for
循环就能将其遍历打印出来。
但这样使用起来并不方便,所以我们更多时候是希望将其存放为一个数组:
fn main() {
let args: Vec<String> = std::env::args().collect();
}
这里调用的collect
函数很重要,它的作用就和它的名字一样:收集。
它可以将一系列迭代器的结果收集起来放到一个容器中,而这个容器需要我们自己写在最前面,也就是此时的类型标注:Vec<String>
。
也就是说,它是根据返回值来确认接下来要执行的行为的!这在C++中是难以想象的,但在rust中就是这么容易的实现了。
迭代器和链表结构有点像,你只能一个一个的拿取数据。
如果不使用collect
函数,那你就得这样写:
fn main() {
let mut args = std::env::args(); //得到参数的迭代器
let mut v_args = Vec::new();
loop {
let t = args.next(); //取出下一个数据
if let None = t { //没有数据,则跳出循环
break;
}
v_args.push(t.unwrap()); //有数据,则推入数组中
}
}
上面两个写法是完全等价的,这里用到的next
函数就是迭代器所特有的函数,调用一次,就拿回一个数据,直到没有数据了,就返回None
,此时循环也就结束了,是不是和链表结构很像?
任何具有迭代器结构的都有这个collect
函数,它的作用就是简化这一过程的。
比如数组本身也可以被for
遍历循环,说明其本身也有迭代器的实现,所以其身上也有collect
函数:
let arr = [1, 2, 3];
let test: Vec<i32> = arr.iter().map(|x| *x).collect();
通过iter
函数即可获取数组的迭代器,但要注意,此时迭代器的元素都是原数组中元素的引用,引用元素是无法再被集合到另一个数组中的。
所以这里我在其中间调用了map
函数,其目的就是让所有元素都调用一下map
内部的闭包,通过前面加*
对其进行解引用返回,这样就能形成一个新Vec
数组了。
map
也很常用,用于将所有迭代器对象都去执行某个步骤并返回新的迭代器结果,比如这里的解引用,其中闭包的参数就是迭代器中的每个数据。
回到本文,有了命令行参数之后,我们自然就可以通过用户传入的不同参数来执行不同任务了。
而这一步说实话也是最繁琐的,所有我这里只简单做个演示即可,后面直接使用现成的库要方便的多: