[edit this page]

测试与条件

对不同的数据执行不同的命令

退出代码,成功与失败,测试文件,字符串与数值,处理不同条件,条件运算符,条件复合命令

[edit this page]
本章仍在写作中。它尚不完整,还有待时不时地更新,内容也很有可能会被移动。
如果你对本篇指南的进展感兴趣,可以点击这里为源仓库加星。

什么是条件,为什么我要使用他们?

理解条件最好的办法就是把他们实质理解为 选择(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 复合命令的任一条分支被执行,剩下的分支就会被自动跳过:也就是说全部分支中只有一条会被执行。如果没有一条 ifelif 分支满足执行条件,若 else 分支存在,那么这条分支就会被执行。

实际上,if 复合命令是一条语句,描述了一系列可能被执行的分支,每一条分支前都有一个命令列表,用来评估这条分支是否被选中。绝大多数 if 语句都只有一条分支或是一条主分支外加一条 else 分支。

条件命令列表(Conditional command lists)

如前所述,与其他绝大多数条件语句相似,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 命令被内嵌在初始的分支条件中。本质上对风格或偏好的不同选择会决定你倾向于使用哪种方法,下面是我对此的一些想法:

条件测试命令(Conditional test commands)

条件中最常见的命令是 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 命令,所以我们仍需要对他们施加标准的命令-参数间隔规则。

Fork me on GitHub