第27章:流程控制:if 分支
在上章中,我们遇到了一个问题。如何能使得我们的报表生成脚本能适应运行脚本的用户权限?该问题的解决方案要求我们在脚本中找到一个基于测试结果的「改变方向」的方法。在编程术语中叫做我们需要编程分支(branch)。
让我们考虑一个关于逻辑表达的伪代码(pseudocode)示例,一段便于人类理解的对计算机语言的模拟。
这是一个分支的示例。基于对「x 是否等于 5?」这个条件做一件事「说 x 等于 5」,否则做另一件事「说 x 不等于 5」。
if
使用 shell,我们可以将上面这个逻辑写成下面的代码:
或者,我们可以直接在命令行中输入(稍微短一点)。
上例中,我们执行了两次命令;一次,设置 x 的值为 5,输出字符串 "equals 5",第二次设置 x 的值为 0,输出字符串 "does not equals 5"。
if
语句的句法如下:
其中 commands
是一个命令列表。初看之下会有点困惑。但是在我们搞清楚前,有必要来看下 shell 是如何评估一个命令的成功或失败的。
退出状态
命令(包含脚本和我们所写的 shell 函数)在其结束时会提交一个值给系统,叫作退出状态(exit status)。这个值,是一个从 0 到 255 之间的一个整数,指示了该命令的执行是成功还是失败。shell 提供了一个参数使得我们可以检测退出状态。来看一下:
上面这个例子中,我们执行了两次 ls
命令。第一次,命令执行成功。如果显示参数 $?
的值,可以看到是 0
。第二次执行 ls
命令时(指定了成儿不存在的目录),产生了一个错误,再次检查参数 $?
。这次它包含了一个 2
,指示命令遇到了一个错误。一些命令使用不同的退出状态值以提供错误诊断信息,有些命令则仅仅简单地在发生错误时给出一个 1
。手册页通常会包含一个名为「退出状态」的章节,描述了使用什么代码。不过,0
总是指示成功。
shell 提供了两个最最简单的内建命令,除了以 0
或 1
的退出状态结束外,什么也不做。true
命令总是执行成功,而 false
命令总是执行不成功。
我们可以用这些命令来看 if
语句是如何工作的。if
语句真正做的,是评估命令的成功或失败。
命令 echo "It's true."
在 if
后的命令成功执行后被执行,而当在 if
后的命令没有被成功执行时,不被执行。如果有一个命令列表在 if
之后的话,评估的是列表中最后的一个命令:
测试
目前为止,if
最常用的命令就是 test
。test
命令执行各种检查和比较。有两种等效的形式。第一种如下:
test expression
第二种则更通用,如下:
[ expression ]
expression 是一个被用来评估真假的表达式。当表达式为真时,test
命令返回一个为 0
的退出状态,为假则返回一个为 1
的退出状态。
有趣的是,要注意 test
和 [
实际上都是命令。它们是 bash
中的内建命令,但是也存在于 /usr/bin
中以供其它 shell 使用。表达式只不过是其参数,对于 [
命令,则要求 ]
字符为其最后一个参数。
test
和 [
命令支持广泛有用的表达式和测试。
文件表达式
表 27-1 列出了用来评估文件状态的表达式。
表 27-1: test
文件表达式
表达式
如果……为真
file1 -ef file2
file1
和 file2
具有相同的 inode 编号(两个文件名通过硬链接指向同一个文件)。
file1 -nt file2
file1
比 file2
更新。
file1 -ot file2
file1
比 file2
更旧。
-b file
file
存在,且是一个特殊块(设备)文件。
-c file
file
存在,且是一个特殊字符(设备)文件。
-d file
file
存在,且是一个目录。
-e file
file
存在。
-f file
file
存在,且是一个常规文件。
-g file
file
存在,且设置了组 ID。
-G file
file
存在,且属于一个有效的组 ID。
-k file
file
存在,且有其「粘性位」设置。
-L file
file
存在,且是一个符号链接。
-O file
file
存在,且属于一个有效的用户 ID。
-p file
file
存在,且是一个命名管道。
-r file
file
存在,且可读(对有效用户有可读权限)。
-s file
file
存在,且文件长度大于 0。
-S file
file
存在,且是一个网络套接字。
-t fd
fd
是发往和来自终端的文件描述符。
-u file
file
存在,且设置了用户 ID。
-w file
file
存在,且可写(对有效用户有可写权限)。
-x file
file
存在,且可执行(对有效用户有可执行和检索权限)。
这里有一个脚本可以演示一些文件表达式:
上面这个脚本测试了分配到常量 FILE
的文件,并在执行后显示了测试结果。脚本中有两处有趣的地方需要注意。首先,注意参数 $FILE
在表达式中是如何被引用的。这在完成表达式的句法上不是必须的,然而这可以防止空参数或仅包含空白字符的参数。如果 $FILE
的参数扩展导致一个空值,则会引发一个错误(操作符会被解释为非空字符串,而非操作符)。在参数两边使用引号,保证了操作符总是跟随着一个字符串,即便是空字符串。其次,注意脚本末尾的 exit
命令的存在。exit
命令接收单个的可选参数,使其称为脚本的退出状态。当没有传入参数时,退出状态默认就是最后被执行的命令的退出状态。用这种方式使用 exit
允许脚本在如若 $FILE
扩展为一个不存在的文件名的情况下指示错误。出现在脚本最后一行的 exit
命令则是作为一种手续。当一个脚本「运行到最后」(到达文件末尾),会以最后一个被执行的命令的退出状态终结。
类似的,shell 函数可以通过包含一个整型参数到 return
命令中,以返回一个退出状态。如果我们将之前的脚本转换为一个 shell 函数,以嵌入到一个更大的程序中,我们可以用 return
语句替换 exit
命令,并得到期望的行为。
字符串表达式
表 27-2 列出了用来测试字符串的表达式:
表 27-2:test
字符表达式
表达式
如果……为真
string
string
非空。
-n string
string
的长度大于 0。
-z string
string
的长度为 0。
string1 = string2
string1 == string2
string1
和 string2
相等。可以使用单个或两个等于符号。两个等于号得到 bash
的支持,并是常用写法,但却不符合 POSIX。
string1 != string2
string1
和 string2
不相等。
string1 > string2
在排序上,string1
后于 string2
。
string1 < string2
在排序上,string1
先于 string2
。
警告:
>
和<
表达式操作符用在test
时,必须用引号(或者用反斜杠转义)。如果没有的话,就会被 shell 当作重定向符,从而中止脚本,产生潜在的破坏性结果。同时还要注意, 尽管bash
文档指出排列顺序符合当前语言环境的排列顺序,但事实并非如此。 ASCII(POSIX)顺序用于bash
4.0 及以下版本。 此问题已在版本 4.1 中修复。
下面是一个合并了字符串表达式的脚本:
脚本中,我们测试常量 ANSWER
。首先确定字符串是否为空。如果是,就终结脚本,设置退出状态为 1
。注意应用到 echo
命令中的重定向。它将错误信息 "There is no answer." 重定向到标准错误,这是对错误信息的正确处理。如果字符串非空,我们会评测字符串的值,看是等于 "yes"、"no" 还是 "maybe"。我们用 elif
来做这个,意思是 "else if"。用 elif
,我们可以构建更复杂的逻辑测试。
整数表达式
要比较整数的值,而非字符串的值,我们可以用表 27-3 中所列的表达式。
表 27-3:test
整数表达式
表达式
如果……为真
integer1 -eq integer2
integer1
等于 integer2
。
integer1 -ne integer2
integer1
不等于 integer2
。
integer1 -le integer2
integer1
小于等于 integer2
。
integer1 -lt integer2
integer1
小于 integer2
。
integer1 -ge integer2
integer1
大于等于 integer2
。
integer1 -gt integer2
integer1
大于 integer2
。
下面是一个演示脚本:
脚本中有趣的是如何判断一个整数是奇数还是偶数的部分。通过对一个数求对 2
的模,就是被 2
除后返回余数,就可以知道该数的奇偶性。
一个更现代的测试
现代版本的 bash
包含了一个复合命令,是一个 test
的增强替代品。使用如下句法:
[[ expression ]]
这里,和 test
类似,expression 是一个评估真假结果的表达式。[[ ]]
命令则类似 test
(支持其所有的表达式),但是加入了一个新的重要的字符串表达式。
string1 =~ regex
若 string1
匹配了扩展的正则表达式 regex
则返回真。这就为执行如数据验证之类的任务打开了许多可能性。在我们早先的整数表达式示例中,如果常量 INT
包含除了整数之外的任何字符,脚本将会失败。脚本需要一个方法来验证常量包含的是一个整数。使用带 =~
的 [[ ]]
表达式操作符,我们可以这样来改进这个脚本:
通过应用正则表达式,我们可以限制 INT
的值为一个以可选的减号开始的一个或多个数字。该表达式还消除了空值的可能性。
[[ ]]
另一个增加的功能是 ==
操作符, 支持与路径名扩展相同的模式匹配。下面是一个示例:
这使得 [[ ]]
在评测文件和路径名时很有用。
为整数设计的 (())
除了 [[ ]]
复合命令之外,bash
还提供了 (( ))
复合命令,用来操作整数。它支持一整套算术评估,我们会在第 34 章「字符串和数字」中完整地学习这个课题。
(( ))
用来执行算术真值测试(arithmetic truth tests)。如果一个算术评估的结果非零,则该算术真值测试的结果为真。
使用 (( ))
,我们可以略微简化 test-integer2
脚本:
注意,我们使用小于号和大于号,还使用 ==
来测试相等性。对于整数来说,这种句法看起来更自然。还应注意,因为组合命令 (( ))
是 shell 句法的一部分,而非普通命令,且其仅处理整数,它能通过名称识别变量且不需要执行扩展。我们将在第 34 章中更详细地讨论 (( ))
和相关的算术扩展。
综合表达式
还可以组合多个表达式以创建更复杂的评估。表达式通过逻辑操作符来组合。我们已经在第 17 章「搜索文件」中学习 find
命令时看到过了。关于 test
和 [[ ]]
有三个逻辑操作符。它们是 AND
、OR
和 NOT
。test
和 [[ ]]
使用不同的操作符来表示这些操作:
表 27-4:逻辑操作符
操作
test
[[ ]]
和 (( ))
AND
-a
&&
OR
-o
||
NOT
!
!
这里有一个 AND
操作的示例。下面的脚本决定一个整数是否存在于一个序列中:
在上面这个脚本中,我们确定整数 INT
的值是否在 MIN_VAL
与 MAX_VAL
两者之间。用一个包含有被 &&
分隔的两个表达式的 [[ ]]
执行判断。我们还可以用 test
来编写:
!
否定符号反转了一个表达式的输出。如果一个表达式是假,则返回真,反之亦然。在下面的脚本中,我们修改评估的逻辑,以便找出那些数值在指定序列之外的 INT
:
我们为了组合,还在表达式外加了括号。如果没有包括进这些表达式,否定只会应用到第一个表达式,而不是两个的组合。用 test
来编写,会是这样的:
因为 test
所用的表达式和操作符都会被 shell 视为命令的参数(和 [[ ]]
与 (( ))
)不同,那些如 <
、>
、(
和 )
之类对 bash
而言有特殊意义的字符,必须用引号或被转义。
看到 test
和 [[ ]]
做的差不多相同的工作,哪个可取呢?test
是传统的(以及 POSIX 规范的一部分,用于标准 shell,通常用于运行系统启动脚本 ),而 [[ ]]
则是专门针对 bash
(和一些其它现代 shell)的。有一点很重要,知道如何使用广泛被应用的 test
,但是 [[ ]]
显然更有用,更易于编写,所以后者更常用于现代的脚本中。
便携性是小思想的妖怪
如果你跟「真正的」 Unix 用户谈话,会很快发现他们不怎么喜欢 Linux。他们认为 Linux 不纯洁也不干净。Unix 用户的宗旨之一是,任何事物都应该「可便携」。这意味着,你所写的脚本应该能不受变更的运行在任何类 Unix 系统中。
Unix 用户有很好的理由去相信这个。在 POSIX 之前,看到命令和 shell 的专有扩展对 Unix 世界做了什么,他们自然地会提防 Linux 对他们所钟爱的操作系统的影响。
但是便携性有个严重的缺点。它阻碍了进步。它要求总是使用「最小公分母」的技术处理事务。在 shell 编程这项事务中,这意味着使任何事务都与
sh
——原始的 Bourne shell ——兼容。这个缺点成了专有软件供应商用来为他们专有扩展辩解的借口,只不过他们称之为「创新」。但是他们实际上只是为客户锁定的设备罢了。
GNU 工具,如
bash
,则没有这些限制。它们通过支持标准和广泛可用性来鼓励可移植性。你可以免费地在任何种类的系统,甚至是在 Windows 上安装bash
和其它 GNU 工具。所以,轻松点,使用bash
的所有功能。这真的是可便携的。
控制符号:去分支的另一途径
bash
提供了两个可以执行分支的控制符号。&&
(AND)和 ||
(OR),与它们在 [[ ]]
复合命令中作为逻辑操作符类似。 下面是 &&
的句法:
command1 && command2
和 ||
的句法:
command1 || command2
理解这些行为很重要。使用 &&
操作符,command1
被执行,而 command2
则仅在 command1
执行成功之后被执行。使用 ||
操作符,command1
被执行,而 command2
则仅在 command1
执行不成功之后被执行。
实际上,这意味着我们可以做一些这样的事情:
上面的命令会创建一个名为 temp
的目录,如果成功创建了,就切换当前工作目录到 temp
。第二个命令仅在 mkdir
命令成功之后尝试运行。同样的,像这样的一个命令:
将先测试 temp
目录的存在,仅当测试失败时,才会创建该目录。这种类型的结构便于处理脚本中的错误,我们会在后续的章节中讨论该主题。例如,我们可以在脚本中这么做:
如果该脚本需要 temp
目录而其不存在,则脚本会终止执行,并给出值为 1
的退出状态。
总结
我们以一个问题开启了本章。如何能使 sys_info_page
脚本检测用户是否有权限读取所有的 home
目录?在学习了 if
之后,我们可以解决该问题,将这段代码加入到 report_home_space
函数中:
我们评估了 id
命令的输出。使用了 -u
选项,id
输出了有效用户的数字 ID 编号。超级用户的 ID 总是 0,而其他用户的 ID 数字总是大于 0。了解了这一点,我们可以构建两个不同的 here 文档,一个取得了超级用户权限,而另一个则限制在用户自己的家目录中。
我们会中断一下 sys_info_page
程序,但是别担心。会回来的。同时,我们会学习一些在恢复这项工作时所需要的课题。
扩展阅读
在 bash
的手册页中有几个章节提供了本章所覆盖的课题的更具体的细节:
列表 Lists(覆盖了控制操作符
||
和&&
)复合命令 Compound Commands(覆盖了
[[ ]]
、(( ))
和if
)条件表达式 CONDITIONAL EXPRESSIONS
shell 内建命令 SHELL BUILTIN COMMANDS(覆盖了
test
)
Last updated
Was this helpful?