[edit this page]

Tests And Conditionals

Different commands for different data.

Exit codes, success and failure, testing files, strings and numbers, handling different conditions, conditional operators and conditional compound commands.

[edit this page]
This chapter is actively being drafted. It is incomplete, will be updated from time to time and content is likely to move around.
If you are interested in getting updates on the progress of this guide, you can star the source repository here.

What are conditionals and what do I use them for?

The best way to think about conditionals is essentially just as a choice.

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.

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 choice we made but rather because of the actions we take after making the choice.

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.

A script with conditional branches is broad and versitile compared to a linear script without them, in all the ways that games are versitile 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.

Let's start you off with a really simple conditional to help us start the day right:

$ 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.

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".

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.

Usually, we evaluate conditionals explicitly by using compound commands such as the if ... statement in the example above. Another way is using Control Operators, which we very briefly touched down on in a previous chapter when we were discussing List commands. We will enumerate and discuss each type of conditional in-depth here.

The if compound.

The if 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.

    if list [ ;|<newline> ] then list ;|<newline>
    [ elif list [ ;|<newline> ] then list ;|<newline> ] ...
    [ else list ;|<newline> ]

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

The syntax for the if compound is, while at first a little verbose, in essence very simple. We start with the if 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 if compound to be evaluated. If the exit code is zero (0 = success), the first branch will be executed. Otherwise, the first branch will be skipped.

If the first branch is skipped, the if compound will pass the opportunity of execution to the next branch. If one or more elif 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 if compound is executed, the remaining branches are automatically skipped: only one single branch is ever executed. If neither if or elif branches are eligible for execution, the else branch will be executed instead, if it is present.

Effectively, an if 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 if statements will have only a single branch or a primary and an else branch.

Conditional command lists

As stated, the if statement, akin to most other conditional statements, evaluate a List command's final exit code to determine whether its corresponding conditional branch should be taken or skipped. Nearly all if and other conditional statements you'll encounter will have nothing more than a Simple Command 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:

$ 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.

Both of these are identical in operation. In the first, our read command precedes the if statement; in the latter, our read 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:

Conditional test commands

The most common command used as a conditional is the test command, also known as the [ 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 [ as the command name, it is imperative to terminate the command with a trailing ] argument.

In modern bash scripts, however, the test command has, for all intents and purposes, been superceded by its two younger brothers: the [[ and (( keywords. The test command has been effectively rendered obsolete and its flawed and fragile syntax is no match for the special powers granted to both [[ and (( by the bash parser.

It may seem strange at first thought, but it is actually quite interesting a revelation to notice that [ and [[, as we've seen them appear several times in if and other sample statements in this guide, are not some special form of if-syntax - no! They are simple, ordinary commands, just like any other command. The [[ command name takes a list of arguments and its final argument must be ]]. Similarly, [ is a command name which takes test arguments and must be closed with a trailing ] argument. This is especially noticable when we make a mistake and omit spaces between these command names and their arguments:

$ [[ 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

The first statement was written correctly and we got the expected output. In the second statement, we forgot to separate the [[ command name from the first argument, causing the bash parser to go looking for a command named [[Jack. 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 [[Jack.

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 [[ command is the single string Jack=Jane. Unfortunately, the syntax for performing an equality test using the [[ command is [[ arg = arg ]], 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 [[ command: we only have one long argument. And it just so happens that [[ has a short-hand syntax for testing whether a certain string is empty or not: [[ string ]]. Now, perhaps, it becomes clear that bash has mistaken our intention of comparing two strings using the = operator with a different kind of test that simply checks whether our single argument is an empty string. Since the string Jack=Jane is not empty, the test succeeds, and as a result, the && branch is taken.

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.

Fork me on GitHub