做者:Daniel Robbins 来自:IBM DW中国 html
咱们先看一下处理命令行自变量的简单技巧,而后再看看 bash基本编程结构。linux
在 介绍性文章 中的样本程序中,咱们使用环境变量 "$1"来引用第一个命令行自变量。相似地,可使用 "$2"、"$3"等来引用传递给脚本的第二和第三个自变量。这里有一个例子:shell
#!/usr/bin/env bash echo name of script is $0 echo first argument is $1 echo second argument is $2 echo seventeenth argument is $17 echo number of arguments is $#
除如下两个细节以外,此例无需说明。第一,"$0"将扩展成从命令行调用的脚本名称,"$#"将扩展成传递给脚本的自变量数目。试验以上脚本,经过传递不一样类型的命令行自变量来了解其工做原理。编程
有时须要一次引用 全部 命令行自变量。针对这种用途,bash实现了变量 "$@",它扩展成全部用空格分开的命令行参数。在本文稍后的"for" 循环部分中,您将看到使用该变量的例子。bash
若是您曾用过如 C、Pascal、Python 或 Perl那样的过程语言编程,则必定熟悉 "if" 语句和 "for"循环那样的标准编程结构。对于这些标准结构的大多数,Bash有本身的版本。在下几节中,将介绍几种 bash结构,并演示这些结构和您已经熟悉的其它编程语言中结构的差别。若是之前编程很少,也没必要担忧。我提供了足够的信息和示例,使您能够跟上本文的进度。编程语言
若是您曾用 C编写过与文件相关的代码,则应该知道:要比较特定文件是否比另外一个文件新须要大量工做。那是由于C 没有任何内置语法来进行这种比较,必须使用两个 stat() 调用和两个stat 结构来进行手工比较。相反,bash内置了标准文件比较运算符,所以,肯定“/tmp/myfile是否可读”与查看“$myvar 是否大于 4”同样容易。函数
下表列出最经常使用的 bash比较运算符。同时还有如何正确使用每一选项的示例。示例要跟在 "if"以后。例如:学习
if [ -z "$myvar" ] then echo "myvar is not defined" fi
运算符 | 描述 | 示例 |
---|---|---|
文件比较运算符 | ||
-e filename | 若是 filename存在,则为真 | [ -e /var/log/syslog ] |
-d filename | 若是 filename为目录,则为真 | [ -d /tmp/mydir ] |
-f filename | 若是 filename为常规文件,则为真 | [ -f /usr/bin/grep ] |
-L filename | 若是 filename为符号连接,则为真 | [ -L /usr/bin/grep ] |
-r filename | 若是 filename可读,则为真 | [ -r /var/log/syslog ] |
-w filename | 若是 filename可写,则为真 | [ -w /var/mytmp.txt ] |
-x filename | 若是 filename可执行,则为真 | [ -L /usr/bin/grep ] |
filename1-nt filename2 | 若是 filename1比 filename2新,则为真 | [ /tmp/install/etc/services -nt /etc/services ] |
filename1-ot filename2 | 若是 filename1比 filename2旧,则为真 | [ /boot/bzImage -ot arch/i386/boot/bzImage ] |
字符串比较运算符 (请注意引号的使用,这是防止空格扰乱代码的好方法) | ||
-z string | 若是 string长度为零,则为真 | [ -z "$myvar" ] |
-n string | 若是 string长度非零,则为真 | [ -n "$myvar" ] |
string1= string2 | 若是 string1与 string2相同,则为真 | [ "$myvar" = "one two three" ] |
string1!= string2 | 若是 string1与 string2不一样,则为真 | [ "$myvar" != "one two three" ] |
算术比较运算符 | ||
num1-eq num2 | 等于 | [ 3 -eq $mynum ] |
num1-ne num2 | 不等于 | [ 3 -ne $mynum ] |
num1-lt num2 | 小于 | [ 3 -lt $mynum ] |
num1-le num2 | 小于或等于 | [ 3 -le $mynum ] |
num1-gt num2 | 大于 | [ 3 -gt $mynum ] |
num1-ge num2 | 大于或等于 | [ 3 -ge $mynum ] |
有时,有几种不一样方法来进行特定比较。例如,如下两个代码段的功能相同:命令行
if [ "$myvar" -eq 3 ] then echo "myvar equals 3" fi if [ "$myvar" = "3" ] then echo "myvar equals 3" fi
上面两个比较执行相同的功能,可是第一个使用算术比较运算符,而第二个使用字符串比较运算符。设计
大多数时候,虽然能够不使用括起字符串和字符串变量的双引号,但这并非好主意。为何呢?由于若是环境变量中恰巧有一个空格或制表键,bash将没法分辨,从而没法正常工做。这里有一个错误的比较示例:
if [ $myvar = "foo bar oni" ] then echo "yes" fi
在上例中,若是 myvar 等于"foo",则代码将按预想工做,不进行打印。可是,若是 myvar 等于 "foobar oni",则代码将因如下错误失败:
[: too many arguments
在这种状况下,"$myvar"(等于 "foo bar oni")中的空格迷惑了bash。bash 扩展 "$myvar" 以后,代码以下:
[ foo bar oni = "foo bar oni" ]
由于环境变量没放在双引号中,因此 bash认为方括号中的自变量过多。能够用双引号将字符串自变量括起来消除该问题。请记住,若是养成将全部字符串自变量用双引号括起的习惯,将除去不少相似的编程错误。"foobar oni" 比较 应该写成:
if [ "$myvar" = "foo bar oni" ] then echo "yes" fi
若是要扩展环境变量,则必须将它们用 双引号、而不是单引号括起。单引号 禁用 变量(和历史)扩展。
以上代码将按预想工做,而不会有任何使人不快的意外出现。
好了,已经讲了条件语句,下面该探索 bash 循环结构了。咱们将从标准的"for" 循环开始。这里有一个简单的例子:
#!/usr/bin/env bash for x in one two three four do echo number $x done 输出: number one number two number three number four
发生了什么?"for" 循环中的 "for x" 部分定义了一个名为 "$x"的新环境变量(也称为循环控制变量),它的值被依次设置为"one"、"two"、"three" 和"four"。每一次赋值以后,执行一次循环体("do" 和 "done"之间的代码)。在循环体内,象其它环境变量同样,使用标准的变量扩展语法来引用循环控制变量"$x"。还要注意,"for" 循环老是接收 "in"语句以后的某种类型的字列表。在本例中,指定了四个英语单词,可是字列表也能够引用磁盘上的文件,甚至文件通配符。看看下面的例子,该例演示如何使用标准shell 通配符:
#!/usr/bin/env bash for myfile in /etc/r* do if [ -d "$myfile" ] then echo "$myfile (dir)" else echo "$myfile" fi done 输出: /etc/rc.d (dir) /etc/resolv.conf /etc/resolv.conf~ /etc/rpc
以上代码列出在 /etc 中每一个以 "r" 开头的文件。要作到这点,bash在执行循环以前首先取得通配符 /etc/r*,而后扩展它,用字符串/etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc替换。一旦进入循环,根据 myfile 是否为目录,"-d"条件运算符用来执行两个不一样操做。若是是目录,则将 "(dir)"附加到输出行。
还能够在字列表中使用多个通配符、甚至是环境变量:
for x in /etc/r--? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/* do cp $x /mnt/mydir done
Bash将在全部正确位置上执行通配符和环境变量扩展,并可能建立一个很是长的字列表。
虽然全部通配符扩展现例使用了 绝对路径,但也可使用相对路径,以下所示:
for x in ../* mystuff/* do echo $x is a silly file done
在上例中,bash相对于当前工做目录执行通配符扩展,就象在命令行中使用相对路径同样。研究一下通配符扩展。您将注意到,若是在通配符中使用绝对路径,bash将通配符扩展成一个绝对路径列表。不然,bash将在后面的字列表中使用相对路径。若是只引用当前工做目录中的文件(例如,若是输入"for x in*"),则产生的文件列表将没有路径信息的前缀。请记住,可使用"basename" 可执行程序来除去前面的路径信息,以下所示:
for x in /var/log/* do echo `basename $x` is a file living in /var/log done
固然,在脚本的命令行自变量上执行循环一般很方便。这里有一个如何使用本文开始提到的"$@" 变量的例子:
#!/usr/bin/env bash for thing in "$@" do echo you typed ${thing}. done 输出: $ allargs hello there you silly you typed hello. you typed there. you typed you. you typed silly.
在学习另外一类型的循环结构以前,最好先熟悉如何执行 shell算术。是的,确实如此:可使用 shell结构来执行简单的整数运算。只需将特定的算术表达式用 "$((" 和 "))"括起,bash 就能够计算表达式。这里有一些例子:
$ echo $(( 100 / 3 )) 33 $ myvar="56" $ echo $(( $myvar + 12 )) 68 $ echo $(( $myvar - $myvar )) 0 $ myvar=$(( $myvar + 1 )) $ echo $myvar 57
只要特定条件为真,"while" 语句就会执行,其格式以下:
while [ condition ] do statements done
一般使用 "While" 语句来循环必定次数,好比,下例将循环 10 次:
myvar=0 while [ $myvar -ne 10 ] do echo $myvar myvar=$(( $myvar + 1 )) done
能够看到,上例使用了算术表达式来使条件最终为假,并致使循环终止。
"Until" 语句提供了与 "while"语句相反的功能:只要特定条件为 假 ,它们就重复。下面是一个与前面的"while" 循环具备同等功能的 "until" 循环:
myvar=0 until [ $myvar -eq 10 ] do echo $myvar myvar=$(( $myvar + 1 )) done
Case 语句是另外一种便利的条件结构。这里有一个示例片断:
case "${x##*.}" in gz) gzunpack ${SROOT}/${x} ;; bz2) bz2unpack ${SROOT}/${x} ;; *) echo "Archive format not recognized." exit ;; esac
在上例中,bash 首先扩展 "${x##*.}"。在代码中,"$x"是文件的名称,"${x##.*}"除去文件中最后句点后文本以外的全部文本。而后,bash 将产生的字符串与")" 左边列出的值作比较。在本例中,"${x##.}" 先与 "gz" 比较,而后是"bz2",最后是 ""。若是 "${x##.}"与这些字符串或模式中的任何一个匹配,则执行紧接 ")" 以后的行,直到";;" 为止,而后 bash 继续执行结束符 "esac"以后的行。若是不匹配任何模式或字符串,则不执行任何代码行,在这个特殊的代码片断中,至少要执行一个代码块,由于任何不与"gz" 或 "bz2" 匹配的字符串都将与 "" 模式匹配。
在 bash 中,甚至能够定义与其它过程语言(如 Pascal 和C)相似的函数。在 bash中,函数甚至可使用与脚本接收命令行自变量相似的方式来接收自变量。让咱们看一下样本函数定义,而后再从那里继续:
tarview() { echo -n "Displaying contents of $1 " if [ ${1##*.} = tar ] then echo "(uncompressed tar)" tar tvf $1 elif [ ${1##*.} = gz ] then echo "(gzip-compressed tar)" tar tzvf $1 elif [ ${1##*.} = bz2 ] then echo "(bzip2-compressed tar)" cat $1 | bzip2 -d | tar tvf - fi }
可使用 "case" 语句来编写上面的代码。您知道如何编写吗?
咱们在上面定义了一个名为 "tarview"的函数,它接收一个自变量,即某种类型的 tar文件。在执行该函数时,它肯定自变量是哪一种 tar文件类型(未压缩的、gzip 压缩的或 bzip2压缩的),打印一行信息性消息,而后显示 tar文件的内容。应该以下调用上面的函数(在输入、粘贴或找到该函数后,从脚本或命令行调用它):
$ tarview shorten.tar.gz Displaying contents of shorten.tar.gz (gzip-compressed tar) drwxr-xr-x ajr/abbot 0 1999-02-27 16:17 shorten-2.3a/ -rw-r--r-- ajr/abbot 1143 1997-09-04 04:06 shorten-2.3a/Makefile -rw-r--r-- ajr/abbot 1199 1996-02-04 12:24 shorten-2.3a/INSTALL -rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE ....
别忘了,能够将函数(如上面的函数)放在 ~/.bashrc 或 ~/.bash_profile中,以便在 bash 中随时使用它们。
如您所见,可使用与引用命令行自变量一样的机制来在函数定义内部引用自变量。另外,将把"$#" 宏扩展成包含自变量的数目。惟一可能不彻底相同的是变量"$0",它将扩展成字符串 "bash"(若是从 shell交互运行函数)或调用函数的脚本名称。
常常须要在函数中建立环境变量。虽然有可能,可是还有一个技术细节应该了解。在大多数编译语言(如C)中,当在函数内部建立变量时,变量被放置在单独的局部名称空间中。所以,若是在C 中定义一个名为 myfunction 的函数,并在该函数中定义一个名为 "x"的自变量,则任何名为 "x"的全局变量(函数以外的变量)将不受它的印象,从而消除了负做用。
在 C 中是这样,但在 bash 中却不是。在 bash中,每当在函数内部建立环境变量,就将其添加到 全局名称空间。这意味着,该变量将重写函数以外的全局变量,并在函数退出以后继续存在:
#!/usr/bin/env bash myvar="hello" myfunc() { myvar="one two three" for x in $myvar do echo $x done } myfunc echo $myvar $x
运行此脚本时,它将输出 "one two threethree",这显示了在函数中定义的 "$myvar" 如何影响全局变量"$myvar",以及循环控制变量 "$x" 如何在函数退出以后继续存在(若是"$x" 全局变量存在,也将受到影响)。
在这个简单的例子中,很容易找到该错误,并经过使用其它变量名来改正错误。但这不是正确的方法,解决此问题的最好方法是经过使用"local" 命令,在一开始就预防影响全局变量的可能性。当使用 "local"在函数内部建立变量时,将把它们放在 局部名称空间中,而且不会影响任何全局变量。这里演示了如何实现上述代码,以便不重写全局变量:
#!/usr/bin/env bash myvar="hello" myfunc() { local x local myvar="one two three" for x in $myvar do echo $x done } myfunc echo $myvar $x
此函数将输出 "hello" -- 不重写全局变量 "$myvar","$x" 在 myfunc以外不继续存在。在函数的第一行,咱们建立了之后要使用的局部变量x,而在第二个例子 (local myvar="one two three"")中,咱们建立了局部变量myvar, 同时 为其赋值。在将循环控制变量定义为局部变量时,使用第一种形式很方便,由于不容许说:"forlocal x in$myvar"。此函数不影响任何全局变量,鼓励您用这种方式设计全部的函数。只有在明确但愿要修改全局变量时,才 不应该使用 "local"。
咱们已经学习了最基本的 bash 功能,如今要看一下如何基于 bash开发整个应用程序。下一部分正要讲到。再见!
参考资料