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.
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.
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 str | true if length of str is zero; |
-n str | true if length of str is non-zero |
stri = str2 | true if stri and str2 are equal; |
stri != str2 | true 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 num2 | true if num1 equal to num2; |
num1 -ne num2 | true if num1 not equal to num2; |
num1 -lt num2 | true if num1 less than num2; |
num1 -gt num2 | true if num1 greater than num2; |
num1 -le num2 | true if num1 less than or equal to num2; |
num1 -ge num2 | true 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 exp2 | true if both exp1 and exp2 are true; |
exp1 -o exp2 | true if either exp1 or exp2 is true; |
! exp | true 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
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
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.
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.
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.