阿男的小窝

View the Project on GitHub

学习Bash Shell的价值

这次跟大家聊聊Bash Shell编程。

作为程序员,可能在计算机上面输入文字形式的命令,要比使用图形化的软件的时间还要多一些。而我们输入命令的这个媒介就是Shell(命令终端,terminal,在本文中这些名词作为可以互换的概念出现)。

在DOS时期,DOS系统集成了一个命令输入界面,并没有独立的名字。在Windows下,这个终端界面成为了独立的程序,叫cmd。不准备和大家讨论Windows下的这个terminal,因为它实在太弱(原因后面说明)。我们具体来看Linux下的terminal。

Linux下有各种各样的terminal的实现,比如sh,bash,zsh等等,它们在设计上有共通之处,就在这篇文章中拿bash作为说明。

对于初学者来讲,可能bash shell就是一个输入命令,访问文件系统的媒介,使用它来完成日常的一些命令执行工作,以及对文件的操作等等。

但实际上,我们应该体会到,bash shell其实是一个操作系统暴露出来的”接口”,我们通过输入命令,完成了与操作系统的交互。一旦理解了这样的设定,我们就应该知道,实际上bash shell这种东西蕴涵着巨大的能量。

我们想一下bash shell和某一种编程语言相比,根本区别在哪里?

第一个巨大的区别,bash shell是一个开放式的环境。在Linux这样的开源环境下,它本身拥有无限多个小程序,每一个小程序都完成一个很窄的领域的特定的任务。比如我们日常会使用cp来拷贝文件,使用mv命令来移动文件,使用ls命令来查看文件系统。

这些日常操作所用到的命令就像是阳光空气水,我们几乎感受不到它们的存在,但其实离不开它们。

但其实,所有这些命令,背后都有人在维护,在更新代码,在不断开发它们。比如上面列举的lsmvcp这些工具,都是来自于GNU的coreutils这个项目:

https://www.gnu.org/software/coreutils/coreutils.html

在github上面有项目的代码镜像供大家学习参考:

https://github.com/coreutils/coreutils

整个这些小工具,形成了一个生态圈,让我们与操作系统打交道变得很方便。

我们想象,如果使用一门语言,来完成以上这些任务,需要怎么做?我们要么是调用语言本身的一些api自己写代码,要么是找一些开源的库完成一些工作。实际上并不那么直接。

用一句话来概括:在shell环境下,我们是和各种工具打交道;在某一种语言的环境里,我们是和这个语言的接口打交道。

第二个巨大的区别, bash的设计是把工具的输入输出连接起来,而某种语言是把api的接口连接起来。

bash的管道操作符是非常成功的设计,它让所有的小工具联合起来,形成一张网。

我们可以使用ls命令来获得文件列表,然后把这个输出直接交给grep过滤出想要的内容:

$ ls | grep jpg
foo.jpg
bar.jpg

通过管道操作符,我们把命令的输入输出连接在了一起。这种能力是可怕的:通过特定的工具完成特定的任务,再把各个工具串联起来,我们最终获得的是一条自动化的生产线。在这个过程中,而且我们几乎不需要自己写什么代码。

如果使用某种特定的语言完成上述任务,所需要的成本是很高的:我们要自己寻找合适的接口,接口之间的数据处理很大情况下也需要自己处理。

而shell本身,可以调用各种语言的解释器,来完成特定任务。比如我们可以写一个ruby程序,再写一个python程序,再写一个perl程序,然后用bash提供的管道操作符,把三个程序的输入输出连接起来,完成特定任务。

这里面的差异,还需要经验的积累去体会。接下来说说bash的学习门槛。

很多人觉得日常使用bash来执行命令,进行简单的操作还比较容易,但真正学习bash编程,就觉得难度陡增。

首先是对各个工具使用的不熟练。我们做bash编程,很大程度上是为了完成一些手工操作很麻烦的事情。简单来讲,就是把一些可以自动化的任务用脚本完成。

而实现一个看似简单的任务,可能就要许多工具的联合工作才能完成。比如我们想象这样一个任务:

某一个目录下,有很多zip文件,假设有100多个吧,我们想找出里面包含名为”README”文件的所有zip包。

这种工作其实在日常当中非常普遍,我们如果在命令行下实现,该怎么做?

可能新手会一个一个zip文件去unzip -l查看里面的内容,稍微会使用管道操作符的,还会使用grep来过滤一下unzip的输出。100多个zip文件这样查找,会浪费多少时间?

但是对于使用bash脚本经验比较丰富的程序员,这个任务可以简化成一行代码:

for f in $(find *.zip) ; do if [[ $(unzip -l $f | grep README) ]] ; then echo $f ; fi ; done

上面这段代码就可以完成上面所讲的任务,即使这个目录下有成百上千的zip文件,对于程序来说也没有太大区别,人只要输入完命令,等执行结果就可以了,不需要浪费人的宝贵时间去手工做什么事情。

上面的代码,难倒初学者的东西大概有很多。首先,bash脚本和其它的语言编程最大区别就是,bash脚本是一种脚本自身和各种工具的混合体。

比如上面的命令中,for f in $(...)是脚本自身的语法,但是$()里面包含的find *.zip实际上是执行linux下的find命令。

以及后面if [[ $(...) ]]是bash脚本的自身语法,而里面的unzip ... | grep ...则是调用命令。

而且很显然bash脚本是通过读取这些命令的返回值和输出来完成特定的任务。所以这种交互性,是对于新手的一大难点。因此学习bash,必须要习惯并适应这种交互方式,才能培养期好的”感觉”,去编写正确的脚本,完成特定的任务。

接下来的难点是掌握各种工具本身,比如上面的一行脚本当中,分别使用了findunzipgrep, echo命令,并且unzipgrep之间还使用了管道操作符将unzip的输入连接到了grep的输入进行过滤,结果交给if语句进行判断。而判断结果决定是否执行echo命令。

在这一切之前,unzip命令的参数,是从find命令得来的,通过for语句一个一个交给后面的unzip/grep命令去过滤。

所以我们必须对每一个所使用的工具要有所了解。而每一个工具都有独立的学习成本。

特别是对文本进行过滤的一些工具,比如sedawktrcutgrep,背后需要大量的时间投入进行学习。

不管是日常的对操作系统的使用,还是代码分析,bash脚本都是完全不能被其他语言替代的利器。