Table of Contents
Making decisions in Bash
This is my tenth contribution to the Bash Scripting series under the heading of Bash Tips. The previous episodes are listed below in the Links section.
We are currently looking at decision making in Bash, and in the last episode we examined the tests themselves. In this episode we’ll look at the constructs that use these tests: looping constructs, conditional constructs and lists of commands.
Note: this episode and the preceding one were originally recorded as a single episode, but because it was so long it was split into two. As a consequence the audio contains references to examples such as bash9_ex2.sh where the true name is bash10_ex1.sh. The notes have been updated as necessary but not the audio.
Looping Constructs
Bash supports a number of commands which can be used to build loops. These are documented in the Looping Constructs section of the GNU Bash Manual. We will look only at while and until here because they contain tests. We will leave for loops until a later episode.
while command
The syntax of the while command is:
while test_commands
do
commands
done
The commands are executed as long as test_commands return an exit status which is zero (loop while the result is true).
until command
The syntax of the until command is:
until test_commands
do
commands
done
The commands are executed as long as test_commands return an exit status which is non-zero (loop until the result is true).
Examples of while and until
Example 1
The following code snippet will print variable i and increment it while its value is less than 5, so it will output the numbers 0..4:
i=0
while [ "$i" -lt 5 ]; do
echo "$i"
((i++))
done
Note that in this example the while and do parts are both on the same line, separated by a semicolon. Also, as mentioned in the last show, the quotes around "$i" are advisable in case the variable is null, but if the variable is not initialised the loop will fail whether the quotes are used or not. Even the shellcheck tool I use to check my Bash scripts does not complain about missing quotes here.
Example 2
The next snippet will start with variable i set to 5 and decrement it down to zero:
i=5
until [ "$i" -eq 0 ]; do
echo "$i"
((i--))
done
In this case the last value printed will be 1, after which i will be decremented to 0, which will stop the loop.
Conditional Constructs
Bash offers three commands under this heading, two of which have a conditional component. The commands are if, case and select. They are documented in the Conditional Constructs section of the GNU Bash Manual. We will look only at if and case in this episode and will leave select until a later episode.
if command
This command has the syntax:
if test_commands_1
then
commands_1
elif test_commands_2
then
commands_2
else
commands_3
fi
If test_commands_1 returns a status of zero then commands_1 will be executed and the if command will terminate. If the status is non-zero then any elif part will be tested, and the associated commands (commands_2 in this example) executed if the result is true. There may be zero or more of these elif parts.
Once the if and any elif parts are tested and they all return false, the commands in the else part (commands_3 here) will be executed. There may be zero or one else parts.
Note that the then part can be written on the same line as the if/elif, when separated by a semicolon.
case command
The syntax of the case command is as follows:
case word in
pattern_list_1 ) command_list_1 ;;
pattern_list_2 ) command_list_2 ;;
esac
The case command will selectively execute the command_list corresponding to the first pattern_list that matches word.
If a pattern_list contains multiple patterns then they are separated by the | character. The patterns are the Glob patterns we have already seen (show 2278). The pattern_list is terminated by the right parenthesis (and can be preceded by a left parenthesis if desired). The list of patterns and an associated command_list is known as a clause.
There is no limit to the number of case clauses. The first pattern that matches determines the command_list that is executed. There is no default pattern, but making '*' the final one – a pattern that will always match – achieves the same thing.
The clause terminator must be one of ';;', ';&', or ';;&', as explained below:
| Terminator | Meaning |
|---|---|
;; |
no subsequent matches are attempted after the first pattern match |
;& |
execution continues with the command_list associated with the next clause, if any |
;;& |
causes the shell to test the patterns in the next clause, if any, and execute any associated command_list on a successful match |
Examples of if and case
Example 3
In this example shows the full range of this structured command 'if' with elif and else branches:
fruit="apple"
if [ "$fruit" == "banana" ]; then
echo "$fruit: don't eat the skin"
elif [ "$fruit" == "apple" ]; then
echo "$fruit: eat the skin or not, as you please"
elif [ "$fruit" == "kiwi" ]; then
echo "$fruit: most people remove the skin"
else
echo "$fruit: not sure how to advise"
fi
See the downloadable example script bash10_ex1.sh1 which uses the above if structure in a for loop. Run it yourself to see what it does.
Example 4
Here is the same idea using a case command:
fruit="apple"
case $fruit in
banana) echo "$fruit: don't eat the skin" ;;
apple) echo "$fruit: eat the skin or not, as you please" ;;
kiwi) echo "$fruit: most people remove the skin";;
*) echo "$fruit: not sure how to advise"
esac
See the downloadable example script bash10_ex2.sh2 which uses a case command similar to the above in a for loop.
Example 5
This example has been added since the audio was recorded to give an example of the use of the ;;& clause terminator in a case command.
The following downloadable example (bash10_ex3.sh) demonstrates this:
$ cat bash10_ex3.sh
#!/bin/bash
#
# Further demonstration of the 'case' command with alternative clause
# terminators
#
i=704526
echo "Number given is: $i"
case $i in
*0*) echo "it contains a 0" ;;&
*1*) echo "it contains a 1" ;;&
*2*) echo "it contains a 2" ;;&
*3*) echo "it contains a 3" ;;&
*4*) echo "it contains a 4" ;;&
*5*) echo "it contains a 5" ;;&
*6*) echo "it contains a 6" ;;&
*7*) echo "it contains a 7" ;;&
*8*) echo "it contains a 8" ;;&
*9*) echo "it contains a 9" ;;
esac
exit
$ ./bash10_ex3.sh
Number given is: 704526
it contains a 0
it contains a 2
it contains a 4
it contains a 5
it contains a 6
it contains a 7
The script sets variable 'i' to a 6-digit number. The number is displayed with an echo command. The case command tests the variable with glob patterns containing all of the digits 0-9. Each case clause (except the last) is terminated with the ;;& sequence which means that each clause is invoked regardless of the success or failure of the preceding one.
The end result is that every pattern is tested and those that match generate output. If the case clauses had used the usual ;; terminators then the case command would exit after the first match.
Lists of Commands
Bash commands can be typed in lists. The simplest list is just a series of commands (or pipelines - a subject we will look at more in later shows in the Bash Tips series), each separated by a newline.
However, there are other list separators such as ';', '&', '&&', and '||'. The first two, ';' and '&' are not really relevant to decision making, so we will omit these for now. However so-called AND and OR lists are relevant. These consist of commands or pipelines separated by '&&' (logical AND), and '||' (logical OR).
AND Lists
An AND list has the form:
command1 && command2
command2 is executed if, and only if, command1 returns an exit status of zero.
OR Lists
An OR list has the form
command1 || command2
command2 is executed if, and only if, command1 returns a non-zero exit status.
An insight into how these lists behave
These operators short circuit:
- in the case of
'&&'an attempt is being made to determine the result of applying a logical AND operation between the two operands. They both need to be true before the overall result is true. If the first operand (command1) is false then there is no need to compute the second result, the overall result must be false, so there is a short circuit. - in the case of
'||'either or both of the operands of the logical OR operation can be true to give an overall result of true. Thus if command1 returns true nothing else need be done to determine the overall result, whereas if command1 is false, then command2 must be executed to determine the overall result.
I found it useful to consider this when using these types of lists, so I am sharing it with you.
Examples
It is common to see these used in scripts as a simplified form of decision with an explicit test as command1. For example, you might see:
[ -e /some/file ] || exit 1
Here the script will exit if the named file does not exist (we will look at the -e operator in the next episode). Note that it exits with a non-zero result so that the script itself could be used as command1 in an AND or OR list.
It is possible to execute several commands instead of just the exit by grouping them in curly braces ('{}'). For example:
[ -e /home/user1/somefile ] || { echo "Unable to find /home/user1/somefile"; exit 1; }
It is necessary to type a space3 after '{' and before '}'. Also each command within the braces must end with a semicolon (or a newline).
This example could be written as follows, remembering that test is an alternative to '[...]':
test -e /home/user1/somefile || {
echo "Unable to find /home/user1/somefile"
exit 1
}
As we have already seen it is possible to use any test or command which returns an exit status of zero or non-zero as command1 in a list. So the following command list is equivalent to the 'if' example above:
grep -q -e '^banana$' fruit.txt && echo "Found a banana"
However, it is my opinion that it is clearer and more understandable when the 'if' alternative is used.
Links
- “GNU BASH Reference Manual”
- HPR series: Bash Scripting
- Previous episodes under the heading Bash Tips:
- HPR episode 1648 “Bash parameter manipulation”
- HPR episode 1843 “Some Bash tips”
- HPR episode 1884 “Some more Bash tips”
- HPR episode 1903 “Some further Bash tips”
- HPR episode 1951 “Some additional Bash tips”
- HPR episode 2045 “Some other Bash tips”
- HPR episode 2278 “Some supplementary Bash tips”
- HPR episode 2293 “More supplementary Bash tips”
- HPR episode 2639 “Some ancillary Bash tips - 9”
- Resources:
- Examples: bash10_ex1.sh, bash10_ex2.sh, bash10_ex3.sh
The audio refers to the examples by the name they had before the one long show was split into two. What was
bash9_ex2.shhas becomebash10_ex1.sh.↩The audio refers to the examples by the name they had before the one long show was split into two. What was
bash9_ex3.shhas becomebash10_ex2.sh.↩Technically this should be whitespace which means one or more spaces, tabs or newlines.↩