In this chapter you'll learn how to set up program loops. These loops will enable you to execute repeatedly a set of commands either a specified number of times or until some condition is met. The three built-in looping commands are
for
while
until
You'll learn about each one of these loops in separate sections of this chapter.
The for
command is used to execute a set of commands a specified number of times. Its basic format is as shown:
for var in word1 word2 ... wordn do command command ... done
The commands enclosed between the do
and the done
form what's known as the body of the loop. These commands are executed for as many words as you have listed after the in
. When the loop is executed, the first word, word1, is assigned to the variable var, and the body of the loop is then executed. Next, the second word in the list, word2, is assigned to var, and the body of the loop is executed. This process continues with successive words in the list being assigned to var and the commands in the loop body being executed until the last word in the list, wordn, is assigned to var and the body of the loop executed. At that point, no words are left in the list, and execution of the for
command is then finished. Execution then continues with the command that immediately follows the done
. So if there are n words listed after the in
, the body of the loop will have been executed a total of n times after the loop has finished.
Here's a loop that will be executed a total of three times:
for i in 1 2 3 do echo $i done
To try it out, you can type this in directly at the terminal, just like any other shell command:
$ for i in 1 2 3 > do > echo $i > done 1 2 3 $
While the shell is waiting for the done
to be typed to close off the for
command, it displays your secondary command prompt. When it gets the done
, the shell then proceeds to execute the loop. Because three words are listed after the in
(1
, 2
, and 3
), the body of the loop—in this case a single echo
command—will be executed a total of three times.
The first time through the loop, the first word in the list, 1
, is assigned to the variable i
. Then the body of the loop is executed. This displays the value of i
at the terminal. Then the next word in the list, 2
, is assigned to i
and the echo
command re-executed, resulting in the display of 2
at the terminal. The third word in the list, 3
, is assigned to i
the third time through the loop and the echo
command executed. This results in 3
being displayed at the terminal. At that point, no more words are left in the list, so execution of the for
command is then complete, and the shell displays your command prompt to let you know it's done.
Recall the run
program from Chapter 7, “Passing Arguments,” that enabled you to run a file through tbl
, nroff
, and lp
:
$ cat run
tbl $1 | nroff -mm -Tlp | lp
$
If you wanted to run the files memo1
through memo4
through this program, you could type the following at the terminal:
$ for file in memo1 memo2 memo3 memo4 > do > run $file > done request id is laser1-33 (standard input) request id is laser1-34 (standard input) request id is laser1-35 (standard input) request id is laser1-36 (standard input) $
The four words memo1
, memo2
, memo3
, and memo4
will be assigned to the variable file
in order, and the run
program will be executed with the value of this variable as the argument. Execution will be just as if you typed in the four commands:
$ run memo1 request id is laser1-33 (standard input) $ run memo2 request id is laser1-34 (standard input) $ run memo3 request id is laser1-35 (standard input) $ run memo4 request id is laser1-36 (standard input) $
Incidentally, the shell permits filename substitution in the list of words in the for
, meaning that the previous loop could have also been written this way:
for file in memo[1-4] do run $file done
And if you wanted to run all the files in your current directory through run
, you could type
for file in * do run $file done
If the file filelist
contains a list of the files that you want to run through run
, you can type
files=$(cat filelist) for file in $files do run $file done
to run each of the files, or, more succinctly,
for file in $(cat filelist) do run $file done
If you found that you were using the run
program often to process several files at once, you could go inside the run
program and modify it to allow any number of files to be passed as arguments to the program.
$ cat run
#
# process files through nroff -- version 2
#
for file in $*
do
tbl $file | nroff -rom -Tlp | lp
done
$
Recall that the special shell variable $*
stands for all the arguments typed on the command line. So if you executed the new version of run
by typing
run memo1 memo2 memo3 memo4
the $*
in the for
's list would be replaced by the four arguments memo1
, memo2
, memo3
, and memo4
. Of course, you could also type
run memo[1-4]
While we're on the subject of $*
, let's look at it in a bit more detail. We'll write a program called args
that displays all the arguments typed on the command line, one per line.
$ cat args
echo Number of arguments passed is $#
for arg in $*
do
echo $arg
done
$
Now to try it:
$ args a b c Number of arguments passed is 3 a b c $ args 'a b' c Number of arguments passed is 2 a b c $
In the second case, even though a b
was passed as a single argument to args
, the $*
in the for
command was replaced by the shell with a b c
, which is three words. Thus the loop was executed three times.
Whereas the shell replaces the value of $*
with $1
, $2
, ...
, if you instead use the special shell variable "$@"
it will be replaced with "$1"
, "$2"
, ...
. The double quotes are necessary around $@
because without them this variable behaves just like $*
.
Go back to the args
program and replace the $*
with "$@"
:
$ cat args
echo Number of arguments passed is $#
for arg in "$@"
do
echo $arg
done
$
Now try it:
$ args a b c Number of arguments passed is 3 a b c $ args 'a b' c Number of arguments passed is 2 a b c $ args Try it with no arguments Number of arguments passed is 0 $
In the last case, no arguments were passed to the program. So the variable "$@"
was replaced by nothing. The net result is that the body of the loop was not executed at all.
A special notation is recognized by the shell when writing for
commands. If you write
for var do command command ... done
(note the absence of the in
), the shell automatically sequences through all the arguments typed on the command line, just as if you had written
for var in "$@" do command command ... done
Here's the third and last version of the args
program:
The second type of looping command to be described in this chapter is the while
. The format of this command is
while commandt do command command ... done
commandt is executed and its exit status tested. If it's zero, the commands enclosed between the do
and done
are executed. Then commandt is executed again and its exit status tested. If it's zero, the commands enclosed between the do
and done
are once again executed. This process continues until commandt returns a nonzero exit status. At that point, execution of the loop is terminated. Execution then proceeds with the command that follows the done
.
Note that the commands between the do
and done
might never be executed if commandt returns a nonzero exit status the first time it's executed.
Here's a program called twhile
that simply counts to 5:
The variable i
is used as the counting variable and is initially set equal to 1. Then the while
loop is entered. It continues execution as long as i
is less than or equal to 5. Inside the loop, the value of i
is displayed at the terminal. Then it is incremented by one.
The while
loop is often used in conjunction with the shift
command to process a variable number of arguments typed on the command line. The next program, called prargs
, prints each of the command-line arguments one per line.
$ cat prargs # # Print command line arguments one per line # while [ "$#" -ne 0 ] do echo "$1" shift done $ prargs a b c a b c $ prargs 'a b' c a b c $ prargs * addresses intro lotsaspaces names nu numbers phonebook stat $ prargs No arguments $
While the number of arguments is not equal to zero, the value of $1
is displayed and then a shift
executed. Recall that this shifts down the variables (that is, $2
to $1
, $3
to $2
, and so on) and also decrements $#
. When the last argument has been displayed and shifted out, $#
will equal zero, at which point execution of the while
will be terminated. Note that if no arguments are given to prargs
(as was done in the last case), the echo
and shift
are never executed because $#
is equal to zero as soon as the loop is entered.
The while
command continues execution as long as the command listed after the while
returns a zero exit status. The until
command is similar to the while
, only it continues execution as long as the command that follows the until
returns a nonzero exit status. As soon as a zero exit status is returned, the loop is terminated. Here is the general format of the until
:
until commandt do command command ... done
Like the while
, the commands between the do
and done
might never be executed if commandt returns a zero exit status the first time it's executed.
The until
command is useful for writing programs that wait for a particular event to occur. For example, suppose that you want to see whether sandy
is logged on because you have to give her something important. You could send her electronic mail, but you know that she usually doesn't get around to reading her mail until late in the day. One approach is to use the on
program from Chapter 8, “Decisions, Decisions,” to see whether sandy
's logged on:
$ on sandy
sandy is not logged on
$
You could execute this program periodically throughout the day, until sandy
eventually logs on, or you could write your own program to continually check until she does. Let's call the program mon
and have it take a single argument: the name of the user you want to monitor. Instead of having the program continually check for that user logging on, we'll have it check only once every minute. To do this, you have to know about a command called sleep
that suspends execution of a program for a specified number of seconds. So the Unix command (this isn't a shell built-in)
sleep n
suspends execution of the program for n seconds. At the end of that interval, the program resumes execution where it left off—with the command that immediately follows the sleep
.
$ cat mon
#
# Wait until a specified user logs on
#
if [ "$#" -ne 1 ]
then
echo "Usage: mon user"
exit 1
fi
user="$1"
#
# Check every minute for user logging on
#
until who | grep "^$user " > /dev/null
do
sleep 60
done
#
# When we reach this point, the user has logged on
#
echo "$user has logged on"
$
After checking that one argument was provided, the program assigns $1
to user
. Then an until
loop is entered. This loop will be executed until the exit status returned by grep
is zero; that is, until the specified user logs on. As long as the user isn't logged on, the body of the loop—the sleep
command—is executed. This command suspends execution of the program for one minute (60 seconds). At the end of the minute, the pipeline listed after the until
is re-executed and the process repeated.
When the until
loop is exited—signaling that the monitored user has logged on—a message is displayed at the terminal to that effect.
$ mon sandy Time passes sandy has logged on $
Using the program as shown here is not very practical because it ties up your terminal until sandy
logs on. A better idea is to run mon
in the background so that you can use your terminal for other work:
$ mon sandy & Run it in the background [1] 4392 Job number and process id $ nroff newmemo Do other work ... sandy has logged on Happens sometime later
So now you can do other work and the mon
program continues executing in the background until sandy
logs on, or until you log off the system.[1]
Because mon
only checks once per minute for the user's logging on, it won't hog the system's resources while it's running (an important consideration when submitting programs to the background for execution).
Unfortunately, after the specified user
logs on, there's a chance you might miss that one-line message (you may be cat
ing a file and might not even notice it come and go right off your screen). Also if you're editing a file with a screen editor such as vi
when the message comes, it may turn your screen into a mess, and you still might miss the message. A better alternative to writing the message to the terminal might be to mail it instead. Actually, you can let the user select his or her preference by adding an option to the program that, if selected, indicates that the message is to be mailed. If the option is not selected, the message can be displayed at the terminal.
In the version of mon
that follows, a -m
option has been added for this purpose:
$ cat mon
#
# Wait until a specified user logs on -- version 2
#
if [ "$1" = -m ]
then
mailopt=TRUE
shift
else
mailopt=FALSE
fi
if [ "$#" -eq 0 -o "$#" -gt 1 ]
then
echo "Usage: mon [-m] user"
echo" -m means to be informed by mail"
exit 1
fi
user="$1"
#
# Check every minute for user logging on
#
until who | grep "^$user " > /dev/null
do
sleep 60
done
#
# When we reach this point, the user has logged on
#
if [ "$mailopt" = FALSE ]
then
echo "$user has logged on"
else
echo "$user has logged on" | mail steve
fi
$
The first test checks to see whether the -m
option was supplied. If it was, the characters TRUE
are assigned to the variable mailopt
, and shift
is executed to “shift out” the first argument (moving the name of the user to be monitored to $1
and decrementing $#
). If the -m
option wasn't specified as the first argument, the characters FALSE
are assigned to mailopt
.
Execution then proceeds as in the previous version. However, this time when the loop is exited a test is made to see whether the -m
option was selected. If it wasn't, the message is written to standard output; otherwise, it's mailed to steve
.
$ mon sandy -m Usage: mon [-m] user -m means to be informed by mail $ mon -m sandy & [1] 5435 $ vi newmemo Work continues ... you have mail $ mail From steve Wed Aug 28 17:44:46 EDT 2002 sandy has logged on ?d $
Of course, we could have written mon
to accept the -m
option as either the first or second argument, but that goes against the recommended command syntax standard, which specifies that all options should precede any other types of arguments on the command line.[2]
Also note that the old version of mon
could have been executed as follows:
$ mon sandy | mail steve &
[1] 5522
$
to achieve the same net result as adding the -m
option.
Two last points before leaving the discussion of mon
: First, you'll probably always want to run this program in the background. It would be nice if mon
itself could take care of that. Later you'll see how to do it.
Second, the program always sends mail to steve
; not very nice if someone else wants to run it. A better way is to determine the user running the program and then send him or her the mail if the -m
option is selected. But how do you do that? One way is to execute the who
command with the am i
options and get the user name that comes back. This tells you who's logged on to the terminal that the program was run from. You can then use cut
to extract the username from who
's output and use that name as the recipient of the mail. All this can be done in the last if
command of mon
if it's changed to read as shown:
if [ "$#" -eq 1 ] then echo "$user has logged on" else runner=$(who am i | cut -c1-8) echo "$user has logged on" | mail $runner fi
Now the program can be run by anyone, and the mail will be properly sent.
Sometimes you may want to make an immediate exit from a loop. To just exit from the loop (and not from the program), you can use the break
command, whose format is simply
break
When the break
is executed, control is sent immediately out of the loop, where execution then continues as normal with the command that follows the done
.
The Unix command true
serves no purpose but to return an exit status of zero. The command false
also does nothing but return a nonzero exit status. If you write
while true do ... done
the while
loop will theoretically be executed forever because true
always returns a zero exit status. By the way, the :
command also does nothing but return a zero exit status, so an “infinite” loop can also be set up with
while : do ... done
Because false
always returns a nonzero exit status, the loop
until false do ... done
will theoretically execute forever.
The break
command is often used to exit from these sorts of infinite loops, usually when some error condition or the end of processing is detected:
while true do cmd=$(getcmd) if [ "$cmd" = quit ] then break else processcmd "$cmd" fi done
Here the while
loop will continue to execute the getcmd
and processcmd
programs until cmd
is equal to quit
. At that point, the break
command will be executed, thus causing the loop to be exited.
If the break
command is used in the form
break n
the n innermost loops are immediately exited, so in
for file do ... while [ "$count" -lt 10 ] do ... if [ -n "$error" ] then break 2 fi ... done ... done
both the while
and the for
loops will be exited if error
is nonnull.
The continue
command is similar to break
, only it doesn't cause the loop to be exited, merely the remaining commands in the loop to be skipped. Execution of the loop then continues as normal. Like the break
, an optional number can follow the continue
, so
continue n
causes the commands in the innermost n loops to be skipped; but execution of the loops then continues as normal.
for file do if [ ! -e "$file" ] then echo "$file not found!" continue fi # # Process the file # ... done
Each value of file
is checked to make sure that the file exists. If it doesn't, a message is printed, and further processing of the file is skipped. Execution of the loop then continues with the next value in the list. Note that the preceding example is equivalent to writing
An entire loop can be sent to the background for execution simply by placing an ampersand after the done
:
You can also perform I/O redirection on the entire loop. Input redirected into the loop applies to all commands in the loop that read their data from standard input. Output redirected from the loop to a file applies to all commands in the loop that write to standard output:
$ for i in 1 2 3 4 > do > echo $i > done > loopout Redirect loop's output to loopout $ cat loopout 1 2 3 4 $
You can override redirection of the entire loop's input or output by explicitly redirecting the input and/or output of commands inside the loop. To force input or output of a command to come from or go to the terminal, use the fact that /dev/tty
always refers to your terminal. In the following loop, the echo
command's output is explicitly redirected to the terminal to override the global output redirection applied to the loop:
for file do echo "Processing file $file" > /dev/tty ... done > output
echo
's output is redirected to the terminal while the rest goes to the file output
.
Naturally, you can also redirect the standard error output from a loop, simply by tacking on a 2>
file after the done
:
while [ "$endofdata" -ne TRUE ] do ... done 2> errors
Here output from all commands in the loop writing to standard error will be redirected to the file errors
.
A command's output can be piped into a loop, and the entire output from a loop can be piped into another command in the expected manner. Here's a highly manufactured example of the output from a for
command piped into wc
:
If you find yourself frequently executing loops directly at the terminal, you'll want to use the following shorthand notation to type the entire loop on a single line: Put a semicolon after the last item in the list and one after each command in the loop. Don't put a semicolon after the do
.
Following these rules, the loop
for i in 1 2 3 4 do echo $i done
becomes
for i in 1 2 3 4; do echo $i; done
And you can type it in directly this way:
$ for i in 1 2 3 4; do echo $i; done
1
2
3
4
$
The same rules apply to while
and until
loops.
if
commands can also be typed on the same line using a similar format:
$ if [ 1 = 1 ]; then echo yes; fi yes $ if [ 1 = 2 ]; then echo yes; else echo no; fi no $
Let's extend our mon
program further. We'll add a -t
option to it that specifies the time interval, in seconds, to perform the check. Now our mon
program takes both -m
and -t
options. We'll allow it to take these options in any order on the command line, provided that if they are used, they appear before the name of the user that we're monitoring. So valid mon
command lines look like this:
mon ann mon -m ann mon -t 600 ann mon -m -t 600 ann mon -t 600 -m ann
and invalid ones look like this:
mon Missing user name mon -t600 ann Need a space after -t mon ann -m Options must appear first mon -t ann Missing argument after -t
If you start writing the code to allow this sort of flexibility on the command line, you will soon discover that it can start to get a bit complex. Luckily, the shell provides a built-in command called getopts
that exists for the express purpose of processing command-line arguments. The general format of the command is
getopts options variable
The getopts
command is designed to be executed inside a loop. Each time through the loop, getopts
examines the next command line argument and determines whether it is a valid option. This determination is made by checking to see whether the argument begins with a minus sign and is followed by any single letter contained inside options. If it does, getopts
stores the matching option letter inside the specified variable and returns a zero exit status.
If the letter that follows the minus sign is not listed in options, getopts
stores a question mark inside variable before returning with a zero exit status. It also writes an error message to standard error.
If no more arguments are left on the command line or if the next argument doesn't begin with a minus sign, getopts
returns a nonzero exit status.
Suppose that you want getopts
to recognize the options -a
, -i
, and -r
for a command called foo
. Your getopts
call might look like this:
getopts air option
Here the first argument—air
—specifies the three acceptable options to the command, and option
specifies the variable that getopts
will use as previously described.
The getopts
command permits options to be “stacked” together on the command line. This is done by following a single minus sign with one or more consecutive options letters. For example, our foo
command can be executed like this:
foo -a -r -i
or like this:
foo -ari
using this stacking feature.
The getopts
command also handles the case where an option must be followed by an argument. For example, the new -t
option to be added to the mon
command requires a following argument. To handle options that take arguments, getopts
requires that at least one whitespace character separate the option from the argument. Furthermore, such options cannot be stacked.
To indicate to getopts
that an option takes a following argument, you write a colon character after the option letter on the getopts
command line. So our mon
program, which takes -m
and -t
options, should call getopts
like this:
getopts mt: option
If getopts
doesn't find an argument after an option that requires one, it stores a question mark inside the specified variable and writes an error message to standard error. Otherwise, it stores the actual argument inside a special variable called OPTARG
.
One final note about getopts
: Another special variable called OPTIND
is used by the command. This variable is initially set to one and is updated each time getopts
returns to reflect the number of the next command-line argument to be processed.
Here is the third version of mon
that uses the getopts
command to process the command-line arguments. It also incorporates the previously noted change to send mail to the user running the program.
$ cat mon # # Wait until a specified user logs on -- version 3 # # Set up default values mailopt=FALSE interval=60 # process command options while getopts mt: option do case "$option" in m) mailopt=TRUE;; t) interval=$OPTARG;; ?) echo "Usage: mon [-m] [-t n] user" echo " -m means to be informed by mail" echo " -t means check every n secs." exit 1;; esac done # Make sure a user name was specified if [ "$OPTIND" -gt "$#" ] then echo "Missing user name!" exit 2 fi shiftcount=$((OPTIND – 1)) shift $shiftcount user=$1 # # Check for user logging on # until who | grep "^$user " > /dev/null do sleep $interval done # # When we reach this point, the user has logged on # if [ "$mailopt" = FALSE] then echo "$user has logged on" else runner=$(who am i | cut -c1-8) echo "$user has logged on" | mail $runner fi $ mon -m Missing user name! $ mon -x fred Illegal option mon: illegal option -- x Usage: mon [-m] [-t n] user -m means to be informed by mail -t means check every n secs. $ mon -m -t 600 ann & Check every 10 min. for ann [1] 5792 $
When the line
mon -m -t 600 ann &
is executed, the following occurs inside the while
loop in mon
: getopts
is executed, and it stores the character m
inside the variable option
, sets OPTIND
to two, and returns a zero exit status. The case
command is then executed to determine what was stored inside option
. A match on the character m
indicates that the “send mail” option was selected, so mailopt
is set to TRUE
. (Note that the ?
inside the case
is quoted. This is to remove its special meaning as a pattern-matching character from the shell.)
The second time getopts
is executed, getopts
stores the character t
inside option
, stores the next command-line argument (600
) inside OPTARG
, sets OPTIND
to three, and returns a zero exit status. The case
command then matches the character t
stored inside option
. The code associated with that case copies the value of 600 that was stored in OPTARG
into the variable interval
.
The third time getopts
is executed, getopts
returns a nonzero exit status, indicating the end of options. The program then checks the value of OPTIND
against $#
to make sure that the username was typed on the command line. If OPTIND
is greater than $#
, then no more arguments remain on the command line and the user forgot the username argument. Otherwise, the shift
command is executed to move the username argument into $1
. The actual number of places to shift is one less than the value of OPTIND
.
The rest of the mon
program remains as before; the only change is the use of the interval
variable to specify the number of seconds to sleep.
[1] All your processes are automatically terminated when you log off the system. If you want a program to continue executing after you've logged off, you can run it with the nohup
command, or schedule it to run with at
or from the cron
. Consult your Unix User's Manual for more details.
[2] The command syntax standard consists of a set of rules as outlined in the Utility Argument Syntax section of the POSIX standard.
3.149.27.234