第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

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

[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ “$x” -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ “$x” -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
does not equal 5

上例中,我们执行了两次命令;一次,设置 x 的值为 5,输出字符串 "equals 5",第二次设置 x 的值为 0,输出字符串 "does not equals 5"。

if 语句的句法如下:

if commands; then
    commands
[elif commands; then
    commands...]
[else
    commands]
fi

其中 commands 是一个命令列表。初看之下会有点困惑。但是在我们搞清楚前,有必要来看下 shell 是如何评估一个命令的成功或失败的。

退出状态

命令(包含脚本和我们所写的 shell 函数)在其结束时会提交一个值给系统,叫作退出状态(exit status)。这个值,是一个从 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 命令。第一次,命令执行成功。如果显示参数 $? 的值,可以看到是 0。第二次执行 ls 命令时(指定了成儿不存在的目录),产生了一个错误,再次检查参数 $?。这次它包含了一个 2,指示命令遇到了一个错误。一些命令使用不同的退出状态值以提供错误诊断信息,有些命令则仅仅简单地在发生错误时给出一个 1。手册页通常会包含一个名为「退出状态」的章节,描述了使用什么代码。不过,0 总是指示成功。

shell 提供了两个最最简单的内建命令,除了以 01 的退出状态结束外,什么也不做。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 ~]$

命令 echo "It's true."if 后的命令成功执行后被执行,而当在 if 后的命令没有被成功执行时,不被执行。如果有一个命令列表在 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 ~]$

测试

目前为止,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 存在,且可执行(对有效用户有可执行和检索权限)。

这里有一个脚本可以演示一些文件表达式:

#!/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 命令则是作为一种手续。当一个脚本「运行到最后」(到达文件末尾),会以最后一个被执行的命令的退出状态终结。

类似的,shell 函数可以通过包含一个整型参数到 return 命令中,以返回一个退出状态。如果我们将之前的脚本转换为一个 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
}

字符串表达式

表 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 中修复。

下面是一个合并了字符串表达式的脚本:

#!/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。首先确定字符串是否为空。如果是,就终结脚本,设置退出状态为 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

下面是一个演示脚本:

#!/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 还提供了 (( )) 复合命令,用来操作整数。它支持一整套算术评估,我们会在第 34 章「字符串和数字」中完整地学习这个课题。

(( )) 用来执行算术真值测试(arithmetic truth tests)。如果一个算术评估的结果非零,则该算术真值测试的结果为真。

[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 句法的一部分,而非普通命令,且其仅处理整数,它能通过名称识别变量且不需要执行扩展。我们将在第 34 章中更详细地讨论 (( )) 和相关的算术扩展。

综合表达式

还可以组合多个表达式以创建更复杂的评估。表达式通过逻辑操作符来组合。我们已经在第 17 章「搜索文件」中学习 find 命令时看到过了。关于 test[[ ]] 有三个逻辑操作符。它们是 ANDORNOTtest[[ ]] 使用不同的操作符来表示这些操作:

表 27-4:逻辑操作符

操作

test

[[ ]](( ))

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

在上面这个脚本中,我们确定整数 INT 的值是否在 MIN_VALMAX_VAL 两者之间。用一个包含有被 && 分隔的两个表达式的 [[ ]] 执行判断。我们还可以用 test 来编写:

if [ "$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

! 否定符号反转了一个表达式的输出。如果一个表达式是假,则返回真,反之亦然。在下面的脚本中,我们修改评估的逻辑,以便找出那些数值在指定序列之外的 INT

#!/bin/bash

# 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

我们为了组合,还在表达式外加了括号。如果没有包括进这些表达式,否定只会应用到第一个表达式,而不是两个的组合。用 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 规范的一部分,用于标准 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 执行不成功之后被执行。

实际上,这意味着我们可以做一些这样的事情:

[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 脚本检测用户是否有权限读取所有的 home 目录?在学习了 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 编号。超级用户的 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/Pseudocode

Last updated

Was this helpful?