Go to Table of Contents

流程控制:if 分支结构


在上一章中,我们遇到一个问题。怎样使我们的报告生成器脚本能适应运行此脚本的用户的权限? 这个问题的解决方案要求我们能找到一种方法,在脚本中基于测试条件结果,来“改变方向”。 用编程术语表达,就是我们需要程序可以分支。让我们考虑一个简单的用伪码表示的逻辑实例, 伪码是一种模拟的计算机语言,为的是便于人们理解:

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

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

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

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

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

[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
not equal 5"; fi
does 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 函数)会给系统发送一个值,叫做退出状态。 这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。按照惯例,一个零值说明成功,其它所有值说明失败。 Shell 提供了一个参数,我们可以用它检查退出状态。用具体实例看一下:

[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2

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

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

[me@linuxbox~]$ true
[me@linuxbox~]$ echo $?
0
[me@linuxbox~]$ false
[me@linuxbox~]$ echo $?
1

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

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

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

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

测试

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

test expression

比较流行的格式是:

[ expression ]

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

文件表达式

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

表28-1: 测试文件表达式
表达式 如果为真
file1 -ef file2 file1 和 file2 拥有相同的索引号(通过硬链接两个文件名指向相同的文件)。
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 存在并且设置了它的“sticky bit”。
-L file file 存在并且是一个符号链接。
-O file file 存在并且由有效用户 ID 拥有。
-p file file 存在并且是一个命名管道。
-r file file 存在并且可读(有效用户有可读权限)。
-s file file 存在且其长度大于零。
-S file file 存在且是一个网络 socket。
-t fd fd 是一个定向到终端/从终端定向的文件描述符 。 这可以被用来决定是否重定向了标准输入/输出错误。
-u file file 存在并且设置了 setuid 位。
-w file file 存在并且可写(有效用户拥有可写权限)。
-x file file 存在并且可执行(有效用户有执行/搜索权限)。

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

#!/bin/bash
# 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 命令 出现在脚本中的最后一行,是一个当一个脚本“运行到最后”(到达文件末尾),不管怎样, 默认情况下它以退出状态零终止。

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

test_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
}

字符串表达式

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

表28-2: 测试字符串表达式
表达式 如果为真...
string string 不为 null。
-n string 字符串 string 的长度大于零。
-z string 字符串 string 的长度为零。

string1 = string2

string1 == string2

string1 和 string2 相同. 单或双等号都可以,不过双等号更受欢迎。
string1 != string2 string1 和 string2 不相同。
string1 > string2 sting1 排列在 string2 之后。
string1 < string2 string1 排列在 string2 之前。

警告:这个 < 和 > 表达式操作符必须用引号引起来(或者是用反斜杠转义), 当与 test 一块使用的时候。如果不这样,它们会被 shell 解释为重定向操作符,造成潜在地破坏结果。 同时也要注意虽然 bash 文档声明排序遵从当前语系的排列规则,但并不这样。将来的 bash 版本,包含 4.0, 使用 ASCII(POSIX)排序规则。


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

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

整型表达式

下面的表达式用于整数:

表28-3: 测试整数表达式
表达式 如果为真...
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.

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

#!/bin/bash
# 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,并返回余数,从而知道数字是偶数还是奇数。

更现代的测试版本

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

[[ expression ]]

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

string1 =~ regex

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

#!/bin/bash
# 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 的值只是字符串,其开始于一个可选的减号,随后是一个或多个数字。 这个表达式也消除了空值的可能性。

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

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

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

(( )) - 为整数设计

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

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

[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脚本,像这样:

#!/bin/bash
# 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 语法的一部分,而不是一个普通的命令,而且它只处理整数, 所以它能够通过名字识别出变量,而不需要执行展开操作。我们将在第35中进一步讨论(( ))命令 和相关的算术展开操作。

结合表达式

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

表28-4: 逻辑操作符
操作符 测试 [[ ]] and (( ))
AND -a &&
OR -o ||
NOT ! !

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

#!/bin/bash
# test-integer3: determine if an integer is within 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 within $MIN_VAL to $MAX_VAL."
    else
        echo "$INT is out of range."
    fi
else
    echo "INT is not an integer." >&2
    exit 1
fi

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

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 更传统(是 POSIX 的一部分), 然而[[ ]]特定于 bash。知道怎样使用 test 很重要,因为它被非常广泛地应用,但是显然[[ ]]更 有助于,并更易于编码。

可移植性是头脑狭隘人士的心魔

如果你和“真正的” Unix 用户交谈,你很快就会发现他们大多数人不是非常喜欢 Linux。他们 认为 Linux 肮脏且不干净。Unix 追随者的一个宗旨是,一切都应“可移植的”。这意味着你编写 的任意一个脚本都应当无需修改,就能运行在任何一个类似于 Unix 的系统中。

Unix 用户有充分的理由相信这一点。在 POSIX 之前,Unix 用户已经看到了命令的专有扩展以及 shell 对 Unix 世界的所做所为,他们自然会警惕 Linux 对他们心爱系统的影响。

但是可移植性有一个严重的缺点。它防碍了进步。它要求做事情要遵循“最低常见标准”。 在 shell 编程这种情况下,它意味着一切要与 sh 兼容,最初的 Bourne shell。

这个缺点是一个借口,专有软件供应商用它来证明他们的专利扩展,只有他们称他们为“创新”。 但是他们只是为他们的客户锁定设备。

GNU 工具,比如说 bash,就没有这些限制。他们通过支持标准和普遍地可用性来鼓励可移植性。你几乎可以 在所有类型的系统中安装 bash 和其它的 GNU 工具,甚至是 Windows,而没有损失。所以就 感觉可以自由的使用 bash 的所有功能。它是真正的可移植。

控制操作符:分支的另一种方法

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

command1 && command2

command1 || command2

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

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

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

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

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

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

[ -d temp ] || exit 1

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

总结

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

report_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 号。 超级用户总是零,其它每个用户是一个大于零的数字。知道了这点,我们能够构建两种不同的 here 文档, 一个利用超级用户权限,另一个限制于用户拥有的主目录。

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

拓展阅读

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

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

http://en.wikipedia.org/wiki/Pseudocode


Go to Table of Contents