NEXT UP previous
Next: Signals

Flow Control

In common with other high level programming languages, the shell provides a set of commands which are used to control the flow of execution around the code. These include several selection and looping constructs which allow really sophisticated programs to be constructed.

As you will see, the only slightly unusual thing about these constructs is the way that commands and strings are used to specify conditional values, where in a more conventional language a boolean expression would be used.

if Command

You have seen that when the shell executes a command or pipeline, an exit status is returned. One way to determine what action is to be performed next is to use a construct which can test the value returned by a command and use it to make a selection. The if command performs this function. The general format of the if command is:

	if condition_command 
	then 
		true_commands
	else
		false_commands
	fi

What happens is that the condition_command is executed and its exit status is then used to determine which of true_commands or false_commands will be run next. Don't forget that it is the exit status value zero which indicates that a command executed successfully, and hence it is the zero value that is used as the tree condition. Any non-zero exit status value indicates false.

The else clause and the false_commands are optional and without them the if command appears as:

	if condition_command 
	then
		true_commands
	fi

As an example of the use of if, consider the following script, which I will call user1, that takes a word as a parameter and checks the password file to see if the parameter is a valid user login name on the system:

	# usage is: user1 login_name
	if grep "^$1:" /etc/passwd >/dev/null 2>/devinull 
	then
		echo $1 is a valid login name 
	else
		echo $1 is not a valid login name 
	fi
	exit 0

In this case, the conditional test is performed by a grep command whose standard output and error output are discarded, because all that is required is its exit status. This value will be zero (true) if grep successfully finds the word it is looking for or non-zero (false) otherwise. A suitable message is then sent to the standard output:

	$ user1 bash
	bash is not a valid login name
	$ user1 mot
	mot is a valid login name

user1 is smart enough in the first run that, even though the word bash does appear in the password file, it does not appear at the start of a line followed by a colon (as required by ^$1:). This is not to say that user1 cannot be fooled, as the following example shows:

	$ user1 carey:esJ9ohd8HH89i
	carey:esJ9ohd8HH89i is a valid login name

As the code for user1 shows, the use of a hash (#) symbol allows the introduction of comments into a shell script. After the hash, the comment just extends to the end of the current line.

test Command

If there is a standard command which does what you need, then it is now a simple matter to execute it and test its exit status to see if it was successful. If it did not succeed in some way then repeat attempts or remedial action are easy to arrange. This still leaves a large number of conditions for which you might like to test and for which none of the commands you have seen so far will provide a suitable function. There is, however, a command available, called test, which doesn't take any input or generate any output, whose sole purpose is to test for a range of conditions and return an appropriate exit status. The test command has the general form:

	test expression

If the specified expression is true then test will return a zero exit status; otherwise, if it is false, an exit status of 1 is returned.

One point worthy of note here is that when you are writing simple shell scripts and programs to test out some idea, it is very easy to give the name test to the resulting file. When you come to execute your program then you may actually execute the Linux test command instead of your own (depending on the order of directories given in your PATH variable). Executing the Linux test command with no parameters does nothing and just returns the next shell prompt. This gives the impression that your test program didn't work. The same argument applies to any standard Linux command but test is a particularly easy one to get wrong.

There are several types of expression that test can test for. The most common expression types check a file for some attribute, perform various string comparisons or perform numeric comparisons.

In the first case, a command line switch is given, followed by a single parameter, which is the name of a file. The test command then just checks the file for the attribute specified by the switch and gives an exit status based on the result. The most common of these command line switches are:

Expressions of the string type can have one or two string parameters, which can either be literal strings or the contents of shell variables. The most common string expressions are:

-z strtrue if length of str is zero;
-n strtrue if length of str is non-zero
stri = str2true if stri and str2 are equal;
stri != str2true if str1 and str2 are not equal.

The numeric expression type gives facilities for treating the contents of a string or variable as a number, and performing standard numeric comparisons. The list of numeric expressions are:

num1 -eq num2true if num1 equal to num2;
num1 -ne num2true if num1 not equal to num2;
num1 -lt num2true if num1 less than num2;
num1 -gt num2true if num1 greater than num2;
num1 -le num2true if num1 less than or equal to num2;
num1 -ge num2true if num1 greater than or equal to num2.

The test command can also combine its expressions using the logical AND, OR and NOT operators:

exp1 -a exp2true if both exp1 and exp2 are true;
exp1 -o exp2true if either exp1 or exp2 is true;
! exptrue if exp is false and vice versa.

As well as test being able to compare strings as though they were numeric values, there is a complementary feature in the shell which allows bash to perform simple arithmetic operations and to build arithmetic expressions, with the general form:

	$ [expression]

The following example demonstrates this idea in action:

	$ mum1=2
	$ numl=$[$num1*3+1]
	$ echo $num1
	7

The following is a simple shell script, called later, which makes use of the test command to perform simple comparisons and combines this with the arithmetic expression evaluation features of the shell to work out and display what the time will be a given number of hours from the current time:

	if test $# -ne 1 	# Check for 'hours' parameter 
	then
		echo "usage: later <hours>"
		exit 1
	fi
	now='date'		# Get and split current time
	hour='echo $now | cut -b 11-12'
	minsec='echo $now | cut -b 13-18'
	hour=$[($hour+$1)%24] 	# Add 'hours' to current hour 
	if test $hour -ge 12a 	# Convert to 12 hour clock
	then 			# and print later time
		echo $[$hour-12]$minsec pm 
	else
		echo $hour$minsec am
	fi
	exit 0

The script starts off by checking that there is a command line parameter specified. It does this by checking the value of $# and terminating the script with a usage message if the number of parameter values is not equal to 1.

Next, the date command is used to supply the current date and time. The time, which is in 24 hour clock format, is extracted and stored in the variables hour and minsec.

The next line of the script adds the value given on the command line to the current hour and then uses the modulus (%) operator to handle the situation where the new hour value is greater than 24.

Finally, the script does a conversion of the time from 24 hour to 12 hour clock time and displays the 12 hour time, adding am or pm to the end of the string as appropriate.

Executing the later command gives results such as the following:

	$ later 5
	8:32:42 pm
	$ later 12
	3:32:49 am

while Command

The while command allows you to introduce conditional loops into your shell scripts. The general form of the command is:

	while condition do
		commands
	done

The condition can be any command or pipeline whose exit status will be used to determine the next action. If the exit status is true (zero) then the commands enclosed between do and done will be executed, followed by a jump back to the top of the loop to re-evaluate the condition. If the exit status is false (non-zero) then the commands are skipped, and execution continues with whatever follows the keyword done.

A simple example will help to make the idea clear. The following shell script, called filecheck, will repeatedly test to make sure that a given file still exists. As soon as the file is removed, the loop will terminate and the script will exit:

	while test -e $1 
	do
		:
	done
	exit 0

The colon (:) between do and done is just a way of specifying a null command that does nothing. There is no action to perform in the body of this loop, but without the colon bash would generate an error, as it will not allow an empty loop.

Normally, you would run a script like this in the background because, otherwise you would not get a prompt back from the shell until the specified file had been deleted, and that might be a long wait!

Leaving this command running in the background, it will use up quite a lot of processing power, as it does not pause, but continuously re-tests for the existence of the file. In this particular application it may well be adequate to test for the file once every few seconds, rather than as fast as possible. If this is the case, then the colon command can be replaced by the sleep command, which causes execution of the script to be suspended for a specified number of seconds, during which the script will place no load on the processor so that other processes can make use of the time:

	while test -e $1 
	do
		sleep 2 
	done
	echo file $1 does not exist... 
	exit 0

This version of filecheck has also had an echo command added to it so that you will receive a message when the specified file no longer exists. The following sequence creates a file, runs filecheck in the background to test for this file and then deletes the file to make sure that the file does not exist message is displayed:

	$ cat
	Ctrl-d
	$ filecheck lock &
	[1] 2364
	$ rm lock
	file lock does not exist...

Notice that the cat command in this example is just being used to create an empty file. There are other ways that this can be done. One of the simplest is to use the touch command. The actual purpose of this command is to update the timestamp on a file (given by ls -l) to the current time. However, as a side effect, using touch on a file that does not yet exist will create the file first as an empty file, and then update its timestamp:

	$ ls -l lock
	ls: lock: No such file or directory
	$ touch lock
	$ ls -l lock
	-rw-r--r--    1 pc book		0 Jul 3 21:52 lock

until Command

The until command is another looping construct. It is very similar to the while command, and has the general structure:

	until condition 
	do
		commands
	done

The difference between while and until is that while continues to loop for as long as its condition evaluates to true, whereas until does the opposite and continues to loop for as long as its condition evaluates to false.

This would make the until command ideal if, for example, you needed a script to wait until a given file existed before it terminated:

	until test -e $1 
	do
		sleep 2
	done
	echo file $1 now exists... 
	exit 0

Sometimes, you need to create a loop that goes round indefinitely. What is required for this is a command that always returns an exit status of zero (to make while loop indefinitely) or non-zero (to make until loop indefinitely). These two commands are called true and false respectively. For example:

	until false
	do
		read firstword restofline 
		if test $firstword = end 
		then
			exit 0 
		else
			echo $firstword $restofline
		fi
	done

This shell script reads in lines of text and echoes them back to the display. This continues until a line is read that begins with the word end.

for Loops

The if, until and while commands all use exit status values to control their actions. There are occasions when you would like to perform looping or selection based on string values rather than exit status values, and bash also makes provision for this need.

Imagine you want to write a shell script that can look to see which, if any, of a given set of login names exist in the password file. In fact, you have already seen a script which can go part of the way towards this, in the section on for loops. There, the script called user1 just took one name (in $1) and grepped the password file for it. All you really need is just to apply the user1 script repeatedly for each of a set of names ($1, $2 etc.) To do this, you use the for command. The general format of this command is:

	for variable in wordlist 
	do
		commands
	done

The idea is that the specified variable takes each of the values given in wordlist in turn, and then executes commands with those values. For example:

	for i in 1 2 3 4 5 
	do
		echo value of i is $i 
	done

when executed would give the output:

	value of i is 1
	value of i is 2
	value of i is 3
	value of i is 4
	value of i is 5

Now going back to the original problem, all you need to remember is that the special variable $* gives a list of all the positional parameters passed into a shell script, and it then becomes a simple matter to code up a solution to the problem. One possibility (called user2) could be:

	for i in $* 
	do
		if grep "^$i:" /etc/passwd >/dev/null 2>/dev/null
		then
			echo $i is a valid login name 
		else
			echo $i is not a valid login name
		fi
	done
	exit 0

Running this script gives output such as the following:

	$ user2 carey jill rnc bill 
	carey is a valid login name 
	jill is not a valid login name 
	rnc is a valid login name 
	bill is not a valid login name

The use of 'for variable in $*' is such a common construction that it can be abbreviated just to 'for variable' and the command will automatically assume the 'in $*' part.

case Selection

Selecting one possibility from a number of choices, based on the contents of a string or variable, is done with the case command. The general form of the command is:

	case string in 
	expression_l)
		commands_1
		;;
	expression_2)
		commands_2
		;;
	.
	.
	.
	*)
		default_commands
		;;
	esac

In executing a case command, the shell evaluates the string and then compares the result with each of expression_1, expression_2 etc_ in turn, until it finds a match. If a match is found, then the associated commands will be executed, up to a double semi-colon (;;).

The case expressions may also be given as simple regular expressions, using the same set of special characters that the shell can expand in file names (namely, *, and []). This means that using *) as the last expression in a case command will match any string and thus act as a default entry if all previous matches fail.

As an example of the use of the case command, consider the following script, called append, which allows text, either typed at the keyboard or read from an input file, to be added onto the end of a specified output file:

	case $# in
	1)
		cat >>$1
		;;
	2)
		cat >>1 <$2
		;;
	*)
		echo "usage: append out_file [in_file]" 
		;;
	esac
	exit 0

Remember that the $# variable is set to containe the number of positional parameters entered on the command line, excluding the command name itself. For the append script, this value should only be either 1 or 2, depending on the source for the input text.

The square brackets which appear in the usage message are just there to indicate that the enclosed item is optional. This application of square brackets is in fairly common use in Linux command documentation.


NEXT UP previous
Next: Signals