Shell脚本编程初体验
通常,当人们提到“shell脚本语言”时,浮现在他们脑海中是bash,ksh,sh或者其它相类似的linux/unix脚本语言。脚本语言是与计算机交流的另外一种途径。使用图形化窗口界面(不管是windows还是linux都无所谓)用户可以移动鼠标并点击各种对象,比如按钮、列表、选框等等。但这种方式在每次用户想要计算机/服务器完成相同任务时(比如说批量转换照片,或者下载新的电影、mp3等)却是十分不方便。要想让所有这些事情变得简单并且自动化,我们可以使用shell脚本。
某些编程语言,像pascal、foxpro、C、java之类,在执行前需要先进行编译。它们需要合适的编译器来让我们的代码完成某个任务。
而其它一些编程语言,像php、javascript、visualbasic之类,则不需要编译器,因此它们需要解释器,而我们不需要编译代码就可以运行程序。
shell脚本也像解释器一样,但它通常用于调用外部已编译的程序。然后,它会捕获输出结果、退出代码并根据情况进行处理。
Linux世界中最为流行的shell脚本语言之一,就是bash。而我认为(这是我自己的看法)原因在于,默认情况下bash shell可以让用户便捷地通过历史命令(先前执行过的)导航,与之相反的是,ksh则要求对.profile进行一些调整,或者记住一些“魔术”组合键来查阅历史并修正命令。
好了,我想这些介绍已经足够了,剩下来哪个环境最适合你,就留给你自己去判断吧。从现在开始,我将只讲bash及其脚本。在下面的例子中,我将使用CentOS 6.6和bash-4.1.2。请确保你有相同版本,或者更高版本。
Shell脚本流
shell脚本语言就跟和几个人聊天类似。你只需把所有命令想象成能帮你做事的那些人,只要你用正确的方式来请求他们去做。比如说,你想要写文档。首先,你需要纸。然后,你需要把内容说给某个人听,让他帮你写。最后,你想要把它存放到某个地方。或者说,你想要造一所房子,因而你需要请合适的人来清空场地。在他们说“事情干完了”,那么另外一些工程师就可以帮你来砌墙。最后,当这些工程师们也告诉你“事情干完了”的时候,你就可以叫油漆工来给房子粉饰了。如果你让油漆工在墙砌好前就来粉饰,会发生什么呢?我想,他们会开始发牢骚了。几乎所有这些像人一样的命令都会说话,如果它们完成了工作而没有发生什么问题,那么它们就会告诉“标准输出”。如果它们不能做你叫它们做的事——它们会告诉“标准错误”。这样,最后,所有的命令都通过“标准输入”来听你的话。
快速实例——当你打开linux终端并写一些文本时——你正通过“标准输入”和bash说话。那么,让我们来问问bash shell who am i(我是谁?)吧。
<span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> who am i </span><span class="pun"><---</span><span class="pun">你通过标准输入对</span><span class="pln"> bash shell </span><span class="pun">说</span>
<span class="pln">root pts</span><span class="pun">/</span><span class="lit">0</span><span class="lit">2015</span><span class="pun">-</span><span class="lit">04</span><span class="pun">-</span><span class="lit">22</span><span class="lit">20</span><span class="pun">:</span><span class="lit">17</span><span class="pun">(</span><span class="lit">192.168</span><span class="pun">.</span><span class="lit">1.123</span><span class="pun">)</span><span class="pun"><---</span><span class="pln"> bash shell</span><span class="pun">通过标准输出回答你</span>
现在,让我们说一些bash听不懂的问题:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> blablabla </span><span class="pun"><---</span><span class="pun">哈,你又在和标准输入说话了</span>
<span class="pun">-</span><span class="pln">bash</span><span class="pun">:</span><span class="pln"> blablabla</span><span class="pun">:</span><span class="pln"> command </span><span class="kwd">not</span><span class="pln"> found </span><span class="pun"><---</span><span class="pln"> bash</span><span class="pun">通过标准错误在发牢骚了</span>
“:”之前的第一个单词通常是向你发牢骚的命令。实际上,这些流中的每一个都有它们自己的索引号(LCTT 译注:文件句柄号):
- 标准输入(stdin) - 0
- 标准输出(stdout) - 1
- 标准错误(stderr) - 2
如果你真的想要知道哪个输出命令说了些什么——你需要将那次发言重定向到(在命令后使用大于号“>”和流索引)文件:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> blablabla </span><span class="lit">1</span><span class="pun">></span><span class="pln"> output</span><span class="pun">.</span><span class="pln">txt</span>
<span class="pun">-</span><span class="pln">bash</span><span class="pun">:</span><span class="pln"> blablabla</span><span class="pun">:</span><span class="pln"> command </span><span class="kwd">not</span><span class="pln"> found</span>
在本例中,我们试着重定向流1(stdout)到名为output.txt的文件。让我们来看对该文件内容所做的事情吧,使用cat命令可以做这事:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> cat output</span><span class="pun">.</span><span class="pln">txt</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span>
看起来似乎是空的。好吧,现在让我们来重定向流2(stderr):
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> blablabla </span><span class="lit">2</span><span class="pun">></span><span class="pln"> error</span><span class="pun">.</span><span class="pln">txt</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span>
好吧,我们看到牢骚话没了。让我们检查一下那个文件:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> cat error</span><span class="pun">.</span><span class="pln">txt</span>
<span class="pun">-</span><span class="pln">bash</span><span class="pun">:</span><span class="pln"> blablabla</span><span class="pun">:</span><span class="pln"> command </span><span class="kwd">not</span><span class="pln"> found</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span>
果然如此!我们看到,所有牢骚话都被记录到errors.txt文件里头去了。
有时候,命令会同时产生stdout和stderr。要重定向它们到不同的文件,我们可以使用以下语句:
<span class="pln">command </span><span class="lit">1</span><span class="pun">></span><span class="kwd">out</span><span class="pun">.</span><span class="pln">txt </span><span class="lit">2</span><span class="pun">></span><span class="pln">err</span><span class="pun">.</span><span class="pln">txt</span>
要缩短一点语句,我们可以忽略“1”,因为默认情况下stdout会被重定向:
<span class="pln">command </span><span class="pun">></span><span class="kwd">out</span><span class="pun">.</span><span class="pln">txt </span><span class="lit">2</span><span class="pun">></span><span class="pln">err</span><span class="pun">.</span><span class="pln">txt</span>
好吧,让我们试试做些“坏事”。让我们用rm命令把file1和folder1给删了吧:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> rm </span><span class="pun">-</span><span class="pln">vf folder1 file1 </span><span class="pun">></span><span class="kwd">out</span><span class="pun">.</span><span class="pln">txt </span><span class="lit">2</span><span class="pun">></span><span class="pln">err</span><span class="pun">.</span><span class="pln">txt</span>
现在来检查以下输出文件:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> cat </span><span class="kwd">out</span><span class="pun">.</span><span class="pln">txt</span>
<span class="pln">removed </span><span class="str">`file1'</span>
<span class="str">[root@localhost ~]# cat err.txt</span>
<span class="str">rm: cannot remove `</span><span class="pln">folder1</span><span class="str">': Is a directory</span>
<span class="str">[root@localhost ~]#</span>
正如我们所看到的,不同的流被分离到了不同的文件。有时候,这也不是很方便,因为我们想要查看出现错误时,在某些操作前面或后面所连续发生的事情。要实现这一目的,我们可以重定向两个流到同一个文件:
<span class="pln">command </span><span class="pun">>></span><span class="pln">out_err</span><span class="pun">.</span><span class="pln">txt </span><span class="lit">2</span><span class="pun">>></span><span class="pln">out_err</span><span class="pun">.</span><span class="pln">txt</span>
注意:请注意,我使用“>>”替代了“>”。它允许我们附加到文件,而不是覆盖文件。
我们也可以重定向一个流到另一个:
<span class="pln">command </span><span class="pun">></span><span class="pln">out_err</span><span class="pun">.</span><span class="pln">txt </span><span class="lit">2</span><span class="pun">>&</span><span class="lit">1</span>
让我来解释一下吧。所有命令的标准输出将被重定向到out_err.txt,错误输出将被重定向到流1(上面已经解释过了),而该流会被重定向到同一个文件。让我们看这个实例:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> rm </span><span class="pun">-</span><span class="pln">fv folder2 file2 </span><span class="pun">></span><span class="pln">out_err</span><span class="pun">.</span><span class="pln">txt </span><span class="lit">2</span><span class="pun">>&</span><span class="lit">1</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> cat out_err</span><span class="pun">.</span><span class="pln">txt</span>
<span class="pln">rm</span><span class="pun">:</span><span class="pln"> cannot remove </span><span class="str">`folder2': Is a directory</span>
<span class="str">removed `</span><span class="pln">file2</span><span class="str">'</span>
<span class="str">[root@localhost ~]#</span>
看着这些组合的输出,我们可以将其说明为:首先,rm命令试着将folder2删除,而它不会成功,因为linux要求-r键来允许rm命令删除文件夹,而第二个file2会被删除。通过为rm提供-v(详情)键,我们让rm命令告诉我们每个被删除的文件或文件夹。
这些就是你需要知道的,关于重定向的几乎所有内容了。我是说几乎,因为还有一个更为重要的重定向工具,它称之为“管道”。通过使用|(管道)符号,我们通常重定向stdout流。
比如说,我们有这样一个文本文件:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> cat text_file</span><span class="pun">.</span><span class="pln">txt</span>
<span class="typ">This</span><span class="pln"> line does </span><span class="kwd">not</span><span class="pln"> contain H e l l o word</span>
<span class="typ">This</span><span class="pln"> lilne contains </span><span class="typ">Hello</span>
<span class="typ">This</span><span class="pln"> also containd </span><span class="typ">Hello</span>
<span class="typ">This</span><span class="pln"> one </span><span class="kwd">no</span><span class="pln"> due to HELLO all capital</span>
<span class="typ">Hello</span><span class="pln"> bash world</span><span class="pun">!</span>
而我们需要找到其中某些带有“Hello”的行,Linux中有个grep命令可以完成该工作:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> grep </span><span class="typ">Hello</span><span class="pln"> text_file</span><span class="pun">.</span><span class="pln">txt</span>
<span class="typ">This</span><span class="pln"> lilne contains </span><span class="typ">Hello</span>
<span class="typ">This</span><span class="pln"> also containd </span><span class="typ">Hello</span>
<span class="typ">Hello</span><span class="pln"> bash world</span><span class="pun">!</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span>
当我们有个文件,想要在里头搜索的时候,这用起来很不错。当如果我们需要在另一个命令的输出中查找某些东西,这又该怎么办呢?是的,当然,我们可以重定向输出到文件,然后再在文件里头查找:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> fdisk </span><span class="pun">-</span><span class="pln">l</span><span class="pun">></span><span class="pln">fdisk</span><span class="pun">.</span><span class="kwd">out</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> grep </span><span class="str">"Disk /dev"</span><span class="pln"> fdisk</span><span class="pun">.</span><span class="kwd">out</span>
<span class="typ">Disk</span><span class="pun">/</span><span class="pln">dev</span><span class="pun">/</span><span class="pln">sda</span><span class="pun">:</span><span class="lit">8589</span><span class="pln"> MB</span><span class="pun">,</span><span class="lit">8589934592</span><span class="pln"> bytes</span>
<span class="typ">Disk</span><span class="pun">/</span><span class="pln">dev</span><span class="pun">/</span><span class="pln">mapper</span><span class="pun">/</span><span class="typ">VolGroup</span><span class="pun">-</span><span class="pln">lv_root</span><span class="pun">:</span><span class="lit">7205</span><span class="pln"> MB</span><span class="pun">,</span><span class="lit">7205814272</span><span class="pln"> bytes</span>
<span class="typ">Disk</span><span class="pun">/</span><span class="pln">dev</span><span class="pun">/</span><span class="pln">mapper</span><span class="pun">/</span><span class="typ">VolGroup</span><span class="pun">-</span><span class="pln">lv_swap</span><span class="pun">:</span><span class="lit">855</span><span class="pln"> MB</span><span class="pun">,</span><span class="lit">855638016</span><span class="pln"> bytes</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span>
如果你打算grep一些双引号引起来带有空格的内容呢!
注意:fdisk命令显示关于Linux操作系统磁盘驱动器的信息。
就像我们看到的,这种方式很不方便,因为我们不一会儿就把临时文件空间给搞乱了。要完成该任务,我们可以使用管道。它们允许我们重定向一个命令的stdout到另一个命令的stdin流:
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span><span class="pln"> fdisk </span><span class="pun">-</span><span class="pln">l </span><span class="pun">|</span><span class="pln"> grep </span><span class="str">"Disk /dev"</span>
<span class="typ">Disk</span><span class="pun">/</span><span class="pln">dev</span><span class="pun">/</span><span class="pln">sda</span><span class="pun">:</span><span class="lit">8589</span><span class="pln"> MB</span><span class="pun">,</span><span class="lit">8589934592</span><span class="pln"> bytes</span>
<span class="typ">Disk</span><span class="pun">/</span><span class="pln">dev</span><span class="pun">/</span><span class="pln">mapper</span><span class="pun">/</span><span class="typ">VolGroup</span><span class="pun">-</span><span class="pln">lv_root</span><span class="pun">:</span><span class="lit">7205</span><span class="pln"> MB</span><span class="pun">,</span><span class="lit">7205814272</span><span class="pln"> bytes</span>
<span class="typ">Disk</span><span class="pun">/</span><span class="pln">dev</span><span class="pun">/</span><span class="pln">mapper</span><span class="pun">/</span><span class="typ">VolGroup</span><span class="pun">-</span><span class="pln">lv_swap</span><span class="pun">:</span><span class="lit">855</span><span class="pln"> MB</span><span class="pun">,</span><span class="lit">855638016</span><span class="pln"> bytes</span>
<span class="pun">[</span><span class="pln">root@localhost </span><span class="pun">~]#</span>
如你所见,我们不需要任何临时文件就获得了相同的结果。我们把fdisk stdout重定向到了grep stdin。
注意 : 管道重定向总是从左至右的。
还有几个其它重定向,但是我们将把它们放在后面讲。
在shell中显示自定义信息
正如我们所知道的,通常,与shell的交流以及shell内的交流是以对话的方式进行的。因此,让我们创建一些真正的脚本吧,这些脚本也会和我们讲话。这会让你学到一些简单的命令,并对脚本的概念有一个更好的理解。
假设我们是某个公司的总服务台经理,我们想要创建某个shell脚本来注册呼叫信息:电话号码、用户名以及问题的简要描述。我们打算把这些信息存储到普通文本文件data.txt中,以便今后统计。脚本它自己就是以对话的方式工作,这会让总服务台的工作人员的小日子过得轻松点。那么,首先我们需要显示提问。对于显示信息,我们可以用echo和printf命令。这两个都是用来显示信息的,但是printf更为强大,因为我们可以通过它很好地格式化输出,我们可以让它右对齐、左对齐或者为信息留出专门的空间。让我们从一个简单的例子开始吧。要创建文件,请使用你惯用的文本编辑器(kate,nano,vi,……),然后创建名为note.sh的文件,里面写入这些命令:
<span class="pln">echo </span><span class="str">"Phone number ?"</span>