bash1
Bourneagainshell(bash)基本编程
2000年3月01日
通过学习如何使用bash脚本语言编程,将使Linux的日常交互更有趣和有生产力,同时还可以利用那些已熟悉和喜爱的标准UNIX概念(如管道和重定向)。在此三部分系列中,DanielRobbins将以示例指导您如何用bash编程。他将讲述非常基本的知识(这使此系列十分适合初学者),并在后续系列中逐步引入更高级特性。
您可能要问:为什么要学习Bash编程?好,以下是几条令人信服的理由:
如果查看一下,可能会发现:您现在正在运行bash。因为bash是标准Linuxshell,并用于各种目的,所以,即使更改了缺省shell,bash可能仍在系统中某处运行。因为bash已在运行,以后运行的任何bash脚本都天生是有效利用内存的,因为它们与任何已运行的bash进程共享内存。如果正在运行的工具可以胜任工作,并且做得很好,为什么还要装入一个500K的解释器?
不仅在运行bash,实际上,您每天还在与bash打交道。它总在那里,因此学习如何最大限度使用它是有意义的。这样做将使您的bash经验更有趣和有生产力。但是为什么要学习bash编程?很简单,因为您已在考虑如何运行命令、CPing文件以及管道化和重定向输出。为什么不学习一种语言,以便使用和利用那些已熟悉和喜爱的强大省时的概念?命令shell开启了UNIX系统的潜能,而bash正是这个Linuxshell。它是您和机器之间的高级纽带。增长bash知识吧,这将自动提高您在Linux和UNIX中的生产力--就那么简单。
以错误方式学习bash令人十分困惑。许多新手输入"manbash"来查看bash帮助页,但只得到非常简单和技术方面的shell功能性描述。还有人输入"infobash"(来查看GNU信息文档),只能得到重新显示的帮助页,或者(如果幸运)略为友好的信息文档。
尽管这可能使初学者有些失望,但标准bash文档无法满足所有人的要求,它只适合那些已大体熟悉shell编程的人。帮助页中确实有很多极好的技术信息,但对初学者的帮助却有限。
这就是本系列的目的所在。在本系列中,我将讲述如何实际使用bash编程概念,以便编写自己的脚本。与技术描述不同,我将以简单的语言为您解释,使您不仅知道事情做什么,还知道应在何时使用。在此三部分系列末尾,您将可以自己编写复杂的bash脚本,并可以自如地使用bash以及通过阅读(和理解)标准bash文档来补充知识。让我们开始吧。
在bash和几乎所有其它shell中,用户可以定义环境变量,这些环境变量在以ASCII字符串存储。环境变量的最便利之处在于:它们是UNIX进程模型的标准部分。这意味着:环境变量不仅由shell脚本独用,而且还可以由编译过的标准程序使用。当在bash中“导出”环境变量时,以后运行的任何程序,不管是不是shell脚本,都可以读取设置。一个很好的例子是vipw命令,它通常允许root用户编辑系统口令文件。通过将EDITOR环境变量设置成喜爱的文本编辑器名称,可以配置vipw,使其使用该编辑器,而不使用vi,如果习惯于xemacs而确实不喜欢vi,那么这是很便利的。
在bash中定义环境变量的标准方法是:
C代码$myvar='Thisismyenvironmentvariable!'$myvar='Thisismyenvironmentvariable!'
以上命令定义了一个名为"myvar"的环境变量,并包含字符串"Thisismyenvironmentvariable!"。以上有几点注意事项:第一,在等号"="的两边没有空格,任何空格将导致错误(试一下看看)。第二个件要注意的事是:虽然在定义一个字时可以省略引号,但是当定义的环境变量值多于一个字时(包含空格或制表键),引号是必须的。
第三,虽然通常可以用双引号来替代单引号,但在上例中,这样做会导致错误。为什么呢?因为使用单引号禁用了称为扩展的bash特性,其中,特殊字符和字符系列由值替换。例如,"!"字符是历史扩展字符,bash通常将其替换为前面输入的命令。(本系列文章中将不讲述历史扩展,因为它在bash编程中不常用。有关历史扩展的详细信息,请参阅bash帮助页中的“历史扩展”一节。)尽管这个类似于宏的功能很便利,但我们现在只想在环境变量后面加上一个简单的感叹号,而不是宏。
现在,让我们看一下如何实际使用环境变量。这有一个例子:
$echo$myvar
Thisismyenvironmentvariable!
通过在环境变量的前面加上一个$,可以使bash用myvar的值替换它。这在bash术语中叫做“变量扩展”。但是,这样做将怎样:
$echofoo$myvarbar
foo
我们希望回显"fooThisismyenvironmentvariable!bar",但却不是这样。错在哪里?简单地说,bash变量扩展设施陷入了困惑。它无法识别要扩展哪一个变量:$m、$my、$myvar、$myvarbar等等。如何更明确清楚地告述bash引用哪一个变量?试一下这个:
$echofoo${myvar}bar
fooThisismyenvironmentvariable!bar
如您所见,当环境变量没有与周围文本明显分开时,可以用花括号将它括起。虽然$myvar可以更快输入,并且在大多数情况下正确工作,但${myvar}却能在几乎所有情况下正确通过语法分析。除此之外,二者相同,将在本系列的余下部分看到变量扩展的两种形式。请记住:当环境变量没有用空白(空格或制表键)与周围文本分开时,请使用更明确的花括号形式。
回想一下,我们还提到过可以“导出”变量。当导出环境变量时,它可以自动地由以后运行的任何脚本或可执行程序环境使用。shell脚本可以使用shell的内置环境变量支持“到达”环境变量,而C程序可以使用getenv()函数调用。这里有一些C代码示例,输入并编译它们--它将帮助我们从C的角度理解环境变量:
#include<stdio.h>
#include<stdlib.h>
intmain(void){
char*myenvvar=getenv("EDITOR");
printf("Theeditorenvironmentvariableissetto%s\n",myenvvar);
}
将上面的代码保存到文件myenv.c中,然后发出以下命令进行编译:
$gccmyenv.c-omyenv
现在,目录中将有一个可执行程序,它在运行时将打印EDITOR环境变量的值(如果有值的话)。这是在我机器上运行时的情况:
$./myenv
Theeditorenvironmentvariableissetto(null)
啊...因为没有将EDITOR环境变量设置成任何值,所以C程序得到一个空字符串。让我们试着将它设置成特定值:
$EDITOR=xemacs
$./myenv
Theeditorenvironmentvariableissetto(null)
虽然希望myenv打印值"xemacs",但是因为还没有导出环境变量,所以它却没有很好地工作。这次让它正确工作:
$exportEDITOR
$./myenv
Theeditorenvironmentvariableissettoxemacs
现在,如您亲眼所见:不导出环境变量,另一个进程(在本例中是示例C程序)就看不到环境变量。顺便提一句,如果愿意,可以在一行定义并导出环境变量,如下所示:
$exportEDITOR=xemacs
这与两行版本的效果相同。现在该演示如何使用unset来除去环境变量:
$unsetEDITOR
$./myenv
Theeditorenvironmentvariableissetto(null)
截断字符串是将初始字符串截断成较小的独立块,它是一般shell脚本每天执行的任务之一。很多时候,shell脚本需要采用全限定路径,并找到结束的文件或目录。虽然可以用bash编码实现(而且有趣),但标准basenameUNIX可执行程序可以极好地完成此工作:
$basename/usr/local/share/doc/foo/foo.txt
foo.txt
$basename/usr/home/drobbins
drobbins
Basename是一个截断字符串的极简便工具。它的相关命令dirname返回basename丢弃的“另”一部分路径。
$dirname/usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$dirname/usr/home/drobbins/
/usr/home
需要知道一个简便操作:如何创建一个包含可执行命令结果的环境变量。这很容易:
$MYDIR=`dirname/usr/local/share/doc/foo/foo.txt`
$echo$MYDIR
/usr/local/share/doc/foo
上面所做的称为“命令替换”。此例中有几点需要指出。在第一行,简单地将要执行的命令以反引号括起。那不是标准的单引号,而是键盘中通常位于Tab键之上的单引号。可以用bash备用命令替换语法来做同样的事:
$MYDIR=$(dirname/usr/local/share/doc/foo/foo.txt)
$echo$MYDIR
/usr/local/share/doc/foo
如您所见,bash提供多种方法来执行完全一样的操作。使用命令替换可以将任何命令或命令管道放在``或$()之间,并将其分配给环境变量。真方便!下面是一个例子,演示如何在命令替换中使用管道:
MYFILES=$(ls/etc|greppa)
bash-2.03$echo$MYFILES
pam.dpasswd
尽管basename和dirname是很好的工具,但有时可能需要执行更高级的字符串“截断”,而不只是标准的路径名操作。当需要更强的说服力时,可以利用bash内置的变量扩展功能。已经使用了类似于${MYVAR}的标准类型的变量扩展。但是bash自身也可以执行一些便利的字符串截断。看一下这些例子:
$MYVAR=foodforthought.jpg
$echo${MYVAR##*fo}
rthought.jpg
$echo${MYVAR#*fo}
odforthought.jpg
在第一个例子中,输入了${MYVAR##*fo}。它的确切含义是什么?基本上,在${}中输入环境变量名称,两个##,然后是通配符("*fo")。然后,bash取得MYVAR,找到从字符串"foodforthought.jpg"开始处开始、且匹配通配符"*fo"的最长子字符串,然后将其从字符串的开始处截去。刚开始理解时会有些困难,为了感受一下这个特殊的"##"选项如何工作,让我们一步步地看看bash如何完成这个扩展。首先,它从"foodforthought.jpg"的开始处搜索与"*fo"通配符匹配的子字符串。以下是检查到的子字符串:
f
foMATCHES*fo
foo
food
foodf
foodfoMATCHES*fo
foodfor
foodfort
foodforth
foodfortho
foodforthou
foodforthoug
foodforthought
foodforthought.j
foodforthought.jp
foodforthought.jpg
在搜索了匹配的字符串之后,可以看到bash找到两个匹配。它选择最长的匹配,从初始字符串的开始处除去,然后返回结果。
上面所示的第二个变量扩展形式看起来与第一个相同,但是它只使用一个"#"--并且bash执行几乎同样的过程。它查看与第一个例子相同的子字符串系列,但是bash从初始字符串除去最短的匹配,然后返回结果。所以,一查到"fo"子字符串,它就从字符串中除去"fo",然后返回"odforthought.jpg"。
这样说可能会令人十分困惑,下面以一简单方式记住这个功能。当搜索最长匹配时,使用##(因为##比#长)。当搜索最短匹配时,使用#。看,不难记吧!等一下,怎样记住应该使用'#'字符来从字符串开始部分除去?很简单!注意到了吗:在美国键盘上,shift-4是"$",它是bash变量扩展字符。在键盘上,紧靠"$"左边的是"#"。这样,可以看到:"#"位于"$"的“开始处”,因此(根据我们的记忆法),"#"从字符串的开始处除去字符。您可能要问:如何从字符串末尾除去字符。如果猜到我们使用美国键盘上紧靠"$"右边的字符("%),那就猜对了。这里有一些简单的例子,解释如何截去字符串的末尾部分:
$MYFOO="chickensoup.tar.gz"
$echo${MYFOO%%.*}
chickensoup
$echo${MYFOO%.*}
chickensoup.tar
正如您所见,除了将匹配通配符从字符串末尾除去之外,%和%%变量扩展选项与#和##的工作方式相同。请注意:如果要从末尾除去特定子字符串,不必使用"*"字符:
MYFOOD="chickensoup"
$echo${MYFOOD%%soup}
chicken
在此例中,使用"%%"或"%"并不重要,因为只能有一个匹配。还要记住:如果忘记了应该使用"#"还是"%",则看一下键盘上的3、4和5键,然后猜出来。
可以根据特定字符偏移和长度,使用另一种形式的变量扩展,来选择特定子字符串。试着在bash中输入以下行:
$EXCLAIM=cowabunga
$echo${EXCLAIM:0:3}
cow
$echo${EXCLAIM:3:7}
abunga
这种形式的字符串截断非常简便,只需用冒号分开来指定起始字符和子字符串长度。
现在我们已经学习了所有截断字符串的知识,下面写一个简单短小的shell脚本。我们的脚本将接受一个文件作为自变量,然后打印:该文件是否是一个tar文件。要确定它是否是tar文件,将在文件末尾查找模式".tar"。如下所示:
mytar.sh--一个简单的脚本
#!/bin/bash
if["${1##*.}"="tar"]
then
echoThisappearstobeatarball.
else
echoAtfirstglance,thisdoesnotappeartobeatarball.
fi
要运行此脚本,将它输入到文件mytar.sh中,然后输入"chmod755mytar.sh",生成可执行文件。然后,如下做一下tar文件试验:
$./mytar.shthisfile.tar
Thisappearstobeatarball.
$./mytar.shthatfile.gz
Atfirstglance,thisdoesnotappeartobeatarball.
好,成功运行,但是不太实用。在使它更实用之前,先看一下上面使用的"if"语句。语句中使用了一个布尔表达式。在bash中,"="比较运算符检查字符串是否相等。在bash中,所有布尔表达式都用方括号括起。但是布尔表达式实际上测试什么?让我们看一下左边。根据前面所学的字符串截断知识,"${1##*.}"将从环境变量"1"包含的字符串开始部分除去最长的"*."匹配,并返回结果。这将返回文件中最后一个"."之后的所有部分。显然,如果文件以".tar"结束,结果将是"tar",条件也为真。
您可能会想:开始处的"1"环境变量是什么。很简单--$1是传给脚本的第一个命令行自变量,$2是第二个,以此类推。好,已经回顾了功能,下面来初探"if"语句。
与大多数语言一样,bash有自己的条件形式。在使用时,要遵循以上格式;即,将"if"和"then"放在不同行,并使"else"和结束处必需的"fi"与它们水平对齐。这将使代码易于阅读和调试。除了"if,else"形式之外,还有其它形式的"if"语句:
if[condition]
then
action
fi
只有当condition为真时,该语句才执行操作,否则不执行操作,并继续执行"fi"之后的任何行。
if[condition]
then
action
elif[condition2]
then
action2
.
.
.
elif[condition3]
then
else
actionx
fi
以上"elif"形式将连续测试每个条件,并执行符合第一个真条件的操作。如果没有条件为真,则将执行"else"操作,如果有一个条件为真,则继续执行整个"if,elif,else"语句之后的行。
我们已经学习了最基本的bash功能,现在要加快脚步,准备编写一些实际脚本。在下一篇中,将讲述循环概念、函数、名称空间和其它重要主题。然后,将准备好编写一些更复杂的脚本。在第三篇中,将重点讲述一些非常复杂的脚本和功能,以及几个bash脚本设计选项。再见!
您可以参阅本文在developerWorks全球站点上的英文原文.
访问GNU'sbash主页
查看bashonlinereferencemanual