More ancillary Bash tips - 10 (HPR Show 2649)

Dave Morriss


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.


  1. The audio refers to the examples by the name they had before the one long show was split into two. What was bash9_ex2.sh has become bash10_ex1.sh.

  2. The audio refers to the examples by the name they had before the one long show was split into two. What was bash9_ex3.sh has become bash10_ex2.sh.

  3. Technically this should be whitespace which means one or more spaces, tabs or newlines.