学习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
命令来查看文件系统。
这些日常操作所用到的命令就像是阳光空气水,我们几乎感受不到它们的存在,但其实离不开它们。
但其实,所有这些命令,背后都有人在维护,在更新代码,在不断开发它们。比如上面列举的ls
,mv
,cp
这些工具,都是来自于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,必须要习惯并适应这种交互方式,才能培养期好的”感觉”,去编写正确的脚本,完成特定的任务。
接下来的难点是掌握各种工具本身,比如上面的一行脚本当中,分别使用了find
,unzip
,grep
, echo
命令,并且unzip
和grep
之间还使用了管道操作符将unzip的输入连接到了grep的输入进行过滤,结果交给if
语句进行判断。而判断结果决定是否执行echo
命令。
在这一切之前,unzip
命令的参数,是从find
命令得来的,通过for
语句一个一个交给后面的unzip/grep
命令去过滤。
所以我们必须对每一个所使用的工具要有所了解。而每一个工具都有独立的学习成本。
特别是对文本进行过滤的一些工具,比如sed
,awk
,tr
,cut
,grep
,背后需要大量的时间投入进行学习。
不管是日常的对操作系统的使用,还是代码分析,bash脚本都是完全不能被其他语言替代的利器。
- 上一篇 代码中的side effect
- 下一篇 Haskell的类型设计