-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconditionals.html
More file actions
111 lines (80 loc) · 12.2 KB
/
conditionals.html
File metadata and controls
111 lines (80 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
---
id: conditionals
layout: chapter
chapter: 4
title: 测试与条件
subtitle: 对不同的数据执行不同的命令
status: draft
description: >-
退出代码,成功与失败,测试文件,字符串与数值,处理不同条件,条件运算符,条件复合命令
published: true
---
<section>
<h1>什么是条件,为什么我要使用他们?</h1>
<p>理解条件最好的办法就是把他们实质理解为 <em>选择(choice)</em>。</p>
<p>每当一个选择出现,我们可以从众多路径中择其一继续向前。每条路径都是可选的,每条都通向不同的地方。最终走的那条,就取决于我们的选择。</p>
<p>这些不像你在小说中读到的选择:那些小说都已为你选好。小说的故事情节是确定了的,那些选择不会引出其他结局。条件更像是游戏中的选择:时不时地,你要做出一个关键选择,而这些选择都会以某种方式改变游戏的境况。如果你结束游戏重新来过,在某一点做出不同于此前的选择,那么游戏的走向由此就会不同。我们将其称为分支(branching)。每个选择都会开启一条新的分支,以不同方式影响我们的环境。但是要注意,这些分支的差异并不在于 <em>我们做出的选择</em>,而在于 <em>选择做出后我们采取的行动</em>。</p>
<p>听起来似乎有些复杂,但实际上这和我们选择买苹果手机还是安卓手机并没什么不同,或是吃早餐还是不吃,走高速公路还是辅路。我们会考虑各个选项,结合自身情况考虑什么是给定条件下最好的,然后尝试做出选择。条件也是这样:评估我们所拥有的,选择由此去向哪里。</p>
<p>包含条件分支的脚本要远比线性脚本广阔灵活,正如游戏要比线性叙述的书籍灵活。那么,我们为什么需要条件呢?我们需要使用他们来写出能够动态处理变化情境,以及根据情境改变运行方式的脚本。</p>
<p>让我们首先从一个非常简单的条件入手,来开始我们的学习:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>read -p "Would you like some breakfast? [y/n] "</kbd>
Would you like some breakfast? [y/n] <kbd>n</kbd>
<span class="prompt">$ </span><kbd>if [[ $REPLY = y ]]; then</kbd>
<span class="prompt">> </span><kbd> echo "Here you go, an egg sandwich."</kbd><em>Branch #1</em>
<span class="prompt">> </span><kbd>else</kbd>
<span class="prompt">> </span><kbd> echo "Here, you should at least have a coffee."</kbd><em>Branch #2</em>
<span class="prompt">> </span><kbd>fi</kbd>
Here, you should at least have a coffee.
</pre>
<p>条件相比我们之前已写的所有代码,关键的不同在于部分代码永远不会被执行,除非情况发生变化。上面例子中,即使第一条分支内也有对应执行代码,但 bash 并未运行他们,只有第二条分支中的代码被执行。除非情况发生改变——对应上面例子就是我们对前面问题的回答变了,这时脚本中被执行的分支就会切换到第一条,第二条分支中的代码则“死掉”。</p>
<p>Bash 有多种不同方式评估条件。几乎所有这些方式都有一个共同点:他们都是基于另一条命令的退出代码被评估。因此,在深入这一章的内容之前,你首先要足够熟悉前面章节谈论过的退出代码概念。</p>
<p>我们通常使用复合命令明确地评估条件,如上面例子中的 <code>if ...</code> 语句。另一种方式是使用 <dfn>控制运算符 (Control Operaotr)</dfn>,我们在前面章节中讨论 <dfn>List</dfn> 命令时有简单提及。我们接下来会在这里深度列举并讨论每种类型的条件。</p>
<h2><code>if</code> 复合命令</h2>
<p><code>if</code> 语句在编程语言中如此常见,以至于当我们考虑在代码中构建一个选择时,基本可以保证第一个就会想到它。这并不意外,毕竟这些语句清晰、简单又明确。因此对于熟悉 bash 中的条件,他们也是非常好的起点。</p>
<pre class="syntax">
<strong>if</strong> <var>list</var> [ <strong>;</strong>|<strong><newline></strong> ] <strong>then</strong> <var>list</var> <strong>;</strong>|<strong><newline></strong>
[ <strong>elif</strong> <var>list</var> [ <strong>;</strong>|<strong><newline></strong> ] <strong>then</strong> <var>list</var> <strong>;</strong>|<strong><newline></strong> ] ...
[ <strong>else</strong> <var>list</var> <strong>;</strong>|<strong><newline></strong> ]
<strong>fi</strong>
<samp><u title="start of compound command 'if'">if</u> ! rm hello.txt; then <mark>echo "Couldn't delete hello.txt." >&2; exit 1</mark>; <u title="end of compound command 'if'">fi</u></samp>
<samp><u title="start of compound command 'if'">if</u> rm hello.txt; then <mark>echo "Successfully deleted hello.txt."</mark>
else <mark class="red">echo "Couldn't delete hello.txt." >&2; exit 1</mark>; <u title="end of compound command 'if'">fi</u></samp>
<samp><u title="start of compound command 'if'">if</u> mv hello.txt ~/.Trash/; then <mark>echo "Moved hello.txt into the trash."</mark>
elif rm hello.txt; then <mark class="blue">echo "Deleted hello.txt."</mark>
else <mark class="red">echo "Couldn't remove hello.txt." >&2; exit 1</mark>; <u title="end of compound command 'if'">fi</u></samp>
</pre>
<p><code>if</code> 复合命令的语法虽然第一眼看起来有些冗余,实际上非常简单。首先起始于 <code>if</code> 关键词,后面跟的是一个命令列表。这个命令列表会被 bash 执行,完成后 bash 会把 退出代码发给 <code>if</code> 复合命令做评估。如果退出代码是 0(<code>0</code> = <dfn>成功(success)</dfn>),<em>第一条</em> 分支就会被执行,否则,第一条分支会被跳过。</p>
<p>如果第一条分支被跳过,<code>if</code> 复合命令就会把执行的机会留给下一条分支。如果有一个或多个 <code>elif</code> 分支,他们就会相继被执行并评估各自的命令列表,如果成功,就执行他们的分支。需要注意,一旦 <code>if</code> 复合命令的任一条分支被执行,剩下的分支就会被自动跳过:也就是说全部分支中只有一条会被执行。如果没有一条 <code>if</code> 或 <code>elif</code> 分支满足执行条件,若 <code>else</code> 分支存在,那么这条分支就会被执行。</p>
<p>实际上,<code>if</code> 复合命令是一条语句,描述了一系列可能被执行的分支,每一条分支前都有一个命令列表,用来评估这条分支是否被选中。绝大多数 <code>if</code> 语句都只有一条分支或是一条主分支外加一条 <code>else</code> 分支。</p>
<h2>条件命令列表(Conditional command lists)</h2>
<p>如前所述,与其他绝大多数条件语句相似,<code>if</code> 语句会评估一 <dfn>列(List)</dfn> 命令最终的退出代码,从而决定它所对应的条件分支是被执行还是跳过。几乎所有你即将遇到的 <code>if</code> 以及其他条件语句都以一条 <dfn>简单命令</dfn> 作为它的条件,但也还是可以用一个简单命令的列表作为条件。如果我们这样做,就一定要理解只有整个命令列表中最后的那个退出代码,会被用来评估是否执行这条分支:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>read -p "Breakfast? [y/n] "; if [[ $REPLY = y ]]; then echo "Here are your eggs."; fi</kbd>
Breakfast? [y/n] <kbd>y</kbd>
Here are your eggs.
<span class="prompt">$ </span><kbd>if read -p "Breakfast? [y/n] "; [[ $REPLY = y ]]; then echo "Here are your eggs."; fi</kbd>
Breakfast? [y/n] <kbd>y</kbd>
Here are your eggs.
</pre>
<p>上面两个例子有完全相同的执行效果。第一个例子中,我们的 <code>read</code> 命令在 <code>if</code> 语句之前被执行;后一个例子中,<code>read</code> 命令被内嵌在初始的分支条件中。本质上对风格或偏好的不同选择会决定你倾向于使用哪种方法,下面是我对此的一些想法:</p>
<ul>
<li>将数据获取命令内嵌,使条件语句有一种“完整”感:条件成为一个完整单元,其中包含了它所有的依赖。</li>
<li>将数据获取命令前置于条件语句,使这两部分成了完全独立的操作。当其他 <code>elif</code> 分支也是语句的组成部分时,这种做法会使条件语句结构上更对称或者说“平衡”。</li>
</ul>
<h2>条件测试命令(Conditional test commands)</h2>
<p>条件中最常见的命令是 <code>test</code> 命令,也作 <code>[</code> 命令。他们两个是同义的,只是一条命令有两个不同的名字而已。唯一的区别在于当你使用 <code>[</code> 作为命令名时,必须在最后用尾部参数 <code>]</code> 终止命令。</p>
<p>然而,在当代 bash 脚本中,<code>test</code> 命令已经因各种原因,被后面这两个更年轻的兄弟取代了: <code>[[</code> 与 <code>((</code> 。<code>test</code> 命令已被认为是过时的,它有缺陷且脆弱的语法也完全不能与 bash 解析器赋予 <code>[[</code> 和 <code>((</code> 的特殊能力相提并论。</p>
<p>乍一想可能有些奇怪,但其实是很有趣的提示,注意看 <code>[</code> 和 <code>[[</code>,我们其实已在这份指南的 <code>if</code> 以及其他示例语句中多次见过他们,他们并不是 <code>if</code> 语法的特殊形式,不是的!和其他命令一样,他们就是简单、普通的命令。<code>[[</code> 命令会接收一列参数且必须以参数 <code>]]</code> 终止。相似的,<code>[</code> 也是命令名称,会接收测试参数且必须以参数 <code>]</code> 终止。当我们错误省略掉命令名称与他们参数之间的空格时,这一点会尤其凸显:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>[[ Jack = Jane ]] && echo "Jack is Jane" || echo "Jack is not Jane"</kbd>
Jack is not Jane
<span class="prompt">$ </span><kbd>[[Jack = Jane ]] && echo "Jack is Jane" || echo "Jack is not Jane"</kbd>
-bash: [[Jack: command not found
<span class="prompt">$ </span><kbd>[[ Jack=Jane ]] && echo "Jack is Jane" || echo "Jack is not Jane"</kbd>
Jack is Jane
</pre>
<p>第一条语句是正确的,因此我们也得到了预期的输出结果。第二条语句中,我们忘记用空格区隔 <code>[[</code> 命令 <em>名称</em> 与它后面的 <em>第一个参数</em>,因此导致 bash 解析器去错误寻找一个叫作 <code>[[Jack</code> 的命令。因为当 bash 解析这条命令并用单词分割将命令名称和参数分作 token 时,第一个空格区隔开的 token 确实就是字符串 <code>[[Jack</code>。</p>
<p>第三条命令的错误可能更隐蔽。当我们有 bug 的代码导致的不是 bash 解析器错误,而只是表现怪异时,这种情况总是惊人的。这类 bug 既难以发现,也难以理解。在我们的例子中,<code>[[</code> 命令的第一个参数是一个单独的字符串 <code>Jack=Jane</code>。不幸的是,使用 <code>[[</code> 命令执行相等测试的语法是 <code class="syntax"><strong>[[</strong> <var>arg</var> <strong>=</strong> <var>arg</var> <strong>]]</strong></code>,由此 bash 才会比较两个独立的字符串参数是否相等。第三个例子中,在 <code>[[</code> 命令之后,我们并没有三个参数:有的只是一个长参数。错误结果的产生是因为 <code>[[</code> 命令有一个测试某一字符串是否为空的简易语法:<code class="syntax"><strong>[[</strong> <var>string</var> <strong>]]</strong></code>。现在应该很明显了,bash 错误理解了我们的意图,我们本是想使用 <code>=</code> 运算符比较两个字符串是否相同,bash 则以为我们是想确认某个单独的参数是否是一个空字符串。因为字符串 <code>Jack=Jane</code> <em>不为空</em>,所以测试成功,结果 <code>&&</code> 分支被执行。</p>
<p>最后的总结是,一定要认识到这些测试命令本身也是 bash 命令,所以我们仍需要对他们施加标准的命令-参数间隔规则。</p>
</section>