Shell 脚本
本节将介绍初级管理(LPIC-1)考试 102 主题 1.109.2 的内容。这个主题的权值为 3。
在本节中,您将学习如何:
- 使用标准的 shell 语法,例如循环和测试
- 使用命令替换
- 测试命令的成功、失败或其他返回值
- 向超级用户条件性地发送邮件
- 通过 #! 行选择正确的脚本解释器
- 管理脚本的位置、所有者、执行和 suid 权限
本节是在上一节中所学习的有关简单函数的基础知识上构建的,将展示增加 shell 编程能力的一些技术和工具。您已经看到使用 && 和 || 操作符的一些简单逻辑,它们让您可以根据前一个命令是正常退出还是错误退出来执行某个命令。在 ldirs 函数中,可以使用这种方法来根据是否向 ldirs 函数传递了参数来修改对 ls 的调用。现在您将学习如何扩展这些基本技术来进行更加复杂的 shell 编程。
测试
在学习如何为变量赋值和传递参数之后,在任何编程语言中接下来要做的第一件事情都是对这些值和参数进行测试。在 shell 中所做的测试会设置返回状态,这与其他命令的做法类似。实际上, test 是一个内嵌的命令!
test 和 [
test 内嵌命令会根据对表达式 expr 的计算结果来确定返回 0(True)或 1(False) 。也可以使用方括号,test expr 和 [ expr ] 是等效的。可以通过显示 $? 来检查返回值;或者使用本节后面介绍的各种条件结构来对它进行测试。
清单 28. 几个简单的测试例子
[ian@pinguino ~]$ test 3 -gt 4 && echo True || echo false
false
[ian@pinguino ~]$ [ "abc" != "def" ];echo $?
0
[ian@pinguino ~]$ test -d "$HOME" ;echo $?
0
|
在第一个例子中, -gt 操作符用来在两个数值之间进行数学比较运算。在第二个例子中, 使用了另外一种 [ ] 形式来比较两个字符串是否相等。在最后一个例子中,使用 -d 一元操作符对 HOME 变量的值进行测试,看它是否是一个目录。
数值可以使用 -eq、-ne、-lt、-le、-gt 或 -ge 进行比较,分别表示等于、不等于、小于、小于或等于、大于、大于或等于。
字符串可以使用操作符 =、!=、< 和 > 分别进行等于、不等于或第一个字符串是在第二个字符串之前还是之后的比较操作。一元操作符 -z 测试字符串是否为空;如果字符串不为空,那么 -n 或不使用任何操作符就返回 True。
注意: < 和 > 操作符也可以由 shell 用来进行重定向,因此必须使用 \< 或 \> 对它们进行转义。清单 29 给出了几个字符串测试的例子。请检查它们是否如您所期望的那样。
清单 29. 几个字符串测试的例子
[ian@pinguino ~]$ test "abc" = "def" ;echo $?
1
[ian@pinguino ~]$ [ "abc" != "def" ];echo $?
0
[ian@pinguino ~]$ [ "abc" \< "def" ];echo $?
0
[ian@pinguino ~]$ [ "abc" \> "def" ];echo $?
1
[ian@pinguino ~]$ [ "abc" \<"abc" ];echo $?
1
[ian@pinguino ~]$ [ "abc" \> "abc" ];echo $?
1
|
表 5 给出了几个常见的文件测试的例子。如果所测试的文件是一个系统中存在的文件,并且具有指定的特性,测试结果就是 True。
表 5. 几个文件测试的例子
操作符 |
特性 |
-d |
目录 |
-e |
存在(也可以使用 -a) |
-f |
普通文件 |
-h |
符号链接(也可以使用 -L) |
-p |
命名的管道 |
-r |
可以读取 |
-s |
非空 |
-S |
Socket |
-w |
可以写入 |
-N |
上次读取之后已经被修改过了 |
除了上面的一元测试之外,还可以使用表 6 中给出的二元操作符对两个文件进行比较。
表 6. 文件对测试
操作符 |
如果符合该条件就为 True |
-nt |
测试文件 1 是否比文件 2 更新。修改日期会在这个比较和下个比较中使用。 |
-ot |
测试文件 1 是否比文件 2 更旧。 |
-ef |
测试文件 1 是否是到文件 2 的硬链接。 |
其他几个测试允许检查诸如文件权限之类的事情。有关详细信息请参看手册页,也可以通过 help test 来查看有关内置测试的简单介绍。还可以使用 help 命令获得关于其他内置功能的信息。
-o 操作符允许测试不同的 shell 选项,这些选项可通过 set -o option 进行设置,如果选项已设置就返回 True (0),否则返回 False (1),如清单 30 所示。
清单 30. 测试 shell 选项
[ian@pinguino ~]$ set +o nounset
[ian@pinguino ~]$ [ -o nounset ];echo $?
1
[ian@pinguino ~]$ set -u
[ian@pinguino ~]$ test -o nounset; echo $?
0
|
最后,-a 和 -o 操作允许分别使用逻辑 AND 和 OR 来合并表达式,而一元操作符 ! 则是对测试含义取反。可以使用圆括号来对表达式进行分组,并覆盖默认的优先级顺序。记住 shell 通常会在一个子 shell 中运行表达式,因此需要使用 \( 和 \) 对圆括号进行转义,或者使用单引号或双引号将这些操作符围起来。清单 31 展示了 de Morgan 定律在表达式中的应用。
清单 31. 对测试进行合并和分组
[ian@pinguino ~]$ test "a" != "$HOME" -a 3 -ge 4 ; echo $?
1
[ian@pinguino ~]$ [ ! \( "a" = "$HOME" -o 3 -lt 4 \) ]; echo $?
1
[ian@pinguino ~]$ [ ! \( "a" = "$HOME" -o ‘(‘ 3 -lt 4 ‘)‘ ")" ]; echo $?
1
|
(( 和 [[
test 命令的功能非常强大,但是在对转义和字符串与数值比较之间的区别的处理上有些吃力。幸运的是,bash 有两种方法可以按照那些熟悉 C、C++ 或 Java 语法的人更加习惯的方式进行测试。 (( )) 复合命令 可以计算一个算术表达式的值,如果这个表达式的值为 0 就将退出状态设置为 1;如果表达式的值不为 0,就将退出状态设置为 0。不需要对 (( 和 )) 之间的操作符进行转义。数值计算是按照整型进行的。被除数为 0 会产生错误,但溢出不会产生错误。也可以执行在 C 语言中很常见的数值、逻辑和位操作。let 命令也可以执行一个或多个算术表达式。它通常用来对数值变量进行赋值。
清单 32. 对算术表达式进行赋值和测试
[ian@pinguino ~]$ let x=2 y=2**3 z=y*3;echo $? $x $y $z
0 2 8 24
[ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
0 3 8 16
[ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
0 4 8 13
|
与 (( )) 类似, [[ ]] 复合命令让您可以使用更加自然的语法进行文件名和字符串测试。可以使用圆括号和逻辑操作符组合 test 命令所允许的测试。
清单 33. 使用 [[ 进行组合
[ian@pinguino ~]$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]] &&
> echo "home is a writable directory"
home is a writable directory
|
在使用了 = 或 != 操作符时,[[ 也可以对字符串进行模式匹配,如清单 34 所示。
清单 34. 使用 [[ 进行通配符测试
[ian@pinguino ~]$ [[ "abc def .d,x--" == a[abc]*\ ?d* ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $?
1
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $?
1
|
甚至可以在 [[ 复合命令中进行数学测试,不过这要非常谨慎。除非是在 (( 复合命令内部,否则 < 和 > 操作符会将操作数当作字符串进行比较,并按照当前的比较序列的顺序测试其顺序。清单 35 给出了几个例子。
清单 35. 使用 [[ 进行数学测试
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || (( 3 > 2 )) ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 -gt 2 ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 > 2 ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a > 2 ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a -gt 2 ]]; echo $?
-bash: a: unbound variable
|
条件测试
可以使用上面的测试以及 && 和 || 控制操作符来完成很多编程,但 bash 还包括了大家更加熟悉的 “if, then, else” 和 case 结构。在学习这些内容之后,您将学习有关循环结构的内容,到那时您的工具箱就更丰富了。
If, then, else 语句
bash 的 if 命令是一个复合命令,它对测试或命令的返回值($? )进行测试,并根据该值是 True(0)还是 False(非 0)来进行分支跳转。尽管上面的测试只会返回 0 或 1,但是这些命令也可以返回其他值。在本教程稍后您将学习更多有关这种测试的内容。bash 中的 if 命令有一个 then 子句,其中包含了如果测试或命令返回 0 时要执行的命令列表;还可以包含一个或多个可选的 elif 子句,每个 elif 子句中都可以有另外一个测试和一个 then 子句,后者中列有相关的命令列表;最后,还可以包括一个可选的 else 子句以及一个命令列表,如果最初测试或 elif 子句中使用的测试都不为 true,并且后面有一个终止 fi 标记着结构的末尾,这些命令就会执行。
使用到现在为止所学习到的内容,就可以构建一个简单的计算器来计算数学表达式的值,如清单 36 所示。
清单 36. 使用 if, then, else 语句计算表达式
[ian@pinguino ~]$ function mycalc ()
> {
> local x
> if [ $# -lt 1 ]; then
> echo "This function evaluates arithmetic for you if you give it some"
> elif (( $* )); then
> let x="$*"
> echo "$* = $x"
> else
> echo "$* = 0 or is not an arithmetic expression"
> fi
> }
[ian@pinguino ~]$ mycalc 3 + 4
3 + 4 = 7
[ian@pinguino ~]$ mycalc 3 + 4**3
3 + 4**3 = 67
[ian@pinguino ~]$ mycalc 3 + (4**3 /2)
-bash: syntax error near unexpected token `(‘
[ian@pinguino ~]$ mycalc 3 + "(4**3 /2)"
3 + (4**3 /2) = 35
[ian@pinguino ~]$ mycalc xyz
xyz = 0 or is not an arithmetic expression
[ian@pinguino ~]$ mycalc xyz + 3 + "(4**3 /2)" + abc
xyz + 3 + (4**3 /2) + abc = 35
|
计算器使用 local 语句将 x 声明为本地变量,它只能在 mycalc 函数内部使用。 let 函数有几个可能的选项,与之密切相关的 declare 函数也是一样。请查看 bash 的手册页或使用 help let 来获得更多信息。
正如在清单 36 中所看到的一样,如果使用了 shell 元字符,例如 (、)、*、> 和 <,就需要仔细确保表达式进行了正确的转义。不管怎样,现在您有一个非常方便的计算器可以用来像 shell 一样计算数学表达式的值了。
您可能已经注意到 else 子句和最后两个例子了。正如您看到的一样,将 xyz 传递给 mycalc 并不是什么错误,但是这样得出的结果会是 0。这个函数现在还不够智能,因此还不能识别最后一个例子中的字符值并警告用户。可以使用字符串模式的匹配测试,例如 [[ ! ("$*" == *[a-zA-Z]* ]] (或适合您自己的语言环境的形式)来剔除包含字母字符的表达式,不过这也同时会使输入中不能再出现某些十六进制符号,因为您可能会使用十六进制的形式用 0x0f 来表示 15。实际上,shell 允许使用 64 进制(使用 base#value ),因此尽可以在输入中使用任何字母字符,外加 _ 和 @ 。八进制和十六进制使用常见的符号,对于八进制来说以 0 开头,对于十六进制来说以 0x 或 0X 开头。清单 37 给出了几个例子。
清单 37. 使用不同的进制进行计算
[ian@pinguino ~]$ mycalc 015
015 = 13
[ian@pinguino ~]$ mycalc 0xff
0xff = 255
[ian@pinguino ~]$ mycalc 29#37
29#37 = 94
[ian@pinguino ~]$ mycalc 64#1az
64#1az = 4771
[ian@pinguino ~]$ mycalc 64#1azA
64#1azA = 305380
[ian@pinguino ~]$ mycalc 64#1azA_@
64#1azA_@ = 1250840574
[ian@pinguino ~]$ mycalc 64#1az*64**3 + 64#A_@
64#1az*64**3 + 64#A_@ = 1250840574
|
关于输入的更详细的讨论已超出了本教程的范围,因此使用您的计算器时必须要相当仔细。
elif 语句非常方便,它通过简化缩进可以帮助您编写脚本。对mycalc 函数执行 type 命令的输出结果(如清单 38 所示)可能会出乎您的意料吧。
清单 38. Type mycalc
[ian@pinguino ~]$ type mycalc
mycalc is a function
mycalc ()
{
local x;
if [ $# -lt 1 ]; then
echo "This function evaluates arithmetic for you if you give it some";
else
if (( $* )); then
let x="$*";
echo "$* = $x";
else
echo "$* = 0 or is not an arithmetic expression";
fi;
fi
}
|
Case 语句
case 复合命令非常适合用在当具有很多可能性并且希望根据匹配某种特定可能性的值来采取相应动作的情况。case 复合命令是以 case WORD in 开始并以 esac ("case" 这个单词倒着拼)结束。每个 case 语句可以包含单个模式或者多个模式(使用 | 分隔开),后面是一个 ) 符号,然后是一个语句列表,最后是两个分号(;;)。
为了阐述这种用法,假设有一个商店供应咖啡、无咖啡因咖啡(decaf)、茶和冷饮。清单 39 中的函数可以用来针对顾客的不同要求做不同的响应。
清单 39. 使用 case 命令
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don‘t serve that here"
;;
esac
}
[ian@pinguino ~]$ myorder decaf
Hot coffee coming right up
[ian@pinguino ~]$ myorder tea
Hot tea on its way
[ian@pinguino ~]$ myorder milk
Sorry, we don‘t serve that here
|
注意使用 ‘*‘ 可以匹配之前尚未匹配的任何内容。
Bash 有另外一个类似于 case 的结构,它也可以用来将输出结果显示在终端上并让用户选择适当的项。它就是 select 语句,不过在这里不会对它进行过多的介绍。请参看 bash 的手册页,或者输入 help select 来学习更多内容。
当然,使用这种简单方法也有很多问题;比如无法一次购买两份饮料,这个函数也不能处理以小写形式输入的内容。那么能否采用大小写不敏感的匹配形式呢?答案是肯定的,下面就让我们看一下如何实现这种功能。
返回值
Bash shell 有一个内嵌的 shopt ,可以用来设置或取消很多 shell 选项。其中一个选项是 nocasematch ,如果这个选项设置了,就会通知 shell 忽略字符串匹配中的大小写。您的第一想法可能是使用在 test 命令中学习到的 -o 操作数。不幸的是, nocasematch 并不能应用 -o 选项,因此只能考虑其他方法。
shopt 命令与大部分 UNIX 和 Linux 命令一样,都会设置一个返回值,可以使用 $? 来查看这个返回值的内容。返回值不仅仅存在于您之前学习过的测试中,如果您仔细分析一下在 if 语句中进行的测试,就会发现它们实际上是在测试底层 test 命令的返回值是 True(0)还是 False(1 或其他非 0 值)。即使您不使用测试,而是使用其他命令,也是如此。返回值为 0 表示命令执行成功,返回值不为 0 表示命令执行失败。
了解了这些知识,您现在就可以测试 nocasematch 选项了,如果还没有设置这个选项,现在就先设置它,然后当您的函数结束时,将它返回到用户的首选项中。 shopt 命令有 4 个方便的选项: -pqsu ,分别用来打印当前值、不打印任何东西、设置选项或去除选项的设置。-p 和 -q 选项将返回值设置为 0,表示这个 shell 选项已经设置了;为 1 表示该选项没有设置。 -p 选项会打印将这个选项设置为当前值所需要的命令,而 -q 选项则简单地将返回值设置为 0 或 1。
修改后的函数使用 shopt 中的返回值来设置代表 nocasematch 选项的当前状态的本地变量、设置这个选项、运行 case 命令,然后再将 nocasematch 选项重置成原来的值。实现这种功能的一种方法如清单 40 所示。
清单 40. 测试命令的返回值
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
local restorecase;
if shopt -q nocasematch; then
restorecase="-s";
else
restorecase="-u";
shopt -s nocasematch;
fi;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don‘t serve that here"
;;
esac;
shopt $restorecase nocasematch
}
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
[ian@pinguino ~]$ # nocasematch is currently unset
[ian@pinguino ~]$ myorder DECAF
Hot coffee coming right up
[ian@pinguino ~]$ myorder Soda
Your ice-cold soda will be ready in a moment
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
[ian@pinguino ~]$ # nocasematch is unset again after running the myorder function
|
如果您希望自己的函数(或脚本)返回一个其他函数或命令可以测试的值,就请在自己的函数中使用 return 语句。清单 41 显示了如何实现当顾客购买所能提供的饮料时返回 0,当顾客请求购买其他东西就返回 1。
清单 41. 设置函数的返回值
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
local restorecase=$(shopt -p nocasematch) rc=0;
shopt -s nocasematch;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don‘t serve that here";
rc=1
;;
esac;
$restorecase;
return $rc
}
[ian@pinguino ~]$ myorder coffee;echo $?
Hot coffee coming right up
0
[ian@pinguino ~]$ myorder milk;echo $?
Sorry, we don‘t serve that here
1
|
如果没有指定自己的返回值,那么返回值就是最后一个命令执行的结果。函数总是习惯在您意想不到的情况下被重用,因此最好设置自己的返回值。
命令也可以返回 0 和 1 之外的值,有时需要对此进行区分。例如,如果找到可匹配模式,grep 命令就返回 0;否则就返回 1;但是如果模式无效或该文件规范并不能匹配任何文件,就会返回 2。如果需要区分除成功(0)或失败(非 0)之外的更多的返回值,可能就需要使用 case 命令,也可以使用带有多个 elif 的 if 命令。
命令替换
在 “LPI 101 考试准备(主题 103):GNU 和 UNIX 命令” 教程中您已经见到过命令替换的用法了,不过下面让我们快速回顾一下相关内容。
命令替换让您可以通过简单地在命令两边加上一个 $( 和 ) 或使用一对反单引号 ` 来将一个命令的输出结果作为另外一个命令的输入使用。如果希望嵌套地使用一个命令的输出结果作为生成最终结果的另外一个命令的一部分,就会发现 $() 格式的优点;它也使得确定要执行什么操作变得更加简单,因为圆括号区分左、右,而两边的反单引号是完全相同的。当然,选择权在您手里,反单引号也很常见。
您通常都会在循环(在下面的 循环 一节中进行介绍)中使用命令替换功能。也可以使用它来简化刚才创建的 myorder 函数。由于 shopt -p nocasematch 会打印出用来将 nocasematch 选项设置为其当前值的命令,因此只需要保存输出结果并在 case 语句的结尾执行它即可。不管您对它进行了修改与否,这都会恢复 nocasematch 选项。修订后的函数如清单 42 所示。您可以自己尝试一下。
清单 42. 使用命令替换而不是返回值测试
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
local restorecase=$(shopt -p nocasematch) rc=0;
shopt -s nocasematch;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don‘t serve that here"
rc=1
;;
esac;
$restorecase
return $rc
}
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
[ian@pinguino ~]$ myorder DECAF
Hot coffee coming right up
[ian@pinguino ~]$ myorder TeA
Hot tea on its way
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
|
调试
如果您自己输入了一些函数,并在输入时出现一些错误,您可能会纳闷究竟是什么地方出现了问题,您也可能会非常想弄清楚该如何对函数进行调试。幸运的是,shell 允许您设置 -x 选项来在 shell 执行函数的同时对命令及其参数进行跟踪。清单 43 显示了这对于清单 42 给出的 myorder 函数来说是如何工作的。
清单 43. 跟踪函数执行
[ian@pinguino ~]$ set -x
++ echo -ne ‘\033]0;ian@pinguino:~‘
[ian@pinguino ~]$ myorder tea
+ myorder tea
++ shopt -p nocasematch
+ local ‘restorecase=shopt -u nocasematch‘ rc=0
+ shopt -s nocasematch
+ case "$*" in
+ echo ‘Hot tea on its way‘
Hot tea on its way
+ shopt -u nocasematch
+ return 0
++ echo -ne ‘\033]0;ian@pinguino:~‘
[ian@pinguino ~]$ set +x
+ set +x
|
对于别名、函数或脚本都可以使用这种技术。如果需要更多信息,可以添加 -v 选项进行更详细的输出。
循环
Bash 和其他 shell 都有一些循环结构,这与 C 语言使用的循环结构非常类似。每个循环都会执行一个命令列表 零次到多次。命令列表使用单词 do 和 done 包含起来,其中每条语句前面都有一个分号。
- for
- 循环有两种形式。shell 脚本编程中最常见的形式是对一组值进行迭代,对每个值都执行命令列表一次。这组值可能为空,在这种情况下命令列表就不会被执行。另外一种形式更加类似于传统的 C for 循环,使用 3 个数学表达式来控制循环的起始条件、步进函数和结束条件。
- while
- 循环每次都在循环开始时计算一个条件的值,如果这个条件为 true,就执行命令列表。如果这个条件最初不为 true,那么这些命令就永远都不会执行。
- until
- 循环执行一个命令列表,并在每个循环结束时计算某个条件的值。如果这个条件为 true,就再次执行这个循环。即使条件最初不为 true,这些命令也会至少被执行一次。
如果所测试的条件是一系列命令,那么所使用的就是最后执行的命令的返回值。清单 44 给出了循环命令的例子。
清单 44. For、while 和 until 循环
[ian@pinguino ~]$ for x in abd 2 "my stuff"; do echo $x; done
abd
2
my stuff
[ian@pinguino ~]$ for (( x=2; x<5; x++ )); do echo $x; done
2
3
4
[ian@pinguino ~]$ let x=3; while [ $x -ge 0 ] ; do echo $x ;let x--;done
3
2
1
0
[ian@pinguino ~]$ let x=3; until echo -e "x=\c"; (( x-- == 0 )) ; do echo $x ; done
x=2
x=1
x=0
|
尽管这些例子都是假想的,但它们的确揭示了一些重要的概念。通常您希望能迭代传递到函数或 shell 脚本中的参数,或命令替换所创建的列表。之前您已经发现,shell 可以引用以 $* 或 $@ 传递的参数列表,还发现是否使用引号包括这些表达式会对如何解释它们造成影响。清单 45 给出了一个函数,它首先打印参数个数,然后根据 4 种选择来打印参数的内容。清单 46 给出了这个函数执行时的情况,其中为了函数执行而在 IFS 变量前面多加了一个字符。
清单 45. 打印参数信息的函数
[ian@pinguino ~]$ type testfunc
testfunc is a function
testfunc ()
{
echo "$# parameters";
echo Using ‘$*‘;
for p in $*;
do
echo "[$p]";
done;
echo Using ‘"$*"‘;
for p in "$*";
do
echo "[$p]";
done;
echo Using ‘$@‘;
for p in $@;
do
echo "[$p]";
done;
echo Using ‘"$@"‘;
for p in "$@";
do
echo "[$p]";
done
}
|
清单 46. 使用 testfunc 打印参数信息
[ian@pinguino ~]$ IFS="|${IFS}" testfunc abc "a bc" "1 2
> 3"
3 parameters
Using $*
[abc]
[a]
[bc]
[1]
[2]
[3]
Using "$*"
[abc|a bc|1 2
3]
Using $@
[abc]
[a]
[bc]
[1]
[2]
[3]
Using "$@"
[abc]
[a bc]
[1 2
3]
|
我们需要仔细学习它们之间的差异,尤其是对引号中的格式和包括诸如空格和换行符之类的参数的用法。
Break 和 continue
break 命令让您可以从一个循环中立即退出。如果使用了嵌套循环,也可以指定退出的层次数。因此如果在 for 循环中有一个 until 循环,而这个 for 循环在另外一个 for 循环之中,所有这些循环又全部在一个 while 循环中,那么 break 3 就会立即结束 until 循环和 2 个 for 循环,并将控制权返回给 while 循环中的代码。
continue 语句可以跳过命令列表中的剩下的语句,直接跳转到下一次循环的开头。
清单 47. 使用 break 和 continue
[ian@pinguino ~]$ for word in red blue green yellow violet; do
> if [ "$word" = blue ]; then continue; fi
> if [ "$word" = yellow ]; then break; fi
> echo "$word"
> done
red
green
|
再访 ldirs
还记得为了让 ldirs 函数能够从一个长列表中提取出文件名并确定它是否是一个目录,我们做了多少工作吗?您开发的那个最终函数还算不错,不过现在您掌握了本教程中的所有信息之后,还会创建相同的函数吗?也许就不会了。现在您知道如何使用 [ -d $name ] 来测试一个名字是否是目录了,并且了解了 for 循环的用法。清单 48 给出了可以编写 ldirs 函数的另外一种方法。
清单 48. 实现 ldirs 的另外一种方法
[ian@pinguino developerworks]$ type ldirs
ldirs is a function
ldirs ()
{
if [ $# -gt 0 ]; then
for file in "$@";
do
[ -d "$file" ] && echo "$file";
done;
else
for file in *;
do
[ -d "$file" ] && echo "$file";
done;
fi;
return 0
}
[ian@pinguino developerworks]$ ldirs
my dw article
my-tutorial
readme
schema
tools
web
xsl
[ian@pinguino developerworks]$ ldirs *s* tools/*
schema
tools
xsl
tools/java
[ian@pinguino developerworks]$ ldirs *www*
[ian@pinguino developerworks]$
|
您会注意到如何没有目录可以匹配您给出的条件,这个函数就会安静地返回。这也许符合您的预期,也许并不符合,不过这个函数可能比使用 sed 解析 ls 命令输出的那个版本更容易理解。至少现在您的工具箱中又多了一个工具。
创建脚本
您可能还记得 myorder 一次只能处理一份饮料。现在可以使用一个 for 循环对这个单一饮料的函数进行组合,从而对参数进行迭代来处理多份饮料。这非常简单,就像是将您的函数放到一个文件中,并添加一些 for 指令一样。清单 49 给出了新 myorder.sh 脚本的内容。
清单 49. 购买多份饮料
[ian@pinguino ~]$ cat myorder.sh
function myorder ()
{
local restorecase=$(shopt -p nocasematch) rc=0;
shopt -s nocasematch;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don‘t serve that here";
rc=1
;;
esac;
$restorecase;
return $rc
}
for file in "$@"; do myorder "$file"; done
[ian@pinguino ~]$ . myorder.sh coffee tea "milk shake"
Hot coffee coming right up
Hot tea on its way
Sorry, we don‘t serve that here
|
注意这个脚本使用了 . 命令来将其引用 到当前 shell 环境中运行,而不是在它自己的 shell 中运行。为了能够执行脚本,可以引用它,也可以使用 chmod -x 命令将这个脚本标记成是可执行的,如清单 50 所示。
清单 50. 将脚本标记成可执行的
[ian@pinguino ~]$ chmod +x myorder.sh
[ian@pinguino ~]$ ./myorder.sh coffee tea "milk shake"
Hot coffee coming right up
Hot tea on its way
Sorry, we don‘t serve that here
|
指定 shell
拥有了一个全新的 shell 脚本之后,您可能会问这个脚本是否在所有的 shell 中都能很好地工作。清单 51 给出了相同的 shell 脚本在 Ubuntu 系统上首先使用 bash shell 然后再使用 dash shell 执行时的情况。
清单 51. Shell 的区别
ian@attic4:~$ ./myorder tea soda
-bash: ./myorder: No such file or directory
ian@attic4:~$ ./myorder.sh tea soda
Hot tea on its way
Your ice-cold soda will be ready in a moment
ian@attic4:~$ dash
$ ./myorder.sh tea soda
./myorder.sh: 1: Syntax error: "(" unexpected
|
这可不太好。
记得我们在前面曾经说过单词 “function” 在 bash 函数定义中是可选的,但它并不是 POSIX shell 规范的一部分吗?与 bash 相比,dash 更小更轻,它并不支持这种可选特性。由于无法确保用户可能会喜欢使用哪种 shell,因此应该总要确保脚本可以移植到所有 shell 环境中,这可能会非常困难;也可以使用所谓的 shebang(#!)方法来指定自%E |