第二十八章:流程控制 if分支结构
最后更新于:2022-04-02 01:46:11
在上一章中,我们遇到一个问题。怎样使我们的报告生成器脚本能适应运行此脚本的用户的权限? 这个问题的解决方案要求我们能找到一种方法,在脚本中基于测试条件结果,来“改变方向”。 用编程术语表达,就是我们需要程序可以分支。让我们考虑一个简单的用伪码表示的逻辑实例, 伪码是一种模拟的计算机语言,为的是便于人们理解:
~~~
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.”
## if
使用 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_
';
Home Space Utilization (All Users)
$(du -sh /home/*)_EOF_ else cat <<- _EOF_
Home Space Utilization ($USER)
$(du -sh $HOME)_EOF_ fi return } ~~~ 我们计算 id 命令的输出结果。通过带有 -u 选项的 id 命令,输出有效用户的数字用户 ID 号。 超级用户总是零,其它每个用户是一个大于零的数字。知道了这点,我们能够构建两种不同的 here 文档, 一个利用超级用户权限,另一个限制于用户拥有的家目录。 我们将暂别 `sys_info_page` 程序,但不要着急。它还会回来。同时,当我们继续工作的时候, 将会讨论一些我们需要的话题。 ## 拓展阅读 bash 手册页中有几部分对本章中涵盖的主题提供了更详细的内容: * Lists ( 讨论控制操作符 `||` 和 `&&` ) * Compound Commands ( 讨论 `[[ ]]`, `(( ))` 和 if ) * CONDITIONAL EXPRESSIONS (条件表达式) * SHELL BUILTIN COMMANDS ( 讨论 test ) 进一步,Wikipedia 中有一篇关于伪代码概念的好文章: [http://en.wikipedia.org/wiki/Pseudocode](http://en.wikipedia.org/wiki/Pseudocode)