Teach Yourself UNIX Shell Programming in 14 Days

Chapter 9: Decision Making in Shell Scripts

Objectives:

This chapter discusses conditional statements and looping in shell scripts, enabling us to use branching and iteration in scripts. Objectives important to this chapter are:

  • if-then-else-fi branching structure
  • exit status
  • using conditional tests
  • loops
  • breaking out of loops
Concepts:

Even for those who have not programmed before, the concepts of looping and decision making should not be unfamiliar. It is only the script syntax that is unfamiliar.

Before we talk about how to do it in a shell script, let's review the three basic ideas in programming of any kind: sequence, selection (or branching), and iteration.

A program tends to execute from the top line down, in the order lines are encountered. This is sequence.

This sequential processing of instructions can be varied by selecting which of several alternative lines to execute, based on the results of a test. Branching is deciding to do one of several things, based on a logical rule, such as deciding to run a program if the user knows the secret password and not to run it if he/she doesn't.

Iteration is the same as looping, which is simply doing a process a number of times, until it does not need to be done any more, such as searching through each file in a list for the same pattern.

The basic decision making command is the if statement. It can take the form

if condition	
then		
commands to execute if condition is true	
else		
commands to execute if condition if false	
fi

The oddest part of the above is the fi. It means that the if command ends here. In other languages it is often written as endif, or end if. In keeping with UNIX's terse nature, it is simply if spelled backward and marks the end of an if structure.

The condition part of the if structure is a test, which has an exit status. The exit status is a numeric value that indicates whether the condition was true or false. If the condition is true, the exit status is 0.

(I know, this is the opposite of the C language. It gives me a headache, too. Programming lessons in Klingon must be like this. Be brave, young warrior. Question: How many Klingons does it take to change a light bulb? Answer: A Klingon warrior is not afraid of the dark! Or UNIX!!)

More about the exit status: commands have an exit status, too. If a command is successful, the exit status is 0. If not, it isn't. You can read the exit status of a command immediately after it executes by testing the value of $?. For example, in this command:

grep "Steve Vincent" /etc/passwd

I am looking for my name in the password file on my system. If grep finds my name in the file, it will output the line containing it. Immediately after the output, I could issue the command

	echo $?

and I would only get a 0 for output if the command had been successful. Of course this seems a bit silly. I could just read the line that was output. I could, but the script can't, so it takes an action next based on the exit status of the command. And, in order to keep the line that might be output from cluttering up the screen (it's unimportant to the script, right?) I can redirect the output of the grep command to the "bit bucket", a file set up to catch unwanted output. It is in the dev directory and it is called null. Example:

grep "Steve Vincent" /etc/passwd > /dev/null

Shell scripts can use relational operators to compare two values. The chart at the bottom of page 225 illustrates this:

if test int -eq int2	
tests whether int equals int2

if test int -ne int2	
tests whether int doesn't equal int2

if test int -lt int2	
tests whether int is less than int2

if test int -le int2	
tests whether int is less than or equal to int2

if test int -gt int2	
tests whether int is greater than int2

if test int -ge int2	
tests whether int is greater than or equal to int2

The test command is used in this way to cause the test condition to be evaluated. If you don't like the test command, just put the test condition inside square brackets and spaces instead, like this:

if [ int -lt int2 ]	
tests whether int is less than int2

The if structure can be modified, as noted above, to use an else statement. The else statement precedes commands that you want carried out if the if statement is false. This only describes a two way situation. Most situations in life have more than two alternatives, so we need the elif statement for subsequent testing. Read it as "else if". Example:

if [ $1 -eq $2 ]	
then		
echo "parameters are the same"	
elif [ $1 -le $2 ]		
then
echo "$1 is less than $2"	
else		
echo "$1 is greater than $2"	
fi

Here there are three logical alternatives, requiring two tests. If both tests are failed, a third test is not necessary, because only three possibilities exist.

When comparing the contents of strings, we can use another set of relational operators, as on page 229.

str1 = str2	true if identical
str1 != str2	true if not identical
str1		true if not NULL
-n str1		true if not NULL and does exist
-z str1		true if NULL and does exist

When comparing file and directory types, we have another set of relational operators, as on page 231.

-s file		true if a non-empty file
-f file		true if a file, not a directory
-d file		true if a directory, not a file
-w file		true if file is writeable
-r file		true if file is readable

As noted above, in the string relational operators, the exclamation point (or, bang) is the negation operator. If reverses the meaning of what follows. When placed to the left of the left value in a comparison, it reverses the true/false return value of the test.

	[ ! x1 le x2 ]

The phrase above returns true if x1 is not less than or equal to x2.

Another chart of comparison operators is on page 234, and you should review it.

Another selection tool is the case statement, more useful for multiple possible outcomes. It takes the place of several if statements. It is considered to be more elegant and efficient than using the several if statements that it replaces. The basic format is:

case $variable in
	"pattern1")
		command ;;
	"pattern2")
		command
		command ;;
	*)
		command ;;
esac

The structure begins with the word case and ends with esac (case backwards, okay?). Following the word case is the value of the variable we are concerned with, and the word in. The second line begins with a pattern to match against the current value of the variable. If there is a match, the commands following that pattern are carried out, until the double semi-colon is encountered. Then the script exits the case structure and executes the next line after it. If no pattern listed matches the value of the variable, the commands following the asterisk (matches anything, remember?) are executed and the case is exited.

The for loop is fairly simple. It requires a list of variables, parameters or even files, and follows the format

for i in 1 2 3 4 5
do
	commands
done

Basically, we either feed the loop a list of variables to use or let it build the list in the first line with expansion. Commands between the do and the done statements are carried out for each item in the for list. The variable i holds the value of each item in the for list as each one is considered and can be used with the commands in the body of the loop.

Sometimes you will want to vary which commands in a loop are executed, perhaps based on the value of i at a given moment. You can place if statements in the body of the loop at key points to decide whether to process a command or not. In order to facilitate skipping the rest of the commands in the body of the loop, but to continue looping with the next item in the for list, use the command continue. This causes the script to stop running the current iteration of the loop, to read the next item in the list, and to continue with the next iteration of the loop.

The while loop is an alternative to the for loop. The syntax is

while [ test goes here ]
do
	command
	command
done

Any number of commands may be placed between the do and done statements. The main difference between the while loop and the for loop is the for loop requires a list to process, and stops running at the end of the list. The while loop runs until its test condition becomes false. Infinite loops are possible, so take care to make sure something happens to end a while loop.

The until loop is the opposite of the while loop, in that it runs until its test condition becomes true. The syntax is the same:

until [ test goes here ]
do
	command
	command
done

and the loop will not run unless the condition is false to begin with.

If you should need to stop running a loop when some critical event takes place, use the break command. Like the continue command break stops running the current iteration of the loop, but unlike continue, break does not go to the next iteration. Program control passes to the next line after the loop when break is executed.