Chapter 8. Loops

Table of Contents

Introduction
The "for" loop
while and until loops
getopts Using arguments and parameters
Exercises:

Introduction

Looping is an integral part of any programming language, and equally so in the shell.

The shell has three types of loops:

  1. for loops

  2. while loops

  3. until loops

Each loop has a slightly different purpose.

The "for" loop

Let's start with the for loop, which has the following syntax:

for variable in list
do
	...
	...
done
                

A more specific example of this case is:

for i in 1 2 3 4
do
	echo ${i}
done
                

If you run the above, you will get four numbers printed to the output:

1
2
3
4
                

So the for loop says:

"for every element in the list (1,2,3,4 in our case) do something (echo $i in our case)"
                

In this example, we are just echoing the output. No rocket science there, but it's a good means of introducing us to for loops.

The lists could be anything, they could say:

for NAME in hamish heidi matthew riaan simone
do
	echo "people involved in this project: "
	echo $NAME
done
                

This would produce:

people involved in this project:
hamish
people involved in this project:
heidi
people involved in this project:
matthew
people involved in this project:
riaan
people involved in this project:
simone
                

You'll notice that the echo commands were printed 5 times, once for every argument in the list. This means that everything enclosed in the DO-DONE block will be executed every time that FOR loops.

Just a quick note, the file command tells us the type of a file. You could say:

file restaurants.txt
                

and hopefully it will return:

restaurants.txt: ASCII text
                

Now, we could equally use a for-loop list in another way - we could say for example:[19]

for files in `ls -1`
do
	echo "file: `file $files`"
done
                

Remember, from earlier in the course, we saw that ls -l or $(ls -1) executes the ls command and produces some output. What this FOR loop is doing, is listing every file in our current directory with the ls -1. For each one listed, it runs the file command on the file.

The output from the above example might look something like:

bash: file: Desktop/: directory: No such file or directory
bash: file: Maildir/: directory: No such file or directory
bash: file: _viminfo: ASCII text: command not found
bash: file: blah.txt: ASCII text: command not found
bash: file: tmp/: directory: No such file or directory
bash: file: urls: ASCII English text: command not found
bash: file: windows.profile/: directory: No such file or directory
bash: file: winscp.RND: data: command not found
                

As long as you provide the "for" loop with a list, it's happy.

Another example of doing a for loop is as follows:

for count in `seq 20`
do
	echo $count
done
                

This will produce a sequence of 20 numbers from 1 through 20.

Do an info on the 'seq' command to find out what else it can do.

Okay, so provided that you supply for with a list, it can cycle through that list and do a command or sequence of commands once for every item on the list.

There's another type of "for" loop, and that's using the for without the 'in' statement.

Here, the for loop uses the arguments supplied on the command line as the list ($1, $2, $3, etc.). Using the general syntax of the "for" loop as follows:

for var 
do
	...
	...
done
                

Cycle through the arguments on the command line with the script:

#!/bin/bash
for arg
do
	echo $arg
done
exit 0
                

Make the script executable, and then run it:

chmod +x for.sh

./for.sh one two three four
                

When run, the script will cycle through the "for" loop four (4) times, echoing your parameters one by one. Let's make this for loop a bit snazzier.

We're going to set a variable count at the top of our script to the value 1, which will keep track of the number of parameters:

#!/bin/bash
count=1
for arg
do
	echo "Argument $count is $arg"
	$((count=count+1))		
done
exit 0
                

This script will not only count up the number of arguments, but will also print the value of each argument. Save the above script, make it executable and run it. If you're using a shell that does not recognise the line with $(()) in it, then you can use the line:

count=`expr $count + 1`
                

You will notice a couple of things. First-off, although it seems to be incrementing the count it also gives us some errors. Something like:

Argument 1 is one
./for.sh: line 6: 2: command not found
Argument 2 is two
./for.sh: line 6: 3: command not found
Argument 3 is three
./for.sh: line 6: 4: command not found
Argument 4 is four
./for.sh: line 6: 5: command not found
                

The errors stem from line 6, the "$((count=count+1))". This line produces a number, and the command not found is this number (i.e. The shell is looking for the command 2, or 3 or 4, etc.) So, one way of getting around this is to put a noop in front of the line:

#!/bin/bash
count=1
for arg
do
	echo "Argument $count is $arg"
	: $((count=count+1))		
done
exit 0
                

This will execute the increment of the count without giving you any sort of error messages.

Running the script will produce:

[riaan@debian] ~$ ./for.sh one two three four
Argument 1 is one
Argument 2 is two
Argument 3 is three
Argument 4 is four
                

Alternatively you could replace line 6 with:

count=$((count+1))	
                

This might be a little more intuitive anyway. Any which way you do it, you should end up with the same four lines of output.

A "for" loop without an 'in' allows you to cycle through your arguments irrespective of the number of arguments.

The final permutation of the "for" loop, although not available under all shells, is one based on the "for" loop in C.

An example may be:

for ((i=0; i<=10; i=i+1))	#can replace i=i+1 with i++
do
	echo $i
done
                

This will count from 0 to 10.

Note that the syntax is like this:

for ((start value; comparison; count increment))
                

If we wanted to count down from 10 to 0, we would do the following:

for ((i=10; i>=0; i=i-1))	#can replace i=i-1 with i--
do
	echo $i
done
                

This version of the "for" loop is useful as it allows a means of iterating a defined number of times based upon a counter rather than a list.

Clearly we could have achieve the same thing with:

for i in `seq 10`
do 
	echo $i
done
                
[Note] Note

the seq command would also allow you to count in reverse

In summary, there are three means of using the FOR loop:

  1. for i in a list

  2. for a variable without the 'in' part, doing the arguments

  3. for i with a counter

You would generally use for loops when you know the exact number of times that you want your loop to execute. If you don't know how many times you are going to execute the loop, you should use a while or an until loop.

Exercises:

  1. Write a script that will cycle through all files in your current directory, printing the size and the name of each file. Additionally, ensure that each file type is recorded and printed.

  2. Write a script that will count to 30, and on every even number print a message indicating that this is an even number. Print a message indicating odd numbers too.

  3. Write a script to cycle through all arguments on the command line, counting the arguments. Ensure that one of your arguments contains the word 'hamish'. On reaching this argument, ensure that you print the message:

    "Hey, hamish is here. How about that!"
                                
  4. Modify your menu.sh script to cycle in a loop an infinite number of times, sleeping for a minumum of 20 seconds before re-printing your menu to the console. Note that the original menu.sh script will need to be altered as in the original, a command line was supplied as a choice of which option to choose in the menu.

Challenge sequence:

Write a script that will create a 6x4 HTML table. For this you will need to understand how HTML tables work. See the appendix Appendix Afor references on books/links to teach you the basics of HTML.

Inside each cell, print the row:column numbers.

| 1:1   | 1:2	| 1:3 | 1:4 | 1:5 | 1:6	|
| 2:1	| 2:2	| 2:3 | 2:4 | 2:5 | 2:6	|
| 3:1	| 3:2	| 3:3 | 3:4 | 3:5 | 3:6	|
| 4:1	| 4:2	| 4:3 | 4:4 | 4:5 | 4:6	|
                    

while and until loops

A while loop has the following syntax:

while <condition is true>
do 
	...
	...
done
                

And the until loop has the following syntax:

until <condition is true> 
do 
	...
	...
done
                

You should notice the [subtle] difference between these two loops.

The while loop executes ONLY WHILE the condition is TRUE(0), whereas the until loop will continue to execute UNTIL the condition BECOMES TRUE(0).

In other words, the UNTIL loop continues with a FALSE (1) condition, and stops as soon as the condition becomes TRUE(0).

Prior to beginning the UNTIL loop, the condition must be FALSE(1) in order to execute the loop at least once.

Prior coming into the while loop however, the condition must be TRUE(0) in order to execute the block within the while statement at least once.

Let's have a look at some examples. Here you could say:

i=5
while test "$i" -le 10
do
	echo $i
done
                

Or we could rewrite the above example as:

i=5
while [ "$i" -le 10 ]
do
	echo $i
done
                

Since the square brackets are just a synonym for the test command.

Another example:

while [ !-d `ls` ]
do 
	echo "file" 
done
                

which says:

"while a particular file is not a directory, echo the word 'file'"

So we could do tests like that where we want to test a particular type of file, and we could do all sorts of conditions.

Remember back to the test command, we could combine the tests with (an -a for AND and -o for OR) some other test condition. So we can combine tests together as many as we want.

while [ somecondition ] -a [ anothercondition ] -o [ yetanothercondition ]
do
	something
done
                

We will also look at the while loop again when we do the read command.

The until command is similar to the while command, but remember that the test is reversed.

For example, we might want to see whether somebody is logged in to our systems. Using the who command, create a script called aretheyloggedin.sh:

user=$1
until `who | grep "$user" > /dev/null`
do 
	echo "User not logged in"

done
                

This runs the who command piping the output to grep, which searches for a particular user.

We're not interested in the output, so we redirect the output to the Linux black hole (/dev/null).

This script will spew out tonnes of lines with:

User not logged in
                

We therefore might want to include a command to sleep for a bit before doing the check or printing the message again. How do we do that?

Simply add the following line:

sleep 10
                

The script becomes:

#!/bin/bash
user=$1
until who |grep "$user"> /dev/null
do 
	echo "User not logged in"
	sleep 10
done
echo "Finally!! $user has entered the OS"
exit 0
                

Until the user logs in, the script will tell you that the user is not logged on. The minute the user logs on, the script will tell you that the user has logged on and the script will then exit.

If we did not want to print anything until the user logged on, we could use the noop in our loop as follows:

#!/bin/bash
user=$1
until who |grep "$user"> /dev/null
do 
	:
	sleep 10
done
echo "Finally, $user logged in"
exit 0
                

And so there's a script that will monitor our system regularly to find out whether a particular user has logged in. As soon as they log on, it will inform us.

When you run the script, it will merely sit there -staring blankly at you. In fact, it is performing that loop repeatedly, but there is no output.

We've looked at the three types of loops that you're going to need when programming in the shell: for loops, while loops and until loops.

These should suffice for most scripts, and unless you're writing particularly complex scripts (in which case you should be writing them in perl!) they should serve your (almost) every need.

The break and continue commands

During execution of the script, we might want to break out of the loop. This time we're going to create a script called html.sh, which is going to produce an html table.

Now an HTML table is built row by row in HTML a table can only built a row at a time. We start by telling the browser that what follows is an HTML table, and every time we start a row we have to enclose the row with a row indicator ( <TR>) and end the row with a row terminator (</TR>) tag.

Each element in the row is enclosed in a table-data tag (<TD>) and terminated in a end-table-data tag (</TD>)

A snippet of how to write a table in HTML (I've set the border of our table to 1):

<TABLE BORDER="1">
<TR><TD>element</TD></TR>
<TR><TD>element</TD></TR>
</TABLE>
                    

The easiest way to generate a table of 4 rows, and 3 columns is to use a for loop since we know the exact number of times that we want to execute the loop.

Adding the following to html.sh:

#!/bin/bash
echo "<TABLE BORDER='1'>"
for row in `seq 4`
do
	echo "<TR></TR">
done
echo "</TABLE>"
exit 0
                    

should create a table with 4 rows, but no columns (table-data).

As usual make the script executable and run it with the following commands:

chmod +x  html.sh

./html.sh > /tmp/table.html
                    

Open your favourite browser (Mozilla, Opera, Galleon, Firebird) and point the browser at this new file by entering the URL:

file:///tmp/table.html
                    

You should see a whole lot of nothing happening, because we haven't put any elements in our table.

Let's add some table data, as well as some extra rows.

#!/bin/bash# Start by warning the browser that a table is starting
	echo "<TABLE BORDER='1'>"
	
# Start the ROWs of the table (4 rows)
	for row in `seq 4` 
	do
# Start the row for this iteration
	   echo "<TR>"
	   
# Within each row, we need 3 columns (or table-data)
	   for col in `seq 3` 
	   do
#If this row 2, then break out of this inner (column) loop, returning to the next ROW above.
	      if [ $row -eq 2 ] 
	      then    
	         break;  
	      fi      
# If this is NOT row 2, then put the cell in here.
	      echo "   <TD>$row,$col</TD>"
	   done
# End this ROW
	   echo "</TR>"
	done#End this table.
	echo "</TABLE>"
	exit 0
                    
[Note] Note

This time, inside each row, we put some data. Previously we placed no data in the rows. Also, notice that when ROW 2 is reached, we "BREAK" out of this inner loop, continuing with the outer loop (i.e. incrementing to the next ROW).

If you hold the shift key down and click the reload button of your web browser, you should see now that you have data in the table. Not really that exciting?!

Let's make this a LOT more fun, I have included the script below. Read through it, work out what it does and then saving it in a script called runfun.sh, run it using the following command line:

./runfun.sh > index.html
                    

Again, point your browser at the resulting file (index.html) and enjoy.

[Note] Note

For this to work propperly you will need to make sure that the index.html file is created in the directory where you have the gif gif.tar.gz files stored.

#!/bin/bash

ANIM=`ls -1 *.gif`
NUM=`echo "$ANIM" | wc -l`
echo "<TABLE BORDER='1' bgcolor='FFFFFF'>"
for file in `seq 2` 
do
   echo "<tr>"
   for row in `seq 3` 
   do
      file=`echo "$ANIM" | head -1`

      NUM=$(( NUM - 1 ))

      ANIM=`echo "$ANIM" | tail -$NUM`

      echo "<td>"
# This is probably the only part you may have difficulty understanding. Here we include 
# an image in the cell rather than text. For this to work, you will need the couple of GIF 
# images packaged with this course.
echo "   <img src=$file alt='Image is: $file'>"
echo "</td>"
done
echo "</tr>"
done
echo "</TABLE>"
                    

This should produce a table for us with 3 rows and 3 columns.

So what happens if we wanted to skip column two, in other words, we didn't want any data in column 2? Well we could add the following if-then statement:

if [ "$col" -eq 2 ]
then 
    break
fi
                    

The break command would break out of the inner loop. So we would find that we don't have any data for column 2, but we do have data for column 1 and 3. You can add an argument to the break command such as:

break 2
                    

which would break out of the two inner-most loops.

Thus, break is a way of immediately terminating a loop. A couple of pointers, even if you broke out of the loops, the exit status is still run. All the break statement is doing is exiting out of the inner loop and then out of the outer loop because we did a 'break 2'.

There's nothing wrong with using break as programming practice goes - it's used by C programmers all over the world.

There might also be instances where you have a loop and on a condition you want it to continue. On a condition that we may want to continue the loop without executing the commands that follow the continue statement.

For example:

loop
do
	condition
		continue
	...
	...
done
                    

Continue tells the loop to skip any commands found on the lines following the continue beginning again at the top of the loop. This is the opposite of what the break command does, which terminates the loop.

A final word on loops. Suppose we wanted to save the output of the loop to a file, we would do this by redirecting the output to a file at the END of the loop as follows:

for ((i=0;i<10;i++))
do
	echo "Number is now $i"
done > forloop.txt
                    

We will see further uses of this when we come to the read command later.

Perhaps we want to take the output of this FOR loop and pipe it into the translate command. We could say:

for ((i=0;i<10;i++))
do
	echo "Number is now $i"
done |tr '[a-z]' '[A-Z]'
                    

We could achieve piping and redirection as per all the previous commands we have done:

for ((i=0;i<10;i++))
do
	echo "Number is now $i"
done |tr '[a-z]' '[A-Z]' >forloop.txt
                    

Note that the pipe or redirect must appear AFTER the 'done' and not after the 'for'.

Exercises:

  1. Write a script that will loop as many times as there are files in your home directory.

  2. Write an infinite while loop that will terminate on a user logging out.

  3. Write a script that will produce an HTML table of the output of the 'free' command. Save the output to a file mem.html, and using your favourite browser see that the output is working correctly.

  4. Write a script that will print every user that logs onto the system



[19] this is not ls -l as you might expect. It is ls -1 (one)