Using Control Structures

Using Control Structures

There are times when you want to control which commands in your script actually get executed, in what order, and how many times. In those instances, you use tools called control structures . Without control structures, your script is executed line by line, from start to finish. Often this is just fine. The simple Trash script you created earlier in this chapter is an example of this, but the Trash script would be better if it could change its behaviorsuch as pausingif it found that it was about to overwrite a file.

In scripting (and in computer programming in general), there are two basic kinds of control structures: conditionals and loops .

Conditionals are the familiar "If . . . then" construct we use in everyday lifefor example, "If you are going to call me after 5 p.m., then use my home number."

We also use loops in everyday language, but less consciously. For example, we say, "Put the dishes on the table" instead of saying, "Put the first dish on the table. Put the second dish on the table. Put the third dish on the table." A loop involves a command or set of commands that are to be repeated.

Using conditionals

In shell scripts, you use conditionals so that your script performs different actions based on questions like "Did the user supply enough arguments to the script?" "Does this file exist?" "Do we have permission to create a file in this directory?" "Is $a greater than $b ?" and so on.

Fundamental to the use of conditionals is the idea of something being true or false . When using conditionals in shell scripts, you'll test whether the expressions are true or false. We show you some common tests below.

Using the if . . . then conditional

The most basic conditional is the if statement . When an if statement is used, a command or set of commands is executed only if something is true.

While reading through the next task, refer to Figure 9.20 . It is a code listing of a script that uses an if statement, while Figure 9.21 shows the output from two executions of the script. In the first case, the if statement finds a false when comparing $1 to see if it is equal to (-eq) $magic_number , and in the second case the if statement finds a true result.

Figure 9.20. Code listing of a script using an if statement.
 #!/bin/sh # This script uses an if statement magic_number=17 echo "Checking to see if your guess () is the magic number...." if [  -eq $magic_number ] then     echo "You got it!" fi echo "Thanks for playing!" 

Figure 9.21. Output from two uses of the script in Figure 9.20 showing the text expression being false and then true.
 localhost:~ vanilla$  ./if.sh 5  Checking to see if your guess (5) is the magic number.... Thanks for playing! localhost:~ vanilla$ ./if.sh 17 Checking to see if your guess (17) is the magic number.... You got it! Thanks for playing! localhost:~ vanilla 

To use an if . . . then structure:

1.
if [ expression ]

An if statement begins with the word if and is followed by a test expression enclosed in square brackets. There must be a space after the [ and before the ] .

There are two main types of test expressions: comparisons and file tests .

Comparison expressions are similar to the expressions used with expr , in that they consist of two arguments separated by an operator. Examples of if statements using comparison tests are

 if [ $count -eq 1 ] if [ $a -gt $b ] if [ $word1 > $word2 ] 

File-test expressions use one operator and a filename or path . Examples include

 if [ -f "$filename" ] if [ -d "$filename" ] 

Table 9.3 lists the most common test expressions. See man test for the complete list.

Table 9.3. Test Expressions for Use in [ ]

E XPRESSION

M EANING

I NTEGER C OMPARISONS

n1 -eq n2

Integer comparison. True if n1 equals n2 . Example: [ $a -eq $b ] .

n1 -ne n2

True if n1 is not equal to n2 .

n1 -lt n2

True if n1 is less than n2 .

n1 -gt n2

True if n1 is greater than n2 .

n1 -le n2

True if n1 is less than or equal to n2 .

n1 -ge n2

True if n1 is greater than or equal to n2 .

S TRING (T EXT ) C OMPARISONS U SING ASCII V ALUES OF T EXT

str1 = str2

True if str1 is identical to str2 . Example: [ $guess = $password ] .

str1 != str2

True if str1 is not identical to str2 .

str1 < str2

True if str1 is alphabetically lower than str2 .

str1 > str2

True if str1 is alphabetically higher than str2 .

-z string

True if string is not empty. Example: [ -z $word ] .

F ILE -T EST E XPRESSIONS

-e filename

True if filename exists.

-f filename

True if filename exists and is a regular file.

-d filename

True if filename exists and is a directory.

-r filename

True if filename exists and is readable.

-w filename

True if filename exists and is writable.

-x filename

True if filename exists and is executable.

-L filename

True if filename exists and is a symbolic link.

file1 -nt file2

True if f1 exists and is newer than f2 (has a more recent modification time).

file1 -ot file2

True if file1 exists and is older than file2 .

file1 -ef file2

True if file1 exists and refers to the same file as file2 .


2.
then

The next line after you start the if statement is simply the word then . All the lines between the then line and the end of the if statement will be executed if the test expression is true.

3.
Enter one or more command lines.

This is where you enter the commands that are executed if the test expression is true.

For example,

 echo "If you see this the test  returned true 

You can have as many lines as you want after the then line.

It is customary (and makes for more readable scripts) to indent every line between the then and the end of the if statement. In Figure 9.20 you can see how the echo statement is indented.

4.
The if statement is closed off (ended) with a line that says simply

fi

That's if backward. Have a look at Figure 9.20 for a complete example. Figure 9.21 shows output from the script.

Tip

  • You can make your scripts a bit easier to read if you put the then part of an if statement on the same line as the test by using a semicolon after the test:

     if [ $number -le $guess ] ; then echo "$a is less than or equal to $b" fi 

You can reverse the meaning of any test expression by adding a ! , which you can think of as "not."


To reverse the meaning of any test expression:

  • Add a ! to the expression:

    if [ ! -f "$filename" ]

    The expression inside the [ ] will be true if the file does exist and false if the file does not. The spaces around the ! are required.

The if statement is very useful, but what about when you want to do one thing if the test is true and another if it is false? In that case, you add an else clause to your if statement. Refer to Figure 9.22 for a code listing of a script that uses an if . . . then . . . else structure, and Figure 9.23 for sample output from the script.

Figure 9.22. Code listing of a script using an if . . . then . . . else structure.
 #!/bin/sh # This script uses an if...then...else structure magic_number=17 echo "Checking to see if your guess () is the magic number...." if [  -eq $magic_number ] then        echo "You got it!" else        echo "Oh no! You didn't get it." fi echo "Thanks for playing!" 

Figure 9.23. Output from two tries on the script in Figure 9.22; on the second try, the if clause is fulfilled, so the then clause is executed.
 localhost:~ vanilla$  ./else.sh 5  Checking to see if your guess (5) is the magic number.... Oh no! You didn't get it. Thanks for playing! localhost:~ vanilla$ ./else.sh 17 Checking to see if your guess (17) is the magic number.... You got it! Thanks for playing! localhost:~ vanilla 

To create an if . . . then . . . else structure:

1.
if [ testexpression ]

Create the first part of the if statement.

For example, from Figure 9.22:

if [ $1 -eq $magic_number ]

2.
then

This line simply begins the block of code that will be executed if the condition is true.

3.
Enter the commands for the true part.

Remember to indent these commands to make the script more readable.

From 9.22:

echo "You got it!"

Now, instead of finishing the if statement (with fi ), add a line (not indented) that says simply:

4.
else

The commands that follow the else line will be executed if the test is false.

5.
Enter the commands for the false part.

As with the commands for the true part, you may enter as many command lines here as you like.

Using Figure 9.22 again as an example:

echo "Oh no! You didn't get it."

Finally, you still end the entire if statement with the backward if :

6.
fi

Sometimes if . . . then . . . else is not enough. You'd like to try one test, and then if that fails, try another. You could do this by nesting one or more if statements inside one another as in Figure 9.24 , but a better way is to use elif clauses, as shown in Figure 9.25 . Figure 9.26 shows the output that would result from either script. They both do the same thing in different ways.

Figure 9.24. Code listing of a script that uses nested if statements.
 #!/bin/sh # This script uses nested if statements. magic_number=17 echo "Checking to see if your guess () is the magic number...." if [  -eq $magic_number ] then        echo "You got it!" else        if [  -gt $magic_number ]        then             echo "Your guess () is greater than the magic number."        fi        if [  -lt $magic_number ]        then             echo "Your guess () is less than the magic number."        fi fi echo "Thanks for playing!" 

Figure 9.25. Code listing of a script that does the same thing as Figure 9.24 but uses the elif clause instead.
 #!/bin/sh # This script uses elif clauses in an if statement. magic_number=17 echo "Checking to see if your guess () is the magic number...." if [  -eq $magic_number ] then        echo "You got it!" elif [  -gt $magic_number ] then        echo "Your guess () is greater than the magic number." elif [  -lt $magic_number ] then        echo "Your guess () is less than the magic number." fi echo "Thanks for playing!" 

Figure 9.26. Output from either of the scripts in Figures 9.24 and 9.25.
 localhost:~ vanilla$  ./guess.sh 100  Checking to see if your guess (100) is the magic number.... Your guess (100) is greater than the magic number. Thanks for playing! localhost:~ vanilla$  ./guess.sh 5  Checking to see if your guess (5) is the magic number.... Your guess (5) is less than the magic number. Thanks for playing! localhost:~ vanilla$  ./guess.sh 17  Checking to see if your guess (17) is the magic number.... You got it! Thanks for playing! localhost:~ vanilla 

elif means else if and is a way of having multiple tests in the same if statement. Only the first true elif is used.

Using the case conditional

You might be tempted to use a series of elif clauses where you have a variable that is storing one item from a list of possibilities such as start , stop , or restart , and you want to execute a different set of commands based on which one of the words the variable actually has. You could use a series of elif clauses in an if statement to do this, but because this is such a common occurrence, the Bourne shell provides a conditional structure called case exactly for this purpose.

Using a case structure involves setting up a series of cases , which are each associated with a pattern that is checked against a single variable. The first case whose pattern matches is executed, while the rest are ignored.

Refer to Figure 9.27 while reading the following task. Figure 9.27 is a code listing of a script using a case structure. Figure 9.28 shows the output of the script with four different arguments, each one triggering a different case.

Figure 9.27. Code listing of a script that uses case structure to perform one of four different cases, depending on the script's first argument.
 #!/bin/sh # This script uses a case structure. # We will do different things based on # the first argument to this script. time=`date` case "" in        start)                  echo "Received start command at $time"        ;;        stop)                  echo "Received stop command at $time"        ;;        restart)                  # To restart we execute this same script twice, supplying                  # arguments of stop and start                  " 
 #!/bin/sh # This script uses a case structure. # We will do different things based on # the first argument to this script. time=`date` case "$1" in start) echo "Received start command at $time" ;; stop) echo "Received stop command at $time" ;; restart) # To restart we execute this same script twice, supplying # arguments of stop and start "$0" stop "$0" start ;; *) # The * will always match, so this is the default # section. Do this if nothing above matched. echo "Usage: $0 (startstoprestart)" ;; esac 
" stop "
 #!/bin/sh # This script uses a case structure. # We will do different things based on # the first argument to this script. time=`date` case "$1" in start) echo "Received start command at $time" ;; stop) echo "Received stop command at $time" ;; restart) # To restart we execute this same script twice, supplying # arguments of stop and start "$0" stop "$0" start ;; *) # The * will always match, so this is the default # section. Do this if nothing above matched. echo "Usage: $0 (startstoprestart)" ;; esac 
" start ;; *) # The * will always match, so this is the default # section. Do this if nothing above matched. echo "Usage:
 #!/bin/sh # This script uses a case structure. # We will do different things based on # the first argument to this script. time=`date` case "$1" in start) echo "Received start command at $time" ;; stop) echo "Received stop command at $time" ;; restart) # To restart we execute this same script twice, supplying # arguments of stop and start "$0" stop "$0" start ;; *) # The * will always match, so this is the default # section. Do this if nothing above matched. echo "Usage: $0 (startstoprestart)" ;; esac 
(startstoprestart)" ;; esac

Figure 9.28. Output from the script in Figure 9.27 showing the effect of using different arguments. Notice how the usage message comes up.
 localhost:~ vanilla$  ./case.sh start  Received start command at Mon Jun 17 22:49:15 PDT 2002 localhost:~ vanilla$  ./case.sh stop  Received stop command at Mon Jun 17 22:49:21 PDT 2002 localhost:~ vanilla$  ./case.sh restart  Received stop command at Mon Jun 17 22:49:26 PDT 2002 Received start command at Mon Jun 17 22:49:26 PDT 2002 localhost:~ vanilla$  ./case.sh run  Usage: ./case.sh (startstoprestart) localhost:~ vanilla$ 

To use a case structure:

1.
case $ variable in

The variable can be anything, such as one of the special variables for command-line arguments, like $1 or $USER , or any variable you have assigned earlier in your script.

Using Figure 9.27 as an example:

case "$1" in

The contents of the variable will be matched against a series of patterns.

2.
pattern)

This is the pattern that the variable must match to activate this case. Note that there is no opening parenthesis. It might look strange but that's the syntax. The pattern can be a literal string of text, as it is in the first three patterns in Figure 9.27, or it can use the same wildcards available on the command line. For example, * will match anything:

file*) matches file , files , file system , and so on.

The pipe character ( ) can be used to specify alternatives:

startgo)

matches either start or go .

Only the first matching case will be executed (or no cases if none of the patterns match).

In Figure 9.27 the first pattern is

start)

After you declare a pattern, enter a series of command lines to be executed for that case.

3.
Enter one or more command lines.

This is just like the lines that follow the then part of an if statement.

4.
;;

Each case is terminated with a double semicolon. See Figure 9.27 for four examples. The first one is

echo "Received start command at $time"

You may have as many cases as you like.

5.
Repeat steps 24 for each pattern you wish to match.

Usually the last case uses the pattern

*)

which matches anything. You use this case to handle the possibility that none of the prior cases matched. You do not need to do this, but it is usually a good idea. Figure 9.27 uses this case to provide a usage message stating the allowable arguments for the script.

6.
Finally, you terminate the entire case structure:

esac

That's case spelled backward. The Bourne shell seems to like this backward stuff.

Tip

  • Notice how the script in Figure 9.27 uses the $0 variable as a command name to execute itself again in the restart case. That's worth remembering.


Using loops

Use a loop when you want a command line or lines to be repeated over and over.

Loops are most commonly used in shell scripts to process each element of a list. These are called for loops . An example of using this kind of loop is to iterate over every argument in $@ .

Another common use of a loop is to keep performing a series of commands as long as some test keeps returning a true result. These are called while loops , and we'll show you how to improve the Trash script using this technique.

We'll start with a for loop. Refer to Figure 9.29 while reading the following task. Figure 9.29 is a code listing of a simple script that tells you the largest number of all its arguments (it works only with integer arguments). Figure 9.30 shows a couple of examples of output from the script with different arguments.

Figure 9.29. Code listing of a script using a for loop to find the largest of its arguments.
 #!/bin/sh # This script uses a for loop to find the biggest of its arguments. # Start by saving the first argument in $biggest biggest= # Now loop over all the arguments, one at a time for arg in "$@" do        if [ $arg -gt $biggest ] ; then             biggest=$arg        fi done echo "The biggest number is $biggest" 

Figure 9.30. Output from the script in Figure 9.29.
 localhost:~ vanilla$  ./biggest 3 8 9 101 78 344 5 7 8  The biggest number is 344 localhost:~ vanilla$  ./biggest -6 -20 -41 -1 -17  The biggest number is -1 localhost:~ vanilla$ 

To use a for loop:

1.
for loop_variable in list

The loop_variable will have a different value each time through the loop. The first time, it will hold the first item from list , then the second item, and so on.

In Figure 9.29 the special variable $@ is used to create the list. This is the only variable that will be treated as a list of items when enclosed in quotes. In all other cases, enclosing a variable in double quotes causes its contents to be treated as a single item.

The list can be a series of values like for fate in Clotho Lachesis Atropos $fate will hold Clotho the first time through the loop, Lachesis on the second go-round, and Atropos on the third.

It can come from a variable:

 winds="Boreas Eurus Notus Zephyrus" for wind in $winds 

Notice that $winds is not enclosed in quotesthe shell splits it into separate items based on the spaces, resulting in four winds.

The list can come from more than one variable:

 muses="Calliope Clio Erato Euterpe" muses2="Melpomene Thalia Polyhymnia" muses3="Terpsichore Urania" for muse in $muses $muses2 $muses3 

2.
do

The next line is simply the word do by itself.

You can put the do on the first line if you use a semicolon:

for bird in $flight ; do

3.
Enter a series of one or more command lines.

This marks the top of the loop body. The command lines in the loop body repeat for each item in the list, with the loop variable having a different value each time through.

4.
done

This terminates the loop body.

The for loop is an excellent way to process each element in a list (similar loops in other programming languages are sometimes called foreach loops, as in "For each item in the list, do something"). There are times when you don't have a list but still want to repeat a series of commands. Usually the while loop will do what you want.

A while loop combines a conditional test (like an if statement) with a loop body. The test is performed, and if true, the loop body is executed. Then the test is performed again. Lather, rinse, repeat. Unless something happens to alter the outcome of the test, the loop can run forever. Of course, usually you put something in the loop body that alters at least one of the variables in the test. Sometimes the test checks something happening outside the script, perhaps to see if a file still exists.

Refer to Figure 9.31 while reading the following task. Figure 9.31 is a code listing of a script that uses a while loop to count from 0 to 10. Notice how the contents of a variable in the test are altered each time through the loop. Figure 9.32 shows a run of the script.

Figure 9.31. Code listing of a script using a while loop to count to 10.
 #!/bin/sh # This script uses a while loop to count to 10 count=0 while [ $count -le 10 ] ; do        echo $count        count=`expr $count + 1` done 

Figure 9.32. Output from the counting script. Pretty much what you would expect.
 localhost:~ vanilla$  ./count.sh  0 1 2 3 4 5 6 7 8 9 10 localhost:~ vanilla$ 

To use a while loop:

1.
while [ test_expression ] ; do

The test expression is the same thing you used for the if statement earlier in this chapter. See Table 9.3 for a list of common test expressions, or man test for the complete list.

The do command can go on the next line, but by now we think you want to do it like the pros and use a semicolon to put it on the same line as the test.

2.
Enter the commands for the loop body.

As with the for loop, these commands will be executed each time through the loop, as long as the test is true.

In Figure 9.31 the script adds 1 to the value of $count on each pass though the loop. Eventually the test is false (when $count is 11, it is no longer "less than or equal to" 10). The script exits the loop and continues with the next line after the loop.

3.
done

That's the line that marks the end of the loop, the same as with the for loop.

Earlier we promised you an improved version of the Trash script. Figure 9.33 is a code listing of a script that lets you use the Macintosh Trash from the command line, and checks to see if the file(s) you are trashing might overwrite other files in the Trash. The script mimics the behavior of the Finder by renaming the trashed file if a conflict is found. Notice that the script uses a while loop to keep checking to see if a file exists, and if it does, it adds 1 to $count and uses the new value in the filename it checks for. Eventually a filename that is not already in the Trash is found, and the trashed file gets moved to the Trash with the new name.

Figure 9.33. Code listing of a better Trash script. This one uses file tests, a for loop, and a while loop.
 #!/bin/sh # Better trash - script for moving files to the Trash # from the command line. # Each user's Trash is a directory called .trash in their # home directory trash_directory="$HOME/.Trash" if [ ! -d "$trash_directory" ] ; then        echo "Whoa! $trash_directory doesn't exist."        exit ; # The exit command quits the script immediately fi # loop over each command line argument for file in "$@" ; do        if [ -e "$file" ] ; then             count=0             filename="$file"             trashname="$trash_directory/$filename"             # If there is no file with this name in the Trash             # then the while loop won't execute even once, which is OK.             #             while [ -e "$trashname" ] ; do                       count=`expr $count + 1`                       trashname="$trash_directory/$filename $count"             done             # We now have a $trashname that is not in use             # Here's where we actually move the file into the Trash             #             mv "$file" "$trashname"             fi done 



Unix for Mac OS X 10. 4 Tiger. Visual QuickPro Guide
Unix for Mac OS X 10.4 Tiger: Visual QuickPro Guide (2nd Edition)
ISBN: 0321246683
EAN: 2147483647
Year: 2004
Pages: 161
Authors: Matisse Enzer

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net