流程控制:if 分支结构

    这就是一个分支的例子。根据条件,“Does X = 5?” 做一件事情,“Say X equals 5,”否则,做另一件事情,“Say X is not equal to 5.”

    使用 shell,我们可以编码上面的逻辑,如下所示:

    1. if [ $x = 5 ]; then
    2. echo "x equals 5."
    3. else
    4. echo "x does not equal 5."
    5. fi

    或者我们可以直接在命令行中输入以上代码(略有缩短):

    1. [me@linuxbox ~]$ x=5
    2. [me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
    3. not equal 5"; fi
    4. equals 5
    5. [me@linuxbox ~]$ x=0
    6. [me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
    7. not equal 5"; fi
    8. does not equal 5

    在这个例子中,我们执行了两次这个命令。第一次是,把 x 的值设置为5,从而导致输出字符串“equals 5”,第二次是,把 x 的值设置为0,从而导致输出字符串“does not equal 5”。

    这个 if 语句语法如下:

    1. if commands; then
    2. commands
    3. [elif commands; then
    4. commands...]
    5. [else
    6. commands]
    7. fi

    这里的 commands 是指一系列命令。第一眼看到会有点儿困惑。但是在我们弄清楚这些语句之前,我们必须看一下 shell 是如何评判一个命令的成功与失败的。

    28.2 退出状态

    当命令执行完毕后,命令(包括我们编写的脚本和 shell 函数)会给系统发送一个值,叫做退出状态。这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。按照惯例,一个零值说明成功,其它所有值说明失败。Shell 提供了一个参数,我们可以用它检查退出状态。用具体实例看一下:

    1. [me@linuxbox ~]$ ls -d /usr/bin
    2. /usr/bin
    3. [me@linuxbox ~]$ echo $?
    4. 0
    5. [me@linuxbox ~]$ ls -d /bin/usr
    6. ls: cannot access /bin/usr: No such file or directory
    7. [me@linuxbox ~]$ echo $?
    8. 2

    在这个例子中,我们执行了两次 ls 命令。第一次,命令执行成功。如果我们显示参数$?的值,我们看到它是零。我们第二次执行 ls 命令的时候,产生了一个错误,并再次查看参数$?。这次它包含一个数字 2,表明这个命令遇到了一个错误。有些命令使用不同的退出值,来诊断错误,而许多命令当它们执行失败的时候,会简单地退出并发送一个数字1。手册页中经常会包含一章标题为“退出状态”的内容,描述了使用的代码。然而,一个零总是表明成功。

    shell 提供了两个极其简单的内部命令,它们不做任何事情,除了以一个0或1退出状态来终止执行。True 命令总是执行成功,而 false 命令总是执行失败:

    1. [me@linuxbox~]$ true
    2. [me@linuxbox~]$ echo $?
    3. 0
    4. [me@linuxbox~]$ false
    5. [me@linuxbox~]$ echo $?
    6. 1

    我们能够使用这些命令,来看一下 if 语句是怎样工作的。If 语句真正做的事情是计算命令执行成功或失败:

    1. [me@linuxbox ~]$ if true; then echo "It's true."; fi
    2. It's true.
    3. [me@linuxbox ~]$ if false; then echo "It's true."; fi
    4. [me@linuxbox ~]$

    当 if 之后的命令执行成功的时候,命令 echo “It’s true.” 将会执行,否则此命令不执行。如果 if 之后跟随一系列命令,则将计算列表中的最后一个命令:

    1. [me@linuxbox ~]$ if false; true; then echo "It's true."; fi
    2. It's true.
    3. [me@linuxbox ~]$ if true; false; then echo "It's true."; fi
    4. [me@linuxbox ~]$

    28.3 使用 test 测试

    到目前为止,经常与 if 一块使用的命令是 test。这个 test 命令执行各种各样的检查与比较。
    它有两种等价模式:

    1. test expression

    比较流行的格式是:

    这里的 expression 是一个表达式,其执行结果是 true 或者是 false。当表达式为真时,这个 test 命令返回一个零退出状态,当表达式为假时,test 命令退出状态为1。

    28.3.1 文件表达式

    以下表达式被用来计算文件状态:

    这里我们有一个脚本说明了一些文件表达式:

    1. #!/bin/bash
    2. # test-file: Evaluate the status of a file
    3. FILE=~/.bashrc
    4. if [ -e "$FILE" ]; then
    5. if [ -f "$FILE" ]; then
    6. echo "$FILE is a regular file."
    7. fi
    8. if [ -d "$FILE" ]; then
    9. echo "$FILE is a directory."
    10. fi
    11. if [ -r "$FILE" ]; then
    12. echo "$FILE is readable."
    13. fi
    14. if [ -w "$FILE" ]; then
    15. echo "$FILE is writable."
    16. fi
    17. echo "$FILE is executable/searchable."
    18. fi
    19. else
    20. echo "$FILE does not exist"
    21. exit 1
    22. fi
    23. exit

    这个脚本会计算赋值给常量 FILE 的文件,并显示计算结果。对于此脚本有两点需要注意。第一个,在表达式中参数$FILE是怎样被引用的。引号并不是必需的,但这是为了防范空参数。如果 “$FILE” 的参数展开是一个空值,就会导致一个错误(操作符将会被解释为非空的字符串而不是操作符)。用引号把参数引起来就确保了操作符之后总是跟随着一个字符串,即使字符串为空。第二个,注意脚本末尾的 exit 命令。这个 exit 命令接受一个单独的,可选的参数,其成为脚本的退出状态。当不传递参数时,退出状态默认为零。以这种方式使用 exit 命令,则允许此脚本提示失败如果 “$FILE” 展开成一个不存在的文件名。这个 exit 命令出现在脚本中的最后一行,是一个当一个脚本“运行到最后”(到达文件末尾),不管怎样,默认情况下它以退出状态零终止。

    类似地,通过带有一个整数参数的 return 命令,shell 函数可以返回一个退出状态。如果我们打算把上面的脚本转变为一个 shell 函数,为了在更大的程序中包含此函数,我们用 return 语句来代替 exit 命令,则得到期望的行为:

    1. test_file () {
    2. # test-file: Evaluate the status of a file
    3. if [ -e "$FILE" ]; then
    4. if [ -f "$FILE" ]; then
    5. echo "$FILE is a regular file."
    6. fi
    7. if [ -d "$FILE" ]; then
    8. echo "$FILE is a directory."
    9. fi
    10. if [ -r "$FILE" ]; then
    11. echo "$FILE is readable."
    12. fi
    13. if [ -w "$FILE" ]; then
    14. echo "$FILE is writable."
    15. fi
    16. if [ -x "$FILE" ]; then
    17. echo "$FILE is executable/searchable."
    18. fi
    19. else
    20. echo "$FILE does not exist"
    21. return 1
    22. fi
    23. }

    28.3.2 字符串表达式

    以下表达式用来计算字符串:


    警告:


    这是一个演示这些问题的脚本:

    1. #!/bin/bash
    2. # test-string: evaluate the value of a string
    3. ANSWER=maybe
    4. if [ -z "$ANSWER" ]; then
    5. echo "There is no answer." >&2
    6. exit 1
    7. fi
    8. if [ "$ANSWER" = "yes" ]; then
    9. echo "The answer is YES."
    10. elif [ "$ANSWER" = "no" ]; then
    11. echo "The answer is NO."
    12. elif [ "$ANSWER" = "maybe" ]; then
    13. echo "The answer is MAYBE."
    14. else
    15. echo "The answer is UNKNOWN."
    16. fi

    在这个脚本中,我们计算常量 ANSWER。我们首先确定是否此字符串为空。如果为空,我们就终止脚本,并把退出状态设为零。注意这个应用于 echo 命令的重定向操作。其把错误信息 “There is no answer.” 重定向到标准错误,这是处理错误信息的“正确”方法。如果字符串不为空,我们就计算字符串的值,看看它是否等于“yes,” “no,” 或者“maybe”。为此使用了 elif,它是 “else if” 的简写。通过使用 elif,我们能够构建更复杂的逻辑测试。

    28.3.3 整数型表达式

    下面的表达式用于整数:

    这里是一个演示以上表达式用法的脚本:

    1. #!/bin/bash
    2. # test-integer: evaluate the value of an integer.
    3. INT=-5
    4. if [ -z "$INT" ]; then
    5. echo "INT is empty." >&2
    6. exit 1
    7. fi
    8. if [ $INT -eq 0 ]; then
    9. echo "INT is zero."
    10. else
    11. if [ $INT -lt 0 ]; then
    12. echo "INT is negative."
    13. else
    14. echo "INT is positive."
    15. fi
    16. if [ $((INT % 2)) -eq 0 ]; then
    17. echo "INT is even."
    18. else
    19. echo "INT is odd."
    20. fi
    21. fi

    这个脚本中有趣的地方是怎样来确定一个整数是偶数还是奇数。通过用模数2对数字执行求模操作,就是用数字来除以2,并返回余数,从而知道数字是偶数还是奇数。

    目前的 bash 版本包括一个复合命令,作为加强的 test 命令替代物。它使用以下语法:

    1. [[ expression ]]

    这里,类似于 test,expression 是一个表达式,其计算结果为真或假。这个[[ ]]命令非常相似于 test 命令(它支持所有的表达式),但是增加了一个重要的新的字符串表达式:

    1. string1 =~ regex

    其返回值为真,如果 string1匹配扩展的正则表达式 regex。这就为执行比如数据验证等任务提供了许多可能性。在我们前面的整数表达式示例中,如果常量 INT 包含除了整数之外的任何数据,脚本就会运行失败。这个脚本需要一种方法来证明此常量包含一个整数。使用 [[ ]]=~ 字符串表达式操作符,我们能够这样来改进脚本:

    1. #!/bin/bash
    2. # test-integer2: evaluate the value of an integer.
    3. INT=-5
    4. if [ $INT -eq 0 ]; then
    5. echo "INT is zero."
    6. else
    7. if [ $INT -lt 0 ]; then
    8. echo "INT is negative."
    9. echo "INT is positive."
    10. fi
    11. if [ $((INT % 2)) -eq 0 ]; then
    12. echo "INT is even."
    13. else
    14. echo "INT is odd."
    15. fi
    16. fi
    17. else
    18. echo "INT is not an integer." >&2
    19. exit 1
    20. fi

    通过应用正则表达式,我们能够限制 INT 的值只是字符串,其开始于一个可选的减号,随后是一个或多个数字。这个表达式也消除了空值的可能性。

    “[[ ]]” 添加的另一个功能是 “==” 操作符支持类型匹配,正如路径名展开所做的那样。例如:

    1. [me@linuxbox ~]$ FILE=foo.bar
    2. [me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
    3. > echo "$FILE matches pattern 'foo.*'"
    4. > fi
    5. foo.bar matches pattern 'foo.*'

    这就使 “[[ ]]” 有助于计算文件和路径名。

    28.5 (( )) - 为整数设计

    除了 “[[ ]]” 复合命令之外,bash 也提供了 “(( ))” 复合命令,其有利于操作整数。它支持一套
    完整的算术计算,我们将在第35章中讨论这个主题。

    “(( ))” 被用来执行算术真测试。如果算术计算的结果是非零值,则一个算术真测试值为真。

    使用(( )),我们能够略微简化 test-integer2脚本,像这样:

    1. #!/bin/bash
    2. # test-integer2a: evaluate the value of an integer.
    3. INT=-5
    4. if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
    5. if ((INT == 0)); then
    6. echo "INT is zero."
    7. else
    8. if ((INT < 0)); then
    9. echo "INT is negative."
    10. else
    11. echo "INT is positive."
    12. fi
    13. if (( ((INT % 2)) == 0)); then
    14. echo "INT is even."
    15. else
    16. echo "INT is odd."
    17. fi
    18. fi
    19. else
    20. echo "INT is not an integer." >&2
    21. exit 1
    22. fi

    注意我们使用小于和大于符号,以及==用来测试是否相等。这是使用整数较为自然的语法了。也要注意,因为复合命令 “(( ))” 是 shell 语法的一部分,而不是一个普通的命令,而且它只处理整数,所以它能够通过名字识别出变量,而不需要执行展开操作。我们将在第35中进一步讨论 “(( ))” 命令和相关的算术展开操作。

    28.6 结合表达式

    也有可能把表达式结合起来创建更复杂的计算。通过使用逻辑操作符来结合表达式。我们在第18章中学习 find 命令的时候已经知道了这些。有三个用于 test 和 “[[ ]]” 的逻辑操作。它们是 AND、OR 和 NOT。test 和 “[[ ]]” 使用不同的操作符来表示这些操作:

    这里有一个 AND 操作的示例。下面的脚本决定了一个整数是否属于某个范围内的值:

    1. #!/bin/bash
    2. # test-integer3: determine if an integer is within a
    3. # specified range of values.
    4. MIN_VAL=1
    5. MAX_VAL=100
    6. INT=50
    7. if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
    8. if [[ INT -ge MIN_VAL && INT -le MAX_VAL ]]; then
    9. echo "$INT is within $MIN_VAL to $MAX_VAL."
    10. else
    11. echo "$INT is out of range."
    12. fi
    13. else
    14. echo "INT is not an integer." >&2
    15. exit 1
    16. fi

    我们也可以对表达式使用圆括号,为的是分组。如果不使用括号,那么否定只应用于第一个表达式,而不是两个组合的表达式。用 test 可以这样来编码:

    1. if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
    2. echo "$INT is outside $MIN_VAL to $MAX_VAL."
    3. else
    4. echo "$INT is in range."
    5. fi

    因为 test 使用的所有的表达式和操作符都被 shell 看作是命令参数(不像 “[[ ]]” 和 “(( ))” ),对于 bash 有特殊含义的字符,比如说 <,>,(,和 ),必须引起来或者是转义。

    知道了 test 和 “[[ ]]” 基本上完成相同的事情,哪一个更好呢?test 更传统(是 POSIX 的一部分),然而 [[ ]] 特定于 bash。知道怎样使用 test 很重要,因为它被非常广泛地应用,但是显然 “[[ ]]” 更有用,并更易于编码。

    bash 支持两种可以执行分支任务的控制操作符。 &&(AND)||(OR)操作符作用如同复合命令[[ ]]中的逻辑操作符。这是语法:

    1. command1 && command2

    1. command1 || command2

    理解这些操作很重要。对于 && 操作符,先执行 command1,如果并且只有如果 command1 执行成功后,才会执行 command2。对于 || 操作符,先执行 command1,如果并且只有如果 command1 执行失败后,才会执行 command2。

    在实际中,它意味着我们可以做这样的事情:

    1. [me@linuxbox ~]$ mkdir temp && cd temp

    这会创建一个名为 temp 的目录,并且若它执行成功后,当前目录会更改为 temp。第二个命令会尝试执行只有当 mkdir 命令执行成功之后。同样地,一个像这样的命令:

    1. [me@linuxbox ~]$ [ -d temp ] || mkdir temp

    会测试目录 temp 是否存在,并且只有测试失败之后,才会创建这个目录。这种构造类型非常有助于在脚本中处理错误,这个主题我们将会在随后的章节中讨论更多。例如,我们在脚本中可以这样做:

    1. [ -d temp ] || exit 1

    如果这个脚本要求目录 temp,且目录不存在,然后脚本会终止,并返回退出状态1。

    28.8 本章总结

    这一章开始于一个问题。我们怎样使 sys_info_page 脚本来检测是否用户拥有权限来读取所有的家目录?根据我们的 if 知识,我们可以解决这个问题,通过把这些代码添加到 “report_home_space” 函数中:

    我们计算 id 命令的输出结果。通过带有 -u 选项的 id 命令,输出有效用户的数字用户 ID 号。超级用户总是零,其它每个用户是一个大于零的数字。知道了这点,我们能够构建两种不同的 here 文档,一个利用超级用户权限,另一个限制于用户拥有的家目录。

    我们将暂别 sys_info_page 程序,但不要着急。它还会回来。同时,当我们继续工作的时候,将会讨论一些我们需要的话题。

    拓展阅读

    bash 手册页中有几部分对本章中涵盖的主题提供了更详细的内容:

    • Lists ( 讨论控制操作符 “||” 和 “&&” )

    • Compound Commands ( 讨论 “[[ ]]”, “(( ))” 和 if )

    • SHELL BUILTIN COMMANDS ( 讨论 test )

    进一步,Wikipedia 中有一篇关于伪代码概念的好文章: