Shell Control Structures
The scripts we have written so far execute their instructions in the same order in which they appear in the program. In other words, you can say that they execute their instructions sequentially. Such instructions are called as sequential control instructions. But our programming life is not so easy. Sometimes we need to repeat some instructions for a specific number of times and some other time we need to choose among several alternatives, taking actions that depend on the current conditions.
In this chapter we will discus some control structures that determine the ‘flow of control’ in a program. Generally the shell provides three other types of control structures, except sequential control structures.
1. Selection or Decision Control Structures
2. Repetition or Loop Control Structures
3. Case Control Structures
The selection and case control statements lets you select from alternatives, that is they take a decision as to which one to execute next. The loop control statement lets you execute a set of statements repeatedly. Firstly we will discuss decision control structures.
Decision Control Structures
Like any high-level language, FORTRAN, BASIC, Pascal, C etc., the UNIX shell provides if statement.
The if Statement
The if statement lets you conditionally perform some other actions based on the result of a condition. The general format of the if statement is as:
if control_command
then
command1
command2
….
fi
But unlike any conventional programming language in which if statements are concerned with the values of variables – is a equal to b, is salary greater than 10000, is respond to y, or some other such condition. The UNIX if statement is concerned with the status of commands. If the exit status of a command is 0, that is executed successfully, then the commands between the then and fi are executed. This it is the control command whose execution decides whether the commands between the then and the fi should be executed or not. If the control command executes successfully then the commands enclosed between the then and the fi will be executed; otherwise they will be skipped, that is if the exit status is non-zero the commands would not be executed.
One main point to note is that every if statement has a corresponding fi which indicates where the if statement ends. The word if, then, and fi must come at the beginning of a command line or just after a semicolon.
Let us see a simple example of UNIX if; it renames a file.
# rename
# Usage: rename oldname newname
if mv $1 $2
then
echo File renamed successfully.
fi
To execute this script we type the following
$ rename report document
In the script, the mv command renames the report to document. If the mv command does so successfully then it returns an exit status of 0 and we get the following output on the screen.
File renamed successfully.
If the mv command fails, due to any reason, it would return an exit status 1. For this case, nothing would be displayed on the screen.
Now let us see another form of the if statement.
The if–then–else Statement
In the earlier form of if statement, the commands are executed only if the exit status of a control command is 0. This if–then–else structure also executes a different command or set of commands even when the exit status is 1. The general form of the if–then–else structure is as:
if control command
then
command
command
….
else
command
command
….
fi
Here if the exit status of the control command is 0 then the commands between the then and else get executed; otherwise the commands between the else and fi get executed. In either case, only one set of commands execute, never both. The set of commands between the then and the else is called as an ‘if block’ and the commands between the else and fi block is called an ‘else block’. Now let us use this form to extend our previous script by adding an else part as:
# rename
# Usage: rename oldname newname
if mv $1 $2
then
echo File renamed successfully.
else
echo Failed to rename the file.
fi
Here if the mv command executes successfully then the echo statement between then and else executed; otherwise the echo statement between the else and fi get executed.
When you run this script, you get the following output….
$ rename report document
File renamed successfully.
$ rename report device
Failed to rename the file.
$
As you can see, the output of program depends on how the mv commands executed.
Nested if–then–else Statement
Like any conventional high-level language, the shell provides the facility of using an entire if–then–else–fi statement inside another if statement or an else statement. This concept is called as ‘nesting’ of ifs.
The general form of nested ifs is like this:
if control_command1
then
if control_command2
then
command1
command2
else
command3
command4
fi
else
command5
command6
fi
Here we have used if–then–else statement in the if block. You can also use it in else block. If the first control_command1 executes successfully then it goes to the if block. If again the control_command2 executes successfully then the command1 and command2 get executed; otherwise command3 and command4 get executed. On the other hand, if first control_command1 fails then it goes to else block and executes command5 and command6.
The if–then–elif–else–fi Statement
This is an advance version of the if statement. It lets you to group together several alternatives (multiple elses) one after the other. The UNIX shell provides a keyword elif, shorter form of else if to accomplish this. Its general form is as:
if control_command1
then
command1
elif control_command2
then
command2
elif control_command3
then
command3
elif control_command4
then
command4
else
command5
fi
Here if the first control command (control_command1) executes successfully then the command1 is executed; otherwise second control command (control_command2) after the first elif gets executed. If it is successful then the command2 is executed. This process continues until it encounters fi.
The test Command
Till now we have discussed different forms of if statement in which the control flow depends upon the exit status of the control command. But again the life is not so easy because the exit status of a command is not the only matter of interest for the programmer. We have come across through various situations in which we need to executed a command or a set command that depends on numerical comparisons, such as age is less that 20. Or in some other case we need to execute a command or a set of commands if the argument of a shell script is either an ordinary file or a directory file.
To fulfil this requirement, the UNIX shell provides the test command. The test command executes the expression and produces the result that can be understood by the control statement. The test command can perform following types of tests:
- Numerical tests
- String tests
- File tests
1. Numerical Tests
The test command handles various numerical comparisons on integers, such as a number is greater than 0 or not, salary is less than or equal to 12000 or not, and so on. To perform numerical comparisons, the shell provides following set of operators:
-eq equal to
-ne not equal to
-gt greater than
-lt less than
-ge greater than or equal to
-le less than or equal to
While using these operators, the first argument, which is placed left to the operator, is compared to the second argument, right of the operator. Here is a simple shell program that illustrates – how to use such operators. Suppose you want to find out whether the two numbers are equal or not.
# sscript13
# Usage: sscript13
echo Enter two numbers
read a b
if test $a –eq $b
then
echo Both numbers are equal.
else
echo Both numbers are not equal.
fi
when you execute this script, you get the following output
$ sscript13
Enter two numbers
10 10
Both numbers are equal.
$
When you execute the same script with different numbers, the output would be different as:
$ sscript13
Enter two numbers
10 12
Both numbers are not equal.
$
Now read the following script carefully and watch the output on your monitor.
# sscript14
# Usage: sscript14
echo Enter three numbers
read a b c
big=$a
if test $big –lt $b
then
big=$b
fi
if test $big –lt $c
then
big=$c
fi
echo Largest number is $big.
Many UNIX systems provides an alternative notation for the test command; this second notation places the condition with in [ ] (square brackets). Thus the script14 may be rewritten as:
# sscript15
# Usage: sscript15
echo Enter three numbers
read a b c
big=$a
if [ $big –lt $b ]
then
big=$b
fi
if [ $big –lt $c ]
then
big=$c
fi
echo Largest number is $big.
From this script it is clear that the command [ $big –lt $b ] is the same as test $big –lt $b while using the square brackets in place of a test command. Also note that a blank space should be placed immediately after ‘[‘ and immediately before ‘]’; otherwise it would be treated as an invalid statement.
2. File Tests
The shell command provides several options for checking the status of a file as shown below:
-r file true if file exist and is readable
-w file true if file exist and is writeable
-x file true if file exist and is executable
-f file true if file exist and not a directory
-d file true if file exist and is a directory
-c file true if file exist and is a character special file
-b file true if file exist and is a block special file
-s file true if file exist and has a size greater than 0
-k file true if file exist and its sticky bit is set
Suppose we want to know that whether your file, say filename, has a write permission or not.
# check
# Usage: check filename
if [ -w $1 ]
then
echo You can write to $1.
else
echo You can’t write to $1.
When we execute this script on a file, say report, that has write permission to it, you get the following output….
$ check report
You can write to report.
$
If you run this script on a file, say seminar, that has not write permission to it, you get the following output….
$ check seminar
You can’t write to seminar.
$
Now let us write a script that takes a command line argument and displays a message whether it is a directory, a file or something else.
# checkfile
# Usage: checkfile filename
if [ -d $1 ]
then
echo $1 is a directory.
elif [ if $1 ]
then
echo $1 is an ordinary file.
else
echo $1 is something else.
fi.
This script checks the identity of a file. If it is a directory then it prompts the user that it is a directory. If it is not a directory then it checks whether it is an ordinary file or something else. While using these options, there must be a space between the option letter and filename.
3. String Tests
A string is just a series of characters. It may be a filename, a command name, or the value of a variable. The shell provides following set of options to handle string tests:
string1 = string2 true, if the strings are equal
string1 != string2 true, if the strings are not equal
-n string true, if the string has non-zero length
-z string true, if the string has zero length
string true, if the string is not a null string
An undefined variable is treated as a null string.
$ num=20
$ test $num
$ echo $?
0
$ test $n
$ echo $?
1
$
Now let us write a script that asks you to enter two strings at command line and then displays its result.
# sscript16
# Usage: sscript16
echo Enter two strings
read first second
if [ $first = $second ]
then
echo Both strings are equal.
else
echo Both strings are not equal.
fi
Here is the output of this script.
$ sscript16
Enter two strings
Rama Rama
Both strings are equal.
$ sscript16
Enter two strings
Rama Rana
Both strings are not equal.
$
Here one should remember that numerical equality is not necessarily the same as string equality.
- 12 = 12 true, if the character string “12” is the same as “12”
- 12 = 012 false, if the character string “12” is different from “012”
- 12 –eq 012 true, 12 and 012 have the same numerical value
Therefore, the string comparison checks only that two strings contain exactly the same typographic characters.
Now let us see another script that makes your concept more clearer.
# sscript17
# Usage: sscript17 string1 string2
if [ “$1” = “$2” ]
then
echo Both strings are equal.
else
echo Both strings are not equal.
if [ $1 = $2 ]
then
echo Both strings are equal.
else
echo Both strings are not equal.
And here is the output of this script….
$ sscript17 Rahul Rahul
Both strings are equal.
Both strings are equal.
$ sscript17 “Rahul Dravid” “Rahul Dravid”
Both strings are equal.
Test: unknown operator Dravid
$
You may be surprised to get an error message. In second output, just after the message “Both strings are equal”. It happens so because in first if statement the
[ “$1” = “$2” ]
is expanded as:
[ “Rahul Dravid” = “Rahul Dravid” ]
whereas in second if statement the
[ $1 = $2 ]
is expanded as:
[ Rahul Dravid = Rahul Dravid ]
Naturally the word Dravid is treated as an operator, hence an error.
Logical Operators
The shell provides three types of logical operators while performing a test as:
- -a binary AND operator
- -o binary OR operator
- ! binary NOT operator
Note that –a operator is used to combine two commands. Suppose you want to check whether a file report is both readable and writeable or not. With the help of –a operator we so this as:
[ -r report –a –w report ]
Here it results in true if file report is readable and writeable. If we want an expression that may result in true either the file report is readable or writeable, we use the –o option as:
[ -r report –o –w report ]
The third logical operator, ! (NOT) reverses the value of the expression it operates on; it makes a true expression false and a false operation true. For example
[ ! –w report ]
result in true if the file report is not an existing writeable file.
Now let us see how they are used in a shell program. Let us write a shell program that accepts three numbers and finds the greatest of three numbers.
# large
# Usage: large
echo Enter any three numbers
read a b c
if [ $a –gt $b –a $a –gt $c ]
then
echo The largest number is $a
elif [ $b –gt $c ]
echo The largest number is $b
else
echo The largest number is $c
fi
The output of this program is as….
$ large
Enter any three numbers
14 12 11
The largest number is 14
$
Let us see another example of logical operators in which we firstly check whether the file size is zero or not. If not, then we will check whether it has read, write and execute permissions or not. Here is this script.
# sscript18
# Usage: sscript18 filename
if [ ! –z $1 ]
then
if [ -r $1 –a –w $1 –a –x $1 ]
then
echo You can do anything to $1.
else
echo Read, write and execute permission denied.
fi
else
echo Please check your file name.
The case Statement
The case statement is used to select one option from a list of alternatives. Since we can also use nested if–else statements in order to achieve the same. But in later case it is difficult to trace out the behavior of a program manually. The general form of the case statement is as:
case value in
choice1)
command
command
;;
choice2)
command
command
;;
choice3)
command
command
;;
*)
command
command
;;
esac
Here choice1, choice2 and choice3 are labels which identify the potential choices of action. When our script is executed, it evaluates the expression following the case keyword. This value is then compared to the value choice1. If it matches then the commands following the case label choice1 are executed. If it does not match then it takes the value choice2 and compares it. This process continues until it finds a suitable match or *). The *) represents the default clause of the case control statement. It gets executed when all other cases fail. The right parentheses ) is used to identify label names.
On encountering double semicolons ;; simultaneously the control is transferred to the keyword esac (case spelt backward) that marks the end of the case statement. Actually the double semicolons is not necessary after the final choice, but there is no harm to use it there.
Here is a simple example showing how to use a case statement.
# sscript19
# Usage: sscript19
echo Enter any day number from 1 to 7.
read day
case $day in
1) echo Monday ;;
2) echo Tuesday ;;
3) echo Wednesday ;;
4) echo Thursday ;;
5) echo Friday ;;
6) echo Saturday ;;
7) echo Sunday ;;
*) echo This not a legal day number. ;;
esac
Now let us see what the script does?
$ sscript19
Enter any day number from 1 to 7
4
Thursday
$ sscript19
Enter any day number from 1 to 7
7
Sunday
$ sscript19
Enter any day number from 1 to 7
0
This is not a legal day number.
In this program we have used labels in ascending order, that is 1, 2, 3, 4, and so on. Actually this is not necessary. You can use any label in any order. Following script illustrates this view.
# sscript20
# Usage: sscript20
echo Enter any prime number from 2 to 10.
read num
case $num in
7) echo You have entered prime number 7 ;;
2) echo You have entered prime number 3 ;;
5) echo You have entered prime number 5 ;;
3) echo You have entered prime number 3 ;;
*) echo This is not a legal prime number. ;;
esac
This script displays any prime number between 2 to 10; otherwise it would display the message “This is not a legal prime number”.
In last two examples of case statement we have used numbers to the value portion of the case statement and label cases. We can also use a shell script argument or output of a command as the value portion of the case statement.
Let us look at another script that illustrates it.
# sscript21
# Usage: sscript21
echo who
echo pwd
echo ls
echo date
echo Please enter the name of the command you want to run:
read choice
case $choice in
who) who ;;
pwd) pwd ;;
ls) ls ;;
date) date ;;
*) Please read the options again. ;;
esca
When you execute this script, you get the following output….
$ sscript21
who
pwd
ls
date
Please enter the name of the command you want to run:
pwd
\usr\user1
$ sscript20
who
pwd
ls
date
Please enter the name of the command you want to run:
cat
Please read the options again.
$
This script illustrates that you are not bound to use single digits or letters while checking the choices.
Another powerful feature of the case statement is that it attaches more than one label to the same response by using the logical OR operator ||. For instance, the next program shows how to use an OR operator.
# sscript22
# Usage: sscript22
echo Xenix
echo Linux
echo UNIX
echo Phenix
echo Which of the above is not an operating system?
read $choice
case $choice in
Phenix) echo You are really an intelligent person. ;;
Xenix || Linux || UNIX) echo Sorry wrong answer. ;;
*) echo What did you enter\? ;;
esca
When you execute this script, you get he following output….
$ sscript22
Xenix
Linux
UNIX
Phenix
Which of the above is not an operating system?
Xenix
Sorry wrong answer.
$ sscript22
Xenix
Linux
UNIX
Phenix
Which of the above is not an operating system?
Phenix
You are really an intelligent person.
$
Here if we read Xenix, or UNIX, or Linux, the program displays the same message.
We can also use the UNIX shell’s pattern-matching abilities in the choice labels. We have already used *) to match any pattern. Additionally we can also use the ? and [ ] metacharacters. These metacharacters are used for pattern-matching as we have used in matching filenames.
Let us see how to use them in a script.
# sscript23
# Usage: sscript23
echo Enter any word
read $word
case $word in
?) echo It is a single-letter word. ;;
??) echo It is a double-letter word. ;;
[aeiou]*) echo It starts with a small-case vowel. ;;
[AEIOU]*) echo It starts with an upper-case vowel. ;;
[!aeiou]*) echo It does not start with a small-case vowel. ;;
*[0-9]) echo It ends with a digit ;;
*) echo I am unable to trace it.
esac
This program prompts user to enter a word and then determines whether the user’s entered word is a single-letter word, double-letter word, starts with a small-case vowel, starts with an upper-case vowel, does not start with a small-case vowel or ends with a digit. When we run this program we get the following output….
$ sscript23
Enter any word
aish
It starts with a small-case vowel.
$ sscript23
Enter any word
S
It is a single-letter word.
$ sscript23
Enter any word
Mohit
It does not start with a small-case vowel.
$ sscript23
Enter any word
..Sachin
I am unable to trace it.
$
While using case statement, one should remember that it is not mandatory to use default *) case. If we don’t use it and no case label matches then the control is transferred to whatever comes after the case–esac statement.
Also if you want to use default case then make it as the last choice; otherwise the result could be wrong.
Like a shell variable, we can also use a command line argument for the value part of a case statement. For instance the next script shows how to use a command line argument.
# sscript24
# Usage: sscript24 char
case $1 in
[aeiou]) echo You have entered a vowel ;;
[0-9]) echo You have entered a digit ;;
[!aeiou]) echo You have not entered a vowel ;;
*) echo You have entered a special character ;;
esac
Here we have used a positional parameter instead of a regular shell variable for the case value. On executing this script, you got the following output….
$ sscript24 u
You have entered a vowel
$ sscript24 b
You have not entered a vowel
$ sscript24 6
You have entered a digit
$ sscript24 +
You have entered a special character
$
In each case, the command line argument was matched to the list of choices in the script.
Like nested ifs, the shell also provides the facility of using a case statement within another; that is nested case statements. But one should avoid nested case statements.