if 进行分支在上一章中,我们遇到了一个问题。我们如何使报告生成器脚本适应运行该脚本的用户的权限?解决这个问题需要我们根据测试结果找到一种在脚本中“改变方向”的方法。在编程方面,我们需要程序分支。
让我们考虑一个用伪代码表示的逻辑的简单例子,这是一种用于人类消费的计算机语言的模拟。
X = 5  If X = 5, then:  Say “X equals 5.”  Otherwise:  Say “X doesis not equal 5.”
这是一个分支的例子。根据条件,“如果X=5”做一件事,“说X等于5”,否则做另一件事“说X不等于5”。
第二十七章:流量控制:使用 if 进行分支if 退出状态test文件表达式字符串表达式整数表达式更现代的 test 版本(( )) - 专为整数设计组合表达式可移植性是小头脑的小妖精控制操作员:另一种分支方式总结
if 使用shell,我们可以对前面的逻辑进行如下编码:
xxxxxxxxxxx=5 if [ "$x" -eq 5 ]; then     echo "x equals 5." else     echo "x does not equal 5." fi或者我们可以直接在命令行中输入它(稍微缩短)。
xxxxxxxxxx[me@linuxbox ~]$ x=5[me@linuxbox ~]$ if [ "$x" -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fiequals 5[me@linuxbox ~]$ x=0[me@linuxbox ~]$ if [ "$x" -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fidoes not equal 5在这个例子中,我们执行命令两次;第一次,x的值设置为5,这会导致输出字符串“equals 5”,第二次,x值设置为0,这会输出字符串“does not equal 5”。
if 复合命令具有以下语法:
if commands; then  commands  [elif commands; then  commands...]  [else  commands]  fi
其中 commands 是命令列表。乍一看,这有点令人困惑。但在我们澄清这一点之前,我们必须看看shell如何评估命令的成功或失败。
命令(包括我们编写的脚本和shell函数)在终止时向系统发出一个值,称为退出状态(exit status)。此值是0到255范围内的整数,表示命令执行的成功或失败。按照惯例,零值表示成功,任何其他值表示失败。shell提供了一个参数,我们可以使用它来检查命令的退出状态。在这里,我们看到它在行动:
xxxxxxxxxx[me@linuxbox ~]$ ls -d /usr/bin/usr/bin[me@linuxbox ~]$ echo $?0[me@linuxbox ~]$ ls -d /bin/usrls: cannot access /bin/usr: No such file or directory[me@linuxbox ~]$ echo $?2在这个例子中,我们执行 ls 命令两次。第一次,命令成功执行。如果我们显示参数 $? 的值,我们看到它是零。我们再次执行 ls 命令(指定一个不存在的目录),产生错误,并再一次检查参数 $? 。这次它包含一个2,表示命令遇到了错误。一些命令使用不同的退出状态值来提供错误诊断,而许多命令在失败时只是以值1退出。手册页通常包含一个名为“Exit Status”的部分,描述使用的代码。然而,零总是意味着成功。
shell提供了两个极其简单的内置命令,除了以0或1退出状态终止外,什么也不做。 true 命令总是成功执行, false 命令总是失败执行。
xxxxxxxxxx[me@linuxbox ~]$ true[me@linuxbox ~]$ echo $?0[me@linuxbox ~]$ false[me@linuxbox ~]$ echo $?1我们可以使用这些命令来查看 if 语句是如何工作的。if语句真正做的是评估命令的成功或失败。
xxxxxxxxxx[me@linuxbox ~]$ if true; then echo "It's true."; fiIt's true.[me@linuxbox ~]$ if false; then echo "It's true."; fi[me@linuxbox ~]$命令 echo “It's true.” 在 if 命令成功执行时执行,在 if 命令未成功执行时不执行。如果命令列表遵循 if,则计算列表中的最后一个命令:
xxxxxxxxxx[me@linuxbox ~]$ if false; true; then echo "It's true."; fiIt's true.[me@linuxbox ~]$ if true; false; then echo "It's true."; fi[me@linuxbox ~]$test到目前为止, if 最常用的命令是 test 。 test 命令执行各种检查和比较。它有两种等效形式。第一个,如下所示:
test expression
第二种,更受欢迎的形式,如下所示:
[ expression ]
其中 expression 是被评估为真或假的表达式。当表达式为真时, test 命令返回退出状态0,当表达式为假时,返回状态1。
值得注意的是, test 和 [ 实际上都是命令。在bash中,它们是内置的,但它们也作为程序存在于 /usr/bin 中,供其他shell使用。表达式实际上只是它的参数, [ 命令要求 ] 字符作为其最终参数提供。
test 和 [ 命令支持各种有用的表达式和测试。
下表列出了用于评估文件状态的表达式。
| 表达式 | 为true的情况 | 
|---|---|
| file1 -ef file2 | file1 和 file2 具有相同的索引节点号 这两个文件名通过硬链接引用同一个文件。  | 
| file1 -nt file2 | file1 is newer than file2. | 
| file1 -ot file2 | file1 is older than file2. | 
| -b file | file存在,且是块专用(block-special,设备)文件。 | 
| -c file | file存在,且是一个字符特殊(character-special,设备)文件。 | 
| -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存在并且长度大于零。 | 
| -S file | file存在,并且是一个网络套接字。 | 
| -t fd | fd 是指向/来自终端的文件描述符。 这可用于确定是否重定向了标准输入/输出/错误。  | 
| -u file | file存在并且是setuid。 | 
| -w file | file存在并且可写(对有效用户具有写权限)。 | 
| -x file | file存在并且可执行(对有效用户具有执行/搜索权限)。 | 
这里我们有一个脚本,演示了一些文件表达式:
xxxxxxxxxx# test-file: Evaluate the status of a file FILE=~/.bashrc if [ -e "$FILE" ]; then     if [ -f "$FILE" ]; then         echo "$FILE is a regular file."     fi     if [ -d "$FILE" ]; then         echo "$FILE is a directory."     fi     if [ -r "$FILE" ]; then        echo "$FILE is readable."     fi     if [ -w "$FILE" ]; then        echo "$FILE is writable."     fi     if [ -x "$FILE" ]; then         echo "$FILE is executable/searchable."     fi else     echo "$FILE does not exist"     exit 1 fi exit该脚本计算分配给常量 FILE 的文件,并在执行计算时显示其结果。关于这个脚本,有两点值得注意。
首先,请注意参数 $FILE 在表达式中的引用方式。这不是语法上完成表达式所必需的;相反,它是对参数为空或仅包含空格的防御。如果 $FILE 的参数扩展导致空值,则会导致错误(运算符将被解释为非空字符串而不是运算符)。在参数周围使用引号可确保运算符后面始终跟有字符串,即使字符串为空。
其次,请注意脚本末尾附近存在 exit 命令。 exit 命令接受一个可选参数,该参数将成为脚本的退出状态。当没有传递参数时,退出状态默认为最后执行的命令的退出状态。以这种方式使用 exit 允许脚本在 $FILE 扩展为不存在的文件名时指示失败。出现在脚本最后一行的 exit 命令是一种形式。当脚本“runs off the end”(到达文件末尾)时,它会以执行的最后一个命令的退出状态终止。
同样,shell函数可以通过在 return 命令中包含整数参数来返回退出状态。如果我们将之前的脚本转换为shell函数以将其包含在更大的程序中,我们可以用 return 语句替换 exit 命令并获得所需的行为。
xxxxxxxxxxtest_file () {    # test-file: Evaluate the status of a file         FILE=~/.bashrc         if [ -e "$FILE" ]; then         if [ -f "$FILE" ]; then                 echo "$FILE is a regular file."         fi         if [ -d "$FILE" ]; then                 echo "$FILE is a directory."         fi         if [ -r "$FILE" ]; then                 echo "$FILE is readable."         fi         if [ -w "$FILE" ]; then             echo "$FILE is writable."         fi         if [ -x "$FILE" ]; then             echo "$FILE is executable/searchable."         fi     else         echo "$FILE does not exist"         return 1     fi     }下表列出了用于计算字符串的表达式:
| 表达式 | 为true的情况 | 
|---|---|
| string | string 不为空 | 
| -n string | string 长度大于零 | 
| -z string | string 长度为零 | 
| string1 = string2 string1 == string2  | string1 和 string2 相等。 可以使用单等号或双等号。 bash支持使用双等号,通常是首选,但它不符合POSIX标准。  | 
| string1 != string2 | string1 和 string2 不同 | 
| string1 > string2 | string1 在 string2 之后排序。 | 
| string1 < string2 | string1 在 string2 之前排序。 | 
警告:与测试一起使用时, > 和 < 表达式运算符必须加引号(或用反斜杠转义)。如果不是,它们将被shell解释为重定向运算符,可能会产生破坏性的结果。还要注意,虽然bash文档指出排序顺序符合当前区域设置的排序顺序,但可能不符合。ASCII(POSIX)顺序用于bash 4.0之前的版本。此问题已在4.1版本中修复。
这是一个包含字符串表达式的脚本:
xxxxxxxxxx# test-string: evaluate the value of a string ANSWER=maybe if [ -z "$ANSWER" ]; then     echo "There is no answer." >&2    exit 1 fi if [ "$ANSWER" == "yes" ]; then     echo "The answer is YES." elif [ "$ANSWER" == "no" ]; then     echo "The answer is NO." elif [ "$ANSWER" == "maybe" ]; then     echo "The answer is MAYBE." else     echo "The answer is UNKNOWN." fi在这个脚本中,我们计算常量ANSWER 。我们首先确定字符串是否为空。如果是,我们终止脚本并将退出状态设置为1。请注意应用于 echo 命令的重定向。这将错误消息“There is no answer.”重定向到标准错误,这是处理错误消息的正确方法。如果字符串不为空,我们会计算字符串的值,看看它是否等于“yes”、“no”或“maybe”。我们使用 elif 来实现这一点, elif 是 else if 的缩写。通过使用 elif ,我们能够构建更复杂的逻辑测试。
为了将值作为整数而不是字符串进行比较,我们可以使用下表中列出的表达式。
| 表达式 | 为true的情况 | 
|---|---|
| integer1 -eq integer2 | integer1 等于 integer2 | 
| integer1 -ne integer2 | integer1 不等于 integer2 | 
| integer1 -le integer2 | integer1 小于或等于 integer2 (less than or equal) | 
| integer1 -lt integer2 | integer1 小于 integer2 (less than) | 
| integer1 -ge integer2 | integer1 大于或等于 integer2 (greater than or equal) | 
| integer1 -gt integer2 | integer1 大于 integer2 (greater than) | 
以下是一个演示它们的脚本:
xxxxxxxxxx# test-integer: evaluate the value of an integer. INT=-5 if [ -z "$INT" ]; then     echo "INT is empty." >&2     exit 1 fi if [ "$INT" -eq 0 ]; then     echo "INT is zero." else     if [ "$INT" -lt 0 ]; then         echo "INT is negative."     else         echo "INT is positive."     fi     if [ $((INT % 2)) -eq 0 ]; then         echo "INT is even."     else         echo "INT is odd."     fi fi该脚本的有趣之处在于它如何确定整数是偶数还是奇数。通过对数字执行模2运算,将数字除以2并返回余数,它可以判断数字是奇数还是偶数。
test 版本现代版本的bash包含一个复合命令,可以作为 test 的增强替代品。它使用以下语法:
[[ expression ]]
其中,与 test 一样, expression 是一个计算结果为真或假的表达式。 [[ ]] 命令类似于 test (它支持其所有表达式),但添加了一个重要的新字符串表达式。
string1 =~ regex
如果 string1 与扩展正则表达式 regex 匹配,则返回 true 。这为执行数据验证等任务开辟了很多可能性。在前面的整数表达式示例中,如果常量INT包含除整数之外的任何内容,则脚本将失败。脚本需要一种方法来验证常量是否包含整数。使用 [[ ]] 和 =~ 字符表达式运算符,我们可以这样改进脚本:
xxxxxxxxxx# test-integer2: evaluate the value of an integer. INT=-5 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then     if [ "$INT" -eq 0 ]; then         echo "INT is zero."     else         if [ "$INT" -lt 0 ]; then             echo "INT is negative."         else             echo "INT is positive."         fi         if [ $((INT % 2)) -eq 0 ]; then             echo "INT is even."         else             echo "INT is odd."         fi    fi else     echo "INT is not an integer." >&2     exit 1 fi通过应用正则表达式,我们能够将 INT 的值限制为仅以可选减号开头,后跟一个或多个数字的字符串。此表达式还消除了空值的可能性。
[[ ]] 的另一个附加功能是 == 运算符支持模式匹配,就像路径名扩展一样。这里有一个例子:
xxxxxxxxxx[me@linuxbox ~]$ FILE=foo.bar [me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then > echo "$FILE matches pattern 'foo.*'" > fi foo.bar matches pattern 'foo.*'这使得 [[ ]] 在评估文件名和路径名时非常有用。
除了 [[ ]] 复合命令外,bash还提供了 (( )) 复合命令,这对整数操作很有用。它支持一整套算术运算,我们将在 【第34章.字符串和数字】中全面介绍这一主题。
(( )) 用于执行算术真值测试(arithmetic truth tests)。如果算术求值的结果为非零,则算术真值测试结果为真。
xxxxxxxxxx[me@linuxbox ~]$ if ((1)); then echo "It is true."; fi It is true.[me@linuxbox ~]$ if ((0)); then echo "It is true."; fi[me@linuxbox ~]$使用 (( )) ,我们可以稍微简化 test-integer2 脚本,如下所示:
xxxxxxxxxx# test-integer2a: evaluate the value of an integer. INT=-5 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then     if ((INT == 0)); then         echo "INT is zero."    else         if ((INT < 0)); then             echo "INT is negative."         else             echo "INT is positive."         fi         if (( ((INT % 2)) == 0)); then             echo "INT is even."         else             echo "INT is odd."         fi     fi else     echo "INT is not an integer." >&2     exit 1 fi请注意,我们使用小于和大于符号, == 用于测试等价性。这是一种处理整数时看起来更自然的语法。还要注意,由于复合命令 (( )) 是shell语法的一部分,而不是普通命令,而且它只处理整数,因此它能够按名称识别变量,不需要执行扩展。我们将在【第34章】中进一步讨论 (( )) 和相关的算术展开。
还可以组合表达式来创建更复杂的计算。表达式通过使用逻辑运算符组合在一起。当我们学习 find 命令时,我们在【第17章.搜索文件】中看到了这些。 test 和 [[ ]] 有三个逻辑操作。它们是 AND ,OR 和 NOT 。 test 和 [[ ]] 使用不同的运算符来表示这些操作:
| 操作 | test | [[ ]] 和 (( )) | 
|---|---|---|
| AND | -a | && | 
| OR | -o | || | 
| NOT | ! | ! | 
这是一个 AND 操作的示例。以下脚本确定整数是否在值范围内:
xxxxxxxxxx# test-integer3: determine if an integer is within a # specified range of values. MIN_VAL=1MAX_VAL=100 INT=50 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then     if [[ "$INT" -ge "$MIN_VAL" && "$INT" -le "$MAX_VAL" ]]; then        echo "$INT is within $MIN_VAL to $MAX_VAL."     else         echo "$INT is out of range."     fielse     echo "INT is not an integer." >&2     exit 1 fi在这个脚本中,我们确定整数 INT 的值是否位于 MIN_VAL 和 MAX_VAL 的值之间。这是通过单次使用 [[ ]] 来执行的,其中包括由 && 运算符分隔的两个表达式。我们也可以使用 test 对其进行编码:
xxxxxxxxxxif [ "$INT" -ge "$MIN_VAL" -a "$INT" -le "$MAX_VAL" ]; then     echo "$INT is within $MIN_VAL to $MAX_VAL." else     echo "$INT is out of range." fi! 否定运算符反转表达式的结果。如果表达式为 false ,则返回 true ;如果表达式为 true ,则返回 false 。在下面的脚本中,我们修改了计算的逻辑,以找到指定范围之外的 INT 值:
xxxxxxxxxx# test-integer4: determine if an integer is outside a # specified range of values. MIN_VAL=1 MAX_VAL=100 INT=50 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then     if [[ ! ("$INT" -ge "$MIN_VAL" && "$INT" -le "$MAX_VAL") ]]; then        echo "$INT is outside $MIN_VAL to $MAX_VAL."     else         echo "$INT is in range."     fi else     echo "INT is not an integer." >&2     exit 1 fi我们还在表达式周围加上括号,用于分组。如果不包括这些,否定只适用于第一个表达式,而不适用于两者的组合。使用测试对其进行编码的方式如下:
xxxxxxxxxx    if [ ! \( "$INT" -ge "$MIN_VAL" -a "$INT" -le "$MAX_VAL" \) ]; then         echo "$INT is outside $MIN_VAL to $MAX_VAL."     else         echo "$INT is in range."     fi由于 test 使用的所有表达式和运算符都被shell视为命令参数(与 [[ ]] 和 (( )) 不同),因此对bash具有特殊含义的字符,如 < 、 > 、 ( 和 ) ,必须被引用或转义。
看到 test 和 [[ ]] 做大致相同的事情,哪一个更可取? test 是传统的(也是标准shell的POSIX规范的一部分,通常用于运行系统启动脚本),而 [[ ]] 特定于bash(和其他一些现代shell)。了解如何使用 test 很重要,因为它被广泛使用,但 [[ ]] 显然更有用,更容易编码,因此它是现代脚本的首选。
Hobgoblin —— 妖怪,大地精;淘气鬼,怪物;大哥布林
portability —— 可移植性,可携性
如果你和“真正的”Unix用户交谈,你很快就会发现他们中的许多人不太喜欢Linux。他们认为这是不洁的。Unix用户的一个原则是,一切都应该是“可移植的”。这意味着你写的任何脚本都应该能够在任何类Unix系统上运行,不受更改。
Unix用户有充分的理由相信这一点。在POSIX之前,他们已经看到了命令和shell的专有扩展对Unix世界的影响,他们自然对Linux对他们心爱的操作系统的影响持谨慎态度。
但可移植性有一个严重的缺点。它阻碍了进步。它要求事情总是使用“最低公分母”技术来完成。在shell编程的情况下,这意味着使所有内容都与 sh 兼容, sh 是原始的Bourne shell。
这种缺点是专有软件供应商用来为其专有扩展辩护的借口,只是他们称之为“创新”。但它们实际上只是客户的锁定设备。
GNU工具,如bash,没有这样的限制。它们通过支持标准和普遍可用来鼓励可移植性。你可以在几乎任何类型的系统上安装bash和其他GNU工具,甚至是Windows,而无需付费。所以,可以随意使用bash的所有功能。它真的很便携。
bash提供了两个可以执行分支的控制运算符。 && (AND)和 || (OR)运算符的工作方式与 [[ ]] 复合命令中的逻辑运算符相似。以下是 && 的语法:
command1 && command2
以下是 || 的语法:
command1 || command2
了解这些行为很重要。
使用&&运算符时,始终执行command1,只有当command1成功时才执行command2。
使用||运算符时,始终执行command1,只有当command1不成功时才执行command2。
实际上,这意味着我们可以做这样的事情:
xxxxxxxxxx[me@linuxbox ~]$ mkdir temp && cd temp这将创建一个名为 temp 的目录,如果成功,当前工作目录将更改为 temp 。只有当 mkdir 命令成功时,才会尝试执行第二个命令。同样,这样的命令:
xxxxxxxxxx[me@linuxbox ~]$ [[ -d temp ]] || mkdir temp将测试目录 temp 的存在,只有测试失败,才会创建目录。这种构造对于处理脚本中的错误非常方便,我们将在后面的章节中对此进行更多讨论。例如,我们可以在脚本中这样做:
xxxxxxxxxx[ -d temp ] || exit 1如果脚本需要临时目录,但该目录不存在,则脚本将终止,退出状态为1。
记住,如果我们有做复杂事情的冲动,命令可以是组命令:
xxxxxxxxxx{ true && echo "true"; } && { false || echo "false"; }组命令返回组中最后一个命令的退出状态。
我们以一个问题开始了这一章。我们如何使 sys_info_page 脚本检测用户是否有权读取所有主目录?根据我们对 if 的了解,我们可以通过将以下代码添加到 report_home_space 函数中来解决这个问题:
xxxxxxxxxxreport_home_space () {     if [[ "$(id -u)" -eq 0 ]]; then         cat << _EOF_             <h2>Home Space Utilization (All Users)</h2>             <pre>$(du -sh /home/*)</pre> _EOF_     else         cat << _EOF_             <h2>Home Space Utilization ($USER)</h2>             <pre>$(du -sh $HOME)</pre>_EOF_     fi     return }我们评估 id 命令的输出。使用 -u 选项, id 输出有效用户的数字用户 id 号。超级用户的ID始终为零,其他所有用户都是大于零的数字。知道这一点后,我们可以在这里构建两个不同的文档,一个利用超级用户权限,另一个仅限于用户自己的主目录。
我们将暂停 sys_info_page 程序,但别担心。它会回来的。与此同时,我们将讨论一些我们在恢复工作时需要的主题。