-
Notifications
You must be signed in to change notification settings - Fork 261
/
conditionals.html
117 lines (86 loc) · 13.2 KB
/
conditionals.html
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
112
113
114
115
116
117
---
id: null
layout: chapter
chapter: 4
title: Tests And Conditionals
subtitle: Different commands for different data.
status: draft
description: >-
Exit codes, success and failure, testing files, strings and numbers, handling
different conditions, conditional operators and conditional compound commands.
published: true
---
<section>
<h1>What are conditionals and what do I use them for?</h1>
<p>The best way to think about conditionals is essentially just as a <em>choice</em>.</p>
<p>Whenever a choice comes up, we can take one of various different paths forward. Either path is available to us, and each path leads somewhere different. The path that we end up taking, is determined by how our choice turns out.</p>
<p>These are not choices like you read about in a novel: the novel has already chosen for you. The story of a novel is fixed, the choices don't open up alternate endings. Conditionals are choices like in games: every so often, you need to make a critical decision, and each decision changes the game situation in one way or another. If you finish a game and retry it, making one of the choices differently, the game's situation would be different from that point on. We call this
branching. Every choice sets us on a new branch that affects our environment in a different way than the others. Note, though, that these branches are not different because of the <em>choice we made</em> but rather because of the <em>actions we take after making the choice</em>.</p>
<p>That sounds kind of complicated, but it's really no different from choosing to buy an iPhone or an Android phone. Choosing to have breakfast in the morning or skip it. Choosing to take the highway or the side way. We try to make these choices by considering our options. Considering what we have and what's best in the given situation. This is exactly what conditionals are: we evaluate what we have and we choose where to go with that.</p>
<p>A script with conditional branches is broad and versatile compared to a linear script without them, in all the ways that games are versatile compared to the linear narative of a book. So, why do we need conditionals? We need them to write scripts that can dynamically handle varying situations and depending on what the situation is, change how it operates.</p>
<p>Let's start you off with a really simple conditional to help us start the day right:</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>What's key with conditionals versus all the code we've written before, is that we now have code that will never get executed unless the situation changes. Only the code in the second branch was executed, and even though we have actual code in the first branch, bash has never actually executed it. Only if the situation - in this case, our answer to the preceding question - changes, will the executed branch of our script change and will we see the code in the first branch get executed,
but at the same time, that will cause the second branch to become "dead".</p>
<p>Bash has a few different ways of evaluating conditionals. Nearly all of them have a key commonality: they are all evaluated based on the exit code of another command. As such, before diving into this chapter, it is important that you are comfortable with your knowledge on exit codes as discussed in a previous chapter.</p>
<p>Usually, we evaluate conditionals explicitly by using compound commands such as the <code>if ...</code> statement in the example above. Another way is using <dfn>Control Operators</dfn>, which we very briefly touched down on in a previous chapter when we were discussing <dfn>List</dfn> commands. We will enumerate and discuss each type of conditional in-depth here.</p>
<h2>The <code>if</code> compound.</h2>
<p>The <code>if</code> statement is so ubiquitous across programming languages that it is almost guaranteed the first thing that comes to mind when we think about building a choice into our code. This is no accident: these statements are clear, simple and explicit. That also makes them an excellent starting point for us to get familiar with conditionals in 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>The syntax for the <code>if</code> compound is, while at first a little verbose, in essence very simple. We start with the <code>if</code> keyword, followed by a command list. This command list will be executed by bash, and upon completion, bash will hand the final exit code to the <code>if</code> compound to be evaluated. If the exit code is zero (<code>0</code> = <dfn>success</dfn>), the <em>first</em> branch will be executed. Otherwise, the first branch will be skipped.</p>
<p>If the first branch is skipped, the <code>if</code> compound will pass the opportunity of execution to the next branch. If one or more <code>elif</code> branches are available, these will in-turn execute and evaluate their own command list, and if successful, execute their branch. Note that as soon as any branch of the <code>if</code> compound is executed, the remaining branches are automatically skipped: only one single branch is ever executed. If neither <code>if</code> or
<code>elif</code> branches are eligible for execution, the <code>else</code> branch will be executed instead, if it is present.</p>
<p>Effectively, an <code>if</code> compound is a statement that expresses a series of potential branches to execute, each preceded by a command list that evaluates whether or not that branch should be chosen. Most <code>if</code> statements will have only a single branch or a primary and an <code>else</code> branch.</p>
<h2>Conditional command lists</h2>
<p>As stated, the <code>if</code> statement, akin to most other conditional statements, evaluate a <dfn>List</dfn> command's final exit code to determine whether its corresponding conditional branch should be taken or skipped. Nearly all <code>if</code> and other conditional statements you'll encounter will have nothing more than a <dfn>Simple Command</dfn> as its conditional, but it is nevertheless possible to provide a whole list of simple commands. When we do so, it is important to understand that only the final exit code after executing the entire list is relevant for the branch's evaluation:</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>Both of these are identical in operation. In the first, our <code>read</code> command precedes the <code>if</code> statement; in the latter, our <code>read</code> command is embedded in the initial branch conditional. It is essentially a choice of style or preference that will determine which of these methods you prefer. Some thoughts on the matter:</p>
<ul>
<li>Embedding the data-gathering command creates a "wholesome" approach to the conditional: the conditional becomes a unit which consists of all its dependencies.</li>
<li>Preceding the data-gathering command to the conditional statement separates the two distinct operations. It also makes the conditional more symmetric or "balanced" when other <code>elif</code> branches become part of the statement.</li>
</ul>
<h2>Conditional test commands</h2>
<p>The most common command used as a conditional is the <code>test</code> command, also known as the <code>[</code> command. These two are synonymous with each other: they are the same command with a different name. The only difference is that when you use <code>[</code> as the command name, it is imperative to terminate the command with a trailing <code>]</code> argument.</p>
<p>In modern bash scripts, however, the <code>test</code> command has, for all intents and purposes, been superceded by its two younger brothers: the <code>[[</code> and <code>((</code> keywords. The <code>test</code> command has been effectively rendered obsolete and its flawed and fragile syntax is no match for the special powers granted to both <code>[[</code> and <code>((</code> by the bash parser.</p>
<p>It may seem strange at first thought, but it is actually quite interesting a revelation to notice that <code>[</code> and <code>[[</code>, as we've seen them appear several times in <code>if</code> and other sample statements in this guide, are not some special form of <code>if</code>-syntax - no! They are simple, ordinary commands, just like any other command. The <code>[[</code> command name takes a list of arguments and its final argument must be <code>]]</code>.
Similarly, <code>[</code> is a command name which takes test arguments and must be closed with a trailing <code>]</code> argument. This is especially noticable when we make a mistake and omit spaces between these command names and their arguments:</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>The first statement was written correctly and we got the expected output. In the second statement, we forgot to separate the <code>[[</code> command <em>name</em> from the <em>first argument</em>, causing the bash parser to go looking for a command named <code>[[Jack</code>. After all, when bash parses this command and word-splits the command's name and arguments into tokens, the first space-delimited token is the entire string <code>[[Jack</code>.</p>
<p>The third command is perhaps more insiduous. It is always alarming when there are situations where we can write buggy code that does not yield a bash parser error but instead simply behaves "strangely". These types of bugs are hard to spot and can be hard to understand as well. In our situation, the first argument to the <code>[[</code> command is the single string <code>Jack=Jane</code>. Unfortunately, the syntax for performing an equality test using the <code>[[</code> command
is <code class="syntax"><strong>[[</strong> <var>arg</var> <strong>=</strong> <var>arg</var> <strong>]]</strong></code>, where bash will then perform the equality test between the two distinct string arguments. In this third example, however, we do not have three arguments following the <code>[[</code> command: we only have one long argument. And it just so happens that <code>[[</code> has a short-hand syntax for testing whether a certain string is empty or not: <code class="syntax"><strong>[[</strong> <var>string</var> <strong>]]</strong></code>. Now, perhaps, it becomes clear that bash has mistaken our intention of comparing two strings using the <code>=</code> operator with a different kind of test that simply checks whether our single argument is an empty string. Since the string <code>Jack=Jane</code> is <em>not empty</em>, the test succeeds, and as a result, the <code>&&</code> branch is taken.</p>
<p>The take-away from all this is that it is important to realize that these test commands are their own bash commands and we need to continue applying the standard command-argument spacing rules to them.</p>
</section>