理解条件最好的办法就是把他们实质理解为 选择(choice)。
每当一个选择出现,我们可以从众多路径中择其一继续向前。每条路径都是可选的,每条都通向不同的地方。最终走的那条,就取决于我们的选择。
这些不像你在小说中读到的选择:那些小说都已为你选好。小说的故事情节是确定了的,那些选择不会引出其他结局。条件更像是游戏中的选择:时不时地,你要做出一个关键选择,而这些选择都会以某种方式改变游戏的境况。如果你结束游戏重新来过,在某一点做出不同于此前的选择,那么游戏的走向由此就会不同。我们将其称为分支(branching)。每个选择都会开启一条新的分支,以不同方式影响我们的环境。但是要注意,这些分支的差异并不在于 我们做出的选择,而在于 选择做出后我们采取的行动。
听起来似乎有些复杂,但实际上这和我们选择买苹果手机还是安卓手机并没什么不同,或是吃早餐还是不吃,走高速公路还是辅路。我们会考虑各个选项,结合自身情况考虑什么是给定条件下最好的,然后尝试做出选择。条件也是这样:评估我们所拥有的,选择由此去向哪里。
包含条件分支的脚本要远比线性脚本广阔灵活,正如游戏要比线性叙述的书籍灵活。那么,我们为什么需要条件呢?我们需要使用他们来写出能够动态处理变化情境,以及根据情境改变运行方式的脚本。
让我们首先从一个非常简单的条件入手,来开始我们的学习:
$ read -p "Would you like some breakfast? [y/n] "
Would you like some breakfast? [y/n] n
$ if [[ $REPLY = y ]]; then
> echo "Here you go, an egg sandwich."Branch #1
> else
> echo "Here, you should at least have a coffee."Branch #2
> fi
Here, you should at least have a coffee.
条件相比我们之前已写的所有代码,关键的不同在于部分代码永远不会被执行,除非情况发生变化。上面例子中,即使第一条分支内也有对应执行代码,但 bash 并未运行他们,只有第二条分支中的代码被执行。除非情况发生改变——对应上面例子就是我们对前面问题的回答变了,这时脚本中被执行的分支就会切换到第一条,第二条分支中的代码则“死掉”。
Bash 有多种不同方式评估条件。几乎所有这些方式都有一个共同点:他们都是基于另一条命令的退出代码被评估。因此,在深入这一章的内容之前,你首先要足够熟悉前面章节谈论过的退出代码概念。
我们通常使用复合命令明确地评估条件,如上面例子中的 if ...
语句。另一种方式是使用 控制运算符 (Control Operaotr),我们在前面章节中讨论 List 命令时有简单提及。我们接下来会在这里深度列举并讨论每种类型的条件。
if
复合命令if
语句在编程语言中如此常见,以至于当我们考虑在代码中构建一个选择时,基本可以保证第一个就会想到它。这并不意外,毕竟这些语句清晰、简单又明确。因此对于熟悉 bash 中的条件,他们也是非常好的起点。
if list [ ;|<newline> ] then list ;|<newline> [ elif list [ ;|<newline> ] then list ;|<newline> ] ... [ else list ;|<newline> ] fi if ! rm hello.txt; then echo "Couldn't delete hello.txt." >&2; exit 1; fi if rm hello.txt; then echo "Successfully deleted hello.txt." else echo "Couldn't delete hello.txt." >&2; exit 1; fi if mv hello.txt ~/.Trash/; then echo "Moved hello.txt into the trash." elif rm hello.txt; then echo "Deleted hello.txt." else echo "Couldn't remove hello.txt." >&2; exit 1; fi
if
复合命令的语法虽然第一眼看起来有些冗余,实际上非常简单。首先起始于 if
关键词,后面跟的是一个命令列表。这个命令列表会被 bash 执行,完成后 bash 会把 退出代码发给 if
复合命令做评估。如果退出代码是 0(0
= 成功(success)),第一条 分支就会被执行,否则,第一条分支会被跳过。
如果第一条分支被跳过,if
复合命令就会把执行的机会留给下一条分支。如果有一个或多个 elif
分支,他们就会相继被执行并评估各自的命令列表,如果成功,就执行他们的分支。需要注意,一旦 if
复合命令的任一条分支被执行,剩下的分支就会被自动跳过:也就是说全部分支中只有一条会被执行。如果没有一条 if
或 elif
分支满足执行条件,若 else
分支存在,那么这条分支就会被执行。
实际上,if
复合命令是一条语句,描述了一系列可能被执行的分支,每一条分支前都有一个命令列表,用来评估这条分支是否被选中。绝大多数 if
语句都只有一条分支或是一条主分支外加一条 else
分支。
如前所述,与其他绝大多数条件语句相似,if
语句会评估一 列(List) 命令最终的退出代码,从而决定它所对应的条件分支是被执行还是跳过。几乎所有你即将遇到的 if
以及其他条件语句都以一条 简单命令 作为它的条件,但也还是可以用一个简单命令的列表作为条件。如果我们这样做,就一定要理解只有整个命令列表中最后的那个退出代码,会被用来评估是否执行这条分支:
$ read -p "Breakfast? [y/n] "; if [[ $REPLY = y ]]; then echo "Here are your eggs."; fi
Breakfast? [y/n] y
Here are your eggs.
$ if read -p "Breakfast? [y/n] "; [[ $REPLY = y ]]; then echo "Here are your eggs."; fi
Breakfast? [y/n] y
Here are your eggs.
上面两个例子有完全相同的执行效果。第一个例子中,我们的 read
命令在 if
语句之前被执行;后一个例子中,read
命令被内嵌在初始的分支条件中。本质上对风格或偏好的不同选择会决定你倾向于使用哪种方法,下面是我对此的一些想法:
elif
分支也是语句的组成部分时,这种做法会使条件语句结构上更对称或者说“平衡”。条件中最常见的命令是 test
命令,也作 [
命令。他们两个是同义的,只是一条命令有两个不同的名字而已。唯一的区别在于当你使用 [
作为命令名时,必须在最后用尾部参数 ]
终止命令。
然而,在当代 bash 脚本中,test
命令已经因各种原因,被后面这两个更年轻的兄弟取代了: [[
与 ((
。test
命令已被认为是过时的,它有缺陷且脆弱的语法也完全不能与 bash 解析器赋予 [[
和 ((
的特殊能力相提并论。
乍一想可能有些奇怪,但其实是很有趣的提示,注意看 [
和 [[
,我们其实已在这份指南的 if
以及其他示例语句中多次见过他们,他们并不是 if
语法的特殊形式,不是的!和其他命令一样,他们就是简单、普通的命令。[[
命令会接收一列参数且必须以参数 ]]
终止。相似的,[
也是命令名称,会接收测试参数且必须以参数 ]
终止。当我们错误省略掉命令名称与他们参数之间的空格时,这一点会尤其凸显:
$ [[ Jack = Jane ]] && echo "Jack is Jane" || echo "Jack is not Jane"
Jack is not Jane
$ [[Jack = Jane ]] && echo "Jack is Jane" || echo "Jack is not Jane"
-bash: [[Jack: command not found
$ [[ Jack=Jane ]] && echo "Jack is Jane" || echo "Jack is not Jane"
Jack is Jane
第一条语句是正确的,因此我们也得到了预期的输出结果。第二条语句中,我们忘记用空格区隔 [[
命令 名称 与它后面的 第一个参数,因此导致 bash 解析器去错误寻找一个叫作 [[Jack
的命令。因为当 bash 解析这条命令并用单词分割将命令名称和参数分作 token 时,第一个空格区隔开的 token 确实就是字符串 [[Jack
。
第三条命令的错误可能更隐蔽。当我们有 bug 的代码导致的不是 bash 解析器错误,而只是表现怪异时,这种情况总是惊人的。这类 bug 既难以发现,也难以理解。在我们的例子中,[[
命令的第一个参数是一个单独的字符串 Jack=Jane
。不幸的是,使用 [[
命令执行相等测试的语法是 [[ arg = arg ]]
,由此 bash 才会比较两个独立的字符串参数是否相等。第三个例子中,在 [[
命令之后,我们并没有三个参数:有的只是一个长参数。错误结果的产生是因为 [[
命令有一个测试某一字符串是否为空的简易语法:[[ string ]]
。现在应该很明显了,bash 错误理解了我们的意图,我们本是想使用 =
运算符比较两个字符串是否相同,bash 则以为我们是想确认某个单独的参数是否是一个空字符串。因为字符串 Jack=Jane
不为空,所以测试成功,结果 &&
分支被执行。
最后的总结是,一定要认识到这些测试命令本身也是 bash 命令,所以我们仍需要对他们施加标准的命令-参数间隔规则。