Software Development

Beginning with BASH, part 3: BASH scripts

Vincent Danen examines the power of BASH scripting and shows you how to combine the many elements of BASH scripting into one program.

In parts 1 and 2 of this series, I showed you how to customize BASH (Bourne Again Shell) and discussed some of the basics of shell scripting. You learned how to use variables and test expressions. Now I’m going to take a closer look at conditional statements and write some BASH scripts.

BASH uses two types of conditional statements: the if statement and the case statement. These statements are used to determine how to run the script based on whether certain conditions are True.

The if statement
The if statement is also known as the if-then-else statement. It is probably the more widely used of the two different conditional statements. This statement provides you with an easy way to perform complicated conditional tests in your shell scripts, and it even allows you to nest statements—which can make it more useful and yet more complex.

The basic syntax of the if statement is
if [ expression ]
then
commands
elif [ expression2 ]
then
commands2
else
commands3
fi


The if part of the statement marks the beginning of the conditional test. To translate the above to straightforward English, you are testing your condition on the expressions defined (see part 2 for more information on expressions). The above would read as If [expression] is true, then execute [commands], else if [expression2] is true, then execute [commands2]. If [expression2] is false, execute [commands3]. The fi token marks the end of the if statement.

A sample if statement might look like
if [ $1 = "test" ]
then
echo "We are in test mode."
else
echo "We are in process mode."
fi


The if statements are very flexible and may be nested within each other. This means you can perform multiple tests in a row based on previous results. For example, you can test your main variable (let's say to see whether we are in process or test mode, like the above example shows). Perhaps our script processes a certain type of file. We need to know whether the file we want to process is readable or executable, but only if we are running in test mode. If we are running in process mode, we want to execute the file. This can be accomplished with nested if statements by testing to see if $1 (first argument on the command line) is "test"; if so, another if statement tests for the type of file:
if [ $1 = "test" ]
then
echo "We are in test mode."
if [ -r ~/test.file ]
then
echo "Test file is readable, ok to process."
elif [ -x ~/test.file ]
then
echo "Test file is executable, changing permissions."
chmod 444 ~/test.file
else
echo "Test file is neither readable nor executable. Exiting."
fi
else
echo "We are in process mode."
... [ process the file here ]
fi


As you can see, your if statements can get quite complex if you need to test various things.

The case statement
The other conditional statement type is the case statement. Sometimes you need to compare a pattern with several other patterns, and an if statement would be far too tedious for this. At that point, the case statement is more appropriate. The case statement in BASH is more powerful than similar statements in Pascal (case) or C (“switch”) because it can compare strings containing wildcard characters (as opposed to the strict enumerations or integer values in the Pascal and C equivalents).

The syntax for a case statement is
case string1 in
string2)
commands;;
string3)
commands;;
*)
commands;;
esac


The script will compare string1 with string2 and string3. If one of these strings match string1, the associated commands up to the double semicolon (;;) are executed. If neither string2 nor string3 matches string1, the default commands (following the asterisk) are executed. This is because the asterisk, like the wildcard using the same character, matches all strings.

For example, if you want to test the arguments passed to the script, you might use the following:
case $1 in
test)
echo "Running in test mode."
;;
process)
echo "Running in process mode."
;;
safe)
echo "Running in safe mode."
;;
*)
echo "Unknown command."
exit
;;
esac


The above will test $1 (or the first argument passed to the script) to see whether it will run in test, process, or safe mode. If none of these modes is specified, we indicate that we are given an unknown command and exit the program. The program could be run like
~/myscript.sh safe

and the resulting match would be to run the program in safe mode since $1 would be equal to "safe.”

You can use other types of statements in your shell scripts. These are called iteration, or looping, statements. The most common iteration statement is the for statement, but others, such as until and while, are also useful and share the same approach.

The for statement
The for statement executes the commands contained within it a specified number of times. There are two forms of the for statement. The first form has the following syntax:
for var1 in list
do
commands
done


This form of the for statement executes once for each item in the list. The list can be a variable that contains several words separated by spaces, or it can be a list of values that are typed directly in the statement. Each time the loop is executed, the current item in the list is assigned to the var1 variable until the last item in the list is reached. In addition, the var1 variable is accessible in the statement during the looping (as $var1).

The second form of the for statement has the following syntax:
for var1
do
commands
done


This form of the for statement executes once for each item in the variable var1. When this form is used, the shell script assumes that the var1 variable contains all the arguments that were passed to the script on the command line. It would be equivalent to writing
for var1 in "$@"
do
commands
done


A for statement might be useful if you want to translate characters from uppercase to lowercase. In the following example, we list the files in the current directory, and change any uppercase characters to lowercase characters (the script's filename is myscript.sh):
list="`ls -A *`"
for file in $list
do
if [ $file != "myscript.sh" ]
then
echo $file >test.file
newfile="`tr A-Z a-z <test.file`"
mv $file $newfile
rm -f test.file
fi
done


The until statement
The until statement executes a block of code while a provided conditional expression is False, until it becomes True. The syntax for the until statement is
until expression
do
commands
done


For example, if you want to get the number of arguments passed to the script, you might use something like this:
count=1
until [ -z "$*" ]
do
echo "Argument $count is $1"
shift
count=`expr $count + 1`
done


The above starts the counter at 1, for the first argument. Then we count up for each argument passed to the program. After we echo the argument number and value, we shift the argument over (see the shift command below), and then add 1 to the current value of count.

The while statement
The while statement is similar to the until statement; however, it works in reverse. Where the until statement runs until an expression is True, the while statement runs until the expression is False. It also has a similar syntax:
while expression
do
commands
done


For example, to do the same thing we did with the until statement earlier (obtain the number of arguments passed to the script), we might do something like
count=1
while [ -n "$*" ]
do
echo "Argument $count is $1"
shift
count=`expr $count + 1`
done


This works exactly the same as the until script. You'll notice that the only difference is the tested expression, which tests to make sure the argument has a value greater than zero, whereas the until script tested the argument against having a value of zero. In other words, using the while statement, we ran the commands while the value of $* was greater than zero. In the until script, we ran the commands until the value of $* was zero.

The shift command
There is another helpful command that you can make use of in BASH scripts. The shift command moves the current values stored in the positional parameters ($1, $2, etc.) one position to the left (or shifts the values left). For example, assume you pass your script the arguments "file1 file2 file3". The result is $1 is equal to "file1", $2 is equal to "file2", and $3 is equal to "file3". If you then were to execute the shift command, $1 would be equal to "file2" and $2 would be equal to "file3", and $3 would have a null value, as there is no fourth position.

You can also specify a number of positions to shift. The default shift command shifts the positions by one to the left. If you were to issue shift 2, then the positions would be shifted by two values to the left. Using the above example, only $1 would have a value, which would be "file3", and both $2 and $3 would have null values.

The shift command becomes very useful when you are parsing command-line arguments, as the following illustration shows. In the next example, we have a program that requires four command-line options but only two user-defined options. The program uses the argument
~/myscript.sh -m [test|process] -o output

The -m argument leads to the run mode (test or process) while -o leads to the output filename. A valid command line for this script might be
~/myscript.sh -m test -o outfile.txt

The contents of myfile.sh might be similar to the following:
while [ "$1" ]
do
if [ "$1" = "-m" ]
then
mode="$2"
shift 2
elif [ "$1" = "-o" ]
then
output="$2"
shift 2
else
echo "Unrecognized option $1"
exit
fi
done


The above code does some very simple argument parsing for your script. It tests to see if $1 is equal to "-m" first, and if it is, it sets the $mode variable to either test or process (in our example, it would be "test"). It then shifts the positions over by two, so that what were previously the values of $3 and $4 become the values of $1 and $2. It then tests to see if $1 is equal to "-o", and if it is, sets the $output variable to the output filename (in our example, it would be outfile.txt). Order is not necessary; it does not matter whether the mode or output is specified first. For even further error checking, you could perform another test to make sure that the value of $mode is either "test" or "process", and if it is neither, to exit the program with an error message indicating the correct choices.

Functions
BASH allows you to define your own functions in shell scripts, which is very useful for larger programs. The advantage of defining functions is that the resulting code is easier to read and maintain, as well as smaller. The main benefit is that you can reuse the same code in different parts of your script by specifying the code within a function, and then calling the function whenever you need to use the code in your script. Functions in BASH behave the same way as functions defined in C or other programming languages.

The syntax for creating a function is
func () {
commands
}


The syntax is similar to most other programming languages as well. Once the function is defined, you can use the function (and the code contained within it) by calling it with the following command:
func [arg1 arg2 arg3 ...]

where func is the name of the function (notice it is used to both define and call the function), and arg1, arg2, arg3, and so on are the arguments to pass to the function. This adds even more flexibility, since the function can then respond differently based on the arguments passed to it. You can think of functions in terms of being smaller separate programs called by the main script or program. When you pass arguments to a function, the function treats them as positional parameters, the same as a script would with arguments passed on the command line.

Functions should be listed first in all scripts. This is because the script is read by BASH from top to bottom and processed in that order. BASH does not recognize functions that are defined after the function is called and will exit with an error message. When a function is called, BASH looks to see what it has already processed and expects to find the called function defined.

The following example makes use of a number of the topics discussed in this Daily Drill Down:
#!/bin/sh
upcase() {
# convert all file names to upper case if mode is process, otherwise list
if [ "$1" = "-m" ]
then
shift
else
shift 3
fi
list="`ls -A *`"
for file in $list
do
if [ "$file" != "myscript.sh" ]
then
echo $file >test.file
newfile="`tr a-z A-Z <test.file`"
rm -f test.file
if [ "$1" = "test" ]
then
echo "Old file: $file New file: $newfile"
elif [ "$1" = "process" ]
then
mv $file $newfile
else
echo "Invalid mode: $1"
exit
fi
fi
done;
}


lcase() {
# convert all file names to lower case if mode is process, otherwise list
if [ "$1" = "-m" ]
then
shift
else
shift 3
fi
list="`ls -A *`"
for file in $list
do
if [ "$file" != "myscript.sh" ]
then
echo $file >test.file
newfile="`tr A-Z a-z <test.file`"
rm -f test.file
if [ "$1" = "test" ]
then
echo "Old file: $file New file: $newfile"
elif [ "$1" = "process" ]
then
mv $file $newfile
else
echo "Invalid mode: $1"
exit
fi
fi
done;
}


help() {
# display help
echo "Syntax for $0 is:"
echo " -t [upper|lower] (translate current directory to upper or \ lower case file names)"
echo " -m [test|process} (run in test or process mode)"
echo " or -h for help";
}


case $1 in
-t)
if [ "$2" = "upper" ]
then
upcase $@
elif [ "$2" = "lower" ]
then
lcase $@
else
echo "-t must be of type upper or lower"


fi
;;
-m)
shift 2
if [ "$2" = "upper" ]
then
upcase $@
elif [ "$2" = "lower" ]
then
lcase $@
else
echo "-t must be of type upper or lower"
fi
;;
-h)
help $@
;;
*)
echo "Syntax: $0 -t [upper|lower] -m [test|process]"
echo "Syntax: $0 -h (for help)"
esac


The above script has elements of a number of items that I discussed in this Daily Drill Down. The script basically is a directory translator, and it can convert entire directories to all uppercase or all lowercase filenames. A mode type must be specified on the command line as a safeguard, since case on Linux systems is very sensitive. Using test mode, the user can see the output of the directory by comparing old filenames to new filenames. Using process mode, the user can actually convert the entire directory into one case or another. Including the program "-h" on the command line will provide a more detailed help, and if the first argument is nothing other than "-h", "-m", or "-t", a short error message is displayed.

All things considered, the above script is relatively easy, but it does illustrate the power of BASH scripting and serves as a good example of how to combine the many elements of BASH scripting into one program.

Vincent Danen, a native Canadian in Edmonton, Alberta, has been computing since the age of 10, and hehas been using Linux for nearly two years. Prior to that, he used OS/2 exclusively for approximately four years. Vincent is a firm believer in the philosophy behind the Linux "revolution,” and heattempts to contribute the Linux causein as many ways as possible—from his Freezer Burn Web site to building and submitting custom RPMs for the Linux Mandrake project. Vincent also has obtained his Linux Administrator certification from Brainbench. He hopes to tackle the RHCE once it can be taken in Canada.

The authors and editors have taken care in preparation of the content contained herein, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for any damages. Always have a verified backup before making any changes.

About

Vincent Danen works on the Red Hat Security Response Team and lives in Canada. He has been writing about and developing on Linux for over 10 years and is a veteran Mac user.

0 comments

Editor's Picks