Bourne Shell Scripting/Loops

From Wikibooks, the open-content textbooks collection

Jump to: navigation, search

Contents

[edit] For Loops

The for loops are very useful for repeating the same commands on several files. These for loops are only available in Bourne-compatible shells because they work differently in C shell. The following example in the Bourne-compatible shell reports on the names and first lines of some files:

$ for i in /etc/*tab
> do
> echo $i: $(head -1 $i)
> done
/etc/disktab: # $OpenBSD: disktab,v 1.3 2003/03/01 00:46:23 miod Exp $
/etc/fbtab: # $OpenBSD: fbtab.head,v 1.2 1999/05/05 06:56:34 deraadt Exp $
/etc/fstab: /dev/wd0a / ffs rw 1 1
/etc/gettytab: # $OpenBSD: gettytab,v 1.4 2000/09/08 02:27:36 pjanzen Exp $

This example used several features discussed above, and some that were not. A for loop is used to run commands more than once. In this example, the body of the for loop between the do and done (the echo command) was run once for each file in a list. Let us examine each line of the loop.

[edit] The First Line

$ for i in /etc/*tab

Note that /etc/*tab is a filename expansion. Thus the shell expands /etc/*tab into all files in /etc that end with tab. On the example machine, the shell treated the for line exactly the same as if it was:

$ for i in /etc/disktab /etc/fbtab /etc/fstab /etc/gettytab

The name of the command, "for", announces to the shell that this is the for loop. The list of words after the "in" indicate that the loop will be run once for each word, in the order of the words. This example loop will run four times, and the first time is for /etc/disktab.

Then what does i mean? The i is a shell parameter. During each cycle of the loop, the name of the file (/etc/disktab for the first time) will be stored in the shell parameter called i. We can use any name for the shell parameter, within limits, but some parameters have special meanings, so for now, just note that any sequence of lowercase letters will work. The i is short and commonly used in small for loops. We will see soon how to use a shell parameter.

After we type the "for" command into the shell, it knows that we are starting a for loop and must input the body of the loop. Thus the shell switches to a secondary command prompt which it uses when the user is not finished entering a command. In this guide, the secondary prompt will look like this:

>

[edit] The Body

> do
> echo $i: $(head -1 $i)
> done

The "do" and "done" mark the beginning and end of the body of the loop. In our short example, we only have one command between them, so this one command will be repeated once for each /etc/*tab file.

On the "echo" line, the $i is used to substitute the value of a shell parameter. The first time, the shell parameter "i" is /etc/disktab, so that is substituted:

> echo /etc/disktab: $(head -1 /etc/disktab)

The "$i:" looks like it would expand the shell parameter called "i:" instead of "i". But though ":" is not a special character, it also is not valid as part of a parameter name. So "i" is used and the ":" is not part of a substitution. Another way to write this is to enclose the variable in curly braces eg: ${i}. This syntax is useful when you need to append text to the value of a variable, but don't want the text to be considered as part of the variable name.

The other substitution is only a command substitution. The head -1 command shows the first line of the file, so the command becomes

> echo /etc/disktab: # $OpenBSD: disktab,v 1.3 2003/03/01 00:46:23 miod Exp $

[edit] Shortening the Syntax

We now know that the syntax of a for loop looks like this:

$ for PARAMETER in LIST
$ do
$ COMMANDS
$ done

The shell has a special character ";" which one can use to end a command and put another command on the same line. For example, because every "for" needs a "do", it is common to do this:

$ for PARAMETER in LIST; do
$ COMMANDS
$ done

For short loops, we can even do:

$ for PARAMETER in LIST; do COMMANDS; done

Note that strangely, "do" must not have a semicolon after it. It can either have a newline, or the first command of the body, but not a semicolon. This is one of the odder traits of Bourne shell syntax.

We can write our example in one line, which makes it more convenient to type and use.

$ for i in /etc/*tab; do echo $i: `head -1 $i`; done
/etc/disktab: # $OpenBSD: disktab,v 1.3 2003/03/01 00:46:23 miod Exp $
/etc/fbtab: # $OpenBSD: fbtab.head,v 1.2 1999/05/05 06:56:34 deraadt Exp $
/etc/fstab: /dev/wd0a / ffs rw 1 1
/etc/gettytab: # $OpenBSD: gettytab,v 1.4 2000/09/08 02:27:36 pjanzen Exp $

[edit] Common Mistakes

One of the common mistakes is to forget the "do":

$ for i in /etc/*tab; echo $i: `head -1 $i`; done
/bin/ksh: syntax error: `echo' unexpected

Another mistake is to have a semicolon after the "do":

$ for i in /etc/*tab; do; echo $i: `head -1 $i`; done
/bin/ksh: syntax error: `;' unexpected

A common mistake is to have an extra sign before the parameter name between "for" and "in". This is correct in some scripting languages, but normally incorrect in shell. For example:

$ for $i in /etc/*tab; do echo $i: `head -1 $i`; done
/bin/ksh: for: bad identifier

Even if "i" is set to a valid shell parameter name, such as "c", the shell will still report an error. The name between "for" and "in" should not have a dollar sign; only use that for substitutions in the body of the loop.


Another common mistake is that people use a substition as the input list, and mistakenly expect the for loop to read one "line" at a time from the input, when there are spaces in the line. Because by default lists are delimited with spaces, the space-separated words will be broken up as separate items in the list and will be read separately.

For example, if I have a file in my directory named "hi mom" (with a space),

$ ls
hi mom
$ for i in `ls`; do echo $i; done
hi
mom

Most likely, the intended action in this case was for the loop to iterate through each file in the directory, and echo each on a line. But "hi mom" was broken up into two lines because each word was iterated over separately. The simplest solution to this problem is instead to use the "read" command to read one line at a time, and use a while loop to keep reading lines, like this:

$ ls | while read i; do echo $i; done
hi mom

[edit] While Loops

While loops will execute a block of code as long as the control command succeeds. As always in shell scripting, a command succeeds when its exit code is zero.

In this example, we wait until the file "log" is created:

 while [ \! -r log ] 
 do
   echo -n .
   sleep 1
 done

While can be used for infinite loops:

 while true
 do
   ...
 done