第27章:流程控制:if 分支

在上章中,我们遇到了一个问题。如何能使得我们的报表生成脚本能适应运行脚本的用户权限?该问题的解决方案要求我们在脚本中找到一个基于测试结果的「改变方向」的方法。在编程术语中叫做我们需要编程分支(branch)。

让我们考虑一个关于逻辑表达的伪代码(pseudocode)示例,一段便于人类理解的对计算机语言的模拟。

X = 5
If X = 5, then:
    Say “X equals 5.”
Otherwise:
    Say “X is not equal to 5.”

这是一个分支的示例。基于对「x 是否等于 5?」这个条件做一件事「说 x 等于 5」,否则做另一件事「说 x 不等于 5」。

if

使用 shell,我们可以将上面这个逻辑写成下面的代码:

x=5

if [ "$x" -eq 5 ]; then
    echo "x equals 5."
else
    echo "x does not equal 5."
fi

或者,我们可以直接在命令行中输入(稍微短一点)。

上例中,我们执行了两次命令;一次,设置 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 提供了两个最最简单的内建命令,除了以 01 的退出状态结束外,什么也不做。true 命令总是执行成功,而 false 命令总是执行不成功。

我们可以用这些命令来看 if 语句是如何工作的。if 语句真正做的,是评估命令的成功或失败。

命令 echo "It's true."if 后的命令成功执行后被执行,而当在 if 后的命令没有被成功执行时,不被执行。如果有一个命令列表在 if 之后的话,评估的是列表中最后的一个命令:

测试

目前为止,if 最常用的命令就是 testtest 命令执行各种检查和比较。有两种等效的形式。第一种如下:

test expression

第二种则更通用,如下:

[ expression ]

expression 是一个被用来评估真假的表达式。当表达式为真时,test 命令返回一个为 0 的退出状态,为假则返回一个为 1 的退出状态。

有趣的是,要注意 test[ 实际上都是命令。它们是 bash 中的内建命令,但是也存在于 /usr/bin 中以供其它 shell 使用。表达式只不过是其参数,对于 [ 命令,则要求 ] 字符为其最后一个参数。

test[ 命令支持广泛有用的表达式和测试。

文件表达式

表 27-1 列出了用来评估文件状态的表达式。

表 27-1: test 文件表达式

表达式

如果……为真

file1 -ef file2

file1file2 具有相同的 inode 编号(两个文件名通过硬链接指向同一个文件)。

file1 -nt file2

file1file2 更新。

file1 -ot file2

file1file2 更旧。

-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

string1string2 相等。可以使用单个或两个等于符号。两个等于号得到 bash 的支持,并是常用写法,但却不符合 POSIX。

string1 != string2

string1string2 不相等。

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[[ ]] 有三个逻辑操作符。它们是 ANDORNOTtest[[ ]] 使用不同的操作符来表示这些操作:

表 27-4:逻辑操作符

操作

test

[[ ]](( ))

AND

-a

&&

OR

-o

||

NOT

!

!

这里有一个 AND 操作的示例。下面的脚本决定一个整数是否存在于一个序列中:

在上面这个脚本中,我们确定整数 INT 的值是否在 MIN_VALMAX_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

进一步,维基百科有一篇关于伪代码概念的文章:http://en.wikipedia.org/wiki/Pseudocodearrow-up-right

Last updated

Was this helpful?