Making your script responsive
Tutorials – Shell Scripting
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
- If the
$FAVOURITE_OS
variable is exactly equal to"Linux"
, then execute all the commands between thethen
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 if
s 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
(incl. VAT)
Buy Linux Magazine
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.
News
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.