Making your script responsive

Tutorials – Shell Scripting

Article from Issue 221/2019
Author(s):

Knowing the right shell commands may be all the artificial intelligence you need to make your computer work for you.

If each computer program could only perform one, unchangeable sequence of actions, software and computers would be almost useless compared to what they can do today. For this reason, every programming language since the invention of vacuum tubes has keywords and syntax structures that allow the programmer to implement flow control.

In a nutshell, flow control is the capability of a program to autonomously understands which actions to perform, or repeat, according to the current values of variables.

Of course, the kind of autonomous decision making you can implement with shell flow control is no artificial intelligence. However, if shell flow control is inadequate for what you need to do, that is a sign that a shell script might not be the right solution for your problem.

To the extent of the coverage in these Bash tutorials [1] [2], flow control has two forms, and each form has a simple and a more complex variant. The simpler variant consists of explaining to a script how to decide which action or sequence of actions to execute among a set of two or more possible choices. The more complex, but flexible variant is about iterations: You use keywords to make a script repeat some sequence of actions, possibly over all the elements of some set, one at a time, for a fixed or variable number of times.

In practice, both forms of flow control can be, and frequently are, nested in all ways imaginable. Unsurprisingly, it is also possible for a script to either alter – or just interrupt – the default sequence of actions started by any flow control statement. I will show how to do all this in this installment. Please note that, for space reasons, I only focus here on relatively high-level issues (i.e., when and how to use and mix the several flow control constructs). For the actual Bash test operators that can trigger any of these constructs, see the Advanced Bash-Scripting guide [3] for a list with plenty of examples.

Bash 5.0

Almost all of the content covered in this tutorial series is valid for all versions of the Bash shell currently installed with Linux. The main, if not only, exception is a few Bash array features described in the previous installment of this tutorial series [1, 2], which are supported only on Bash v4 or later.

In the interest of complete, up-to_date information, the Bash landscape became just a little bit more complicated on January 7th, 2019, with the release of Bash 5.0. Besides fixing assorted bugs, version 5.0 introduces several new features. The most relevant changes deal with Bash special variables. The $@ and $* variables, which I discussed in the previous installment [2], are expanded in different ways. In addition, there are now new variables called BASH_ARGV0, EPOCHSECONDS, and EPOCHREALTIME, plus an option to expand associative arrays.

Easy Decision Making

The Bash if/then/elif/else construct (Listing 1) shown in Figure 1 (where elif is simply a shortcut for "else if") does just what its name implies. That chunk of code in Listing 1 tells Bash the following:

Listing 1

The if/then/elif/else construct

 

Figure 1: The structure of the if/then/else flow control operator is simple. What matters is knowing when to use it and the best way to order the several tests.
  • If the $FAVOURITE_OS variable is exactly equal to "Linux", then execute all the commands between the then keyword and the next keyword (elif in this case).
  • Otherwise, if $FAVOURITE_OS is equal to "FreeBSD", print "Not the best choice, but almost there" to standard output.
  • For any other value of $FAVOURITE_OS print "You poor thing"

The fi keyword, which is the opposite of if, closes the whole flow control block. The elif part is only necessary if you need to concatenate two or more checks, as in the example above. Syntax-wise, you may have as many nested checks as you desire in one if/else sequence, each executed only if all the checks below it fail. In practice, a long sequence of nested ifs is necessary only when you need to test a different variable, or combination of variables, in each check. In other cases, there are better solutions that I will explain later.

Rather than choosing which way to go, the other high-level type of "flow control" manages all the cases in which you want to repeat some sequence of actions, from beginning to end every time. In Bash, you can repeat a certain sequence of commands:

  • For a specific number of times
  • Over all the elements of some set
  • Until some event happens (or stops happening)

The first two categories are handled with for loops, and the third one with the while or until keywords. A shell for loop has the following general syntax:

for <SOME NUMBER OF TIMES OR SET OF ELEMENTS>
  do
  # sequence of commands here
  done

Both the number of repetitions and the composition of the set may be calculated on the spot, right before starting the loop. What makes a for loop different from another is the nature the SET OF ELEMENTS, as these nested loops show:

for MONTH in January December
  do
  for DAY in {31..1}
    do
      printf "%10.10s %2.2s\n" $MONTH $DAY
    done
  done

In the first loop, for iterates over all the elements of the fixed set composed by the two elements January and December. In the inner loop, the $DAY variable is used as a counter going from 31 to 1. The result is a list of all days from January 31st to December 1st:

January  31
January  30
....
January   1
December 31
December 30

When you need a numeric counter, you may also use this alternative syntax, very similar to the C language's syntax:

for ((I=0;I<5;I++))
  do
  #some command
  done

The loop above would run five times, for all the values of $I from zero to four.

The formats above are already very flexible, but they become really powerful when you make them work on sets that are not hard-coded in the script. To begin with, for may operate on all the elements of an array created, or modified, by the script itself just before entering the loop. This, for example:

for $CUSTOMER in "${!MY_CUSTOMERS[@]}"
  do
    #process the current $CUSTOMER
  done

is how you would process all the customers in your $MY_CUSTOMERS associative array, one at a time. For details about the syntax, see the previous installment of this tutorial [2].

The set of elements for a for loop can also be generated on the fly from any possible source, as in the following example

for file in $( find / -type f -mtime +30 -name '*.jpg' | sort )
  do
    # process the current JPG file
  done

which finds, sorts alphabetically, and then processes in that order all the files in your system which have a .jpg extension and are older than 30 days. Perhaps the most important message of that example is the one "hidden" in the pipeline between the find and sort commands: You can loop on sets built on the fly by sequences of commands that may be even longer and more complex than those found inside the loop itself.

Multiple Decisions

When you need to check more than two or three different values of the same variable, the if/then approach is more verbose than necessary and sometimes much less clear, too. In those situations it is better to handle all those possibilities with one case statement. Listing 2 shows the syntax for case.

Listing 2

case Syntax

 

The case keyword defines the test variable (OS in this example) that will control what to do. That statement is followed by branches, each ending with a double semicolon, which may contain as many statements as you want.

Each branch begins with a list of all the possible values of the test variable, separated by the pipe character (|), which will trigger the execution of the following commands.

Order is crucial here! The several branches are evaluated from top to bottom, stopping at the first one that matches.

Syntax-wise, you can close case statements with just the esac keyword, but that is not all you need to avoid problems. In addition, always end with a branch marked with *), which is executed if no other matches are found. Even adding just an error message here will really help to debug your scripts.

Event-Driven Iteration

I will return to iterations now. What if you need to repeat the execution of some sequence of commands not for a given number of times or over some set of values, but for as long as some condition is true (or false)?

In these instances, you need the while and until commands. while tests for some condition at the top of a loop and keeps looping until that condition is false. until has the same syntax, but loops as long as its condition is true. These two loops will do the same thing:

while [ condition_x is true ]
  do
  #something...
  done
until [ condition_x is false]
  do
  command...
  done

As with anything powerful, these two commands also have a dark side: What if the "condition" is, say, $X is equal to 3, and you set X=1 in the line right before the while or until statements? In such cases, the whole loop controlled by the statements would not be executed, not even once. The opposite is also true. If, for whatever reason, X happens to be equal to 3 when the Bash interpreter starts looking at the while statement, the script will be trapped in repeating the loop forever, unless you abort it manually.

This may or may not be what you want, so you just need to be aware of the possibilities and, as I will show you in the final example, code accordingly.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Decision Making Scripts

    The Bash shell uses different criteria to make decisions. Learn how to teach your shell scripts to make the right choice.

  • Bash vs. Vista PowerShell

    Microsoft’s new PowerShell relies on .NET framework libraries and thus has access to a treasure trove of functions and objects. How does PowerShell measure up to traditional shells like Bash?

  • Tutorials – Shell Scripts

    Letting your scripts ask complex questions and give user feedback makes them more effective.

  • Bash Tuning

    In the old days, shells were capable of little more than calling external programs and executing basic, internal commands. With all the bells and whistles in the latest versions of Bash, however, you hardly need the support of external tools.

  • Bash Alternatives

    Don't let your familiarity with the Bash shell stop you from exploring other options. We take a look at a pair of alternatives that are easy to install and easy to use: Zsh and fish.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News