Thursday, November 12, 2009

Writing Startup Scripts














Writing Startup Scripts


Shell scripts are combinations of shell and user commands that are executed in noninteractive mode for a wide variety of purposes. Whether you require a script that converts a set of filename extensions or need to alert the system administrator by e-mail that disk space is running low, shell scripts can be used. The commands that you place inside a shell script should normally execute in the interactive shell mode as well, making it easy it to take apart large scripts and debug them line by line in your normal login shell. In this section, we will only examine shell scripts that run under the Bourne shell— although many of the scripts will work without modification using other shells, it is always best to check the syntax chart of your own shell before attempting to run the scripts on another shell.




Processing Shell Arguments


A common goal of writing shell scripts is to make them as general as possible, so that they can be used with many different kinds of input. For example, in the cat examples presented earlier, we wouldn’t want to have to create an entirely new script for every file that we wanted to insert data into. Fortunately, shell scripts are able to make use of command-line parameters, which are numerically ordered arguments that are accessible from within a shell script. For example, a shell script to move files from one computer to another computer might require parameters for the source host, the destination host, and the name of the file to be moved. Obviously, we want to be able to pass these arguments to the script, rather than “hardwiring” them into the code. This is one advantage of shell scripts (and Perl programs) over compiled languages like C: scripts are easy to modify, and their operation is completely transparent to the user.


Arguments to shell scripts can be identified by a simple scheme: the command executed is referred to with the argument $0, with the first parameter identified as $1, the second parameter identified by $2, and so on, up to a maximum of nine parameters.


Thus, a script executed with the parameters


display_hardware.sh cdrom scsi ide

would refer internally to “cdrom” as $1, “scsi” as $2, and “ide” as $3. This approach would be particularly useful when calling smaller scripts from the main .profile script.


Let’s see how arguments can be used effectively within a script to process input parameters. The first script we will create simply counts the number of lines in a file (using the wc command), specified by a single command-line argument ($1). To begin with, we create an empty script file:


touch count_lines.sh

Next, we set the permissions on the file to be executable:


chmod +x count_lines.sh

Next, we edit the file


vi count_lines.sh

and add the appropriate code:


#!/bin/sh
echo "Number of lines in file " $1
wc –l $1

The script will take the first command-line argument, then print the number of lines, and then exit. We run the script with this command


./count_lines.sh /etc/group

which gives the following output:


Number of lines in file /etc/group
43

Although the individual activity of scripts is quite variable, the procedure of creating the script file, setting its permissions, editing its contents, and executing it on the command line remains the same across scripts. Of course, you may wish to make the script only available to certain users or groups for execution—this can be enabled by using the chmod command, and explicitly adding or removing permissions when necessary.






Testing File Properties


One of the assumptions that we made in the previous script was that the file specified by $1 actually existed; if it didn’t exist, we obviously would not be able to count the number of lines it contained. If the script is running from the command line, we can safely debug it and interpret any error conditions that arise (such as a file not existing, or having incorrect permissions). However, if a script is intended to run as a scheduled job (using the cron or at facility), it is impossible to debug in real time. Thus, it is often useful to write scripts that can handle error conditions gracefully and intelligently, rather than leaving administrators wondering why a job didn’t produce any output when it was scheduled to run.


The number one cause of runtime execution errors is the incorrect setting of file permissions. Although most users remember to set the executable bit on the script file itself, they often neglect to include error checking for the existence of data files that are used by the script. For example, if we want to write a script that checked the syntax of a configuration file (like the Apache configuration file, httpd.conf), we need to make sure the file actually exists before performing the check; otherwise, the script may not return an error message, and we may erroneously assume that the script file is correctly configured.


Fortunately, Bourne shell makes it easy to test for the existence of files by using the (conveniently named) test facility. In addition to testing for file existence, files that exist can also be tested for read, write, and execute permissions prior to any read, write, or execute file access being attempted by the script. Let’s revise our previous script that counted the number of lines in a file by first verifying that the target file (specified by $1) exists, and then printing the result; otherwise, an error message will be displayed:


#!/bin/sh
if test -a $1
then
echo "Number of lines in file " $1
wc –l $1
else
echo "The file" $1 "does not exist"
fi

When we run this command, if a file exists, it should count the number of lines in the target file as before; otherwise, an error message will be printed. If the /etc/group file did not exist, for example, we’d really want to know about it:


./count_lines.sh /etc/group
The file /etc/group does not exist

There may be some situations where we want to test another file property. For example, the /etc/shadow password database must only be readable by the superuser. Thus, if we execute a script to check whether or not the /etc/shadow file is readable by a nonprivileged user, it should not return a positive result. We can check file readability by using the -r option rather than the -a option. Here’s the revised script:


#!/bin/sh
if test –r $1 then
echo "I can read the file " $1
else
echo "I can't read the file" $1
fi

The following file permissions can also be tested using the test facility:





  • -b File is a special block file.





  • -c File is a special character file.





  • -d File is a directory.





  • -f File is a normal file.





  • -h File is a symbolic link.





  • -p File is a named pipe.





  • -s File has nonzero size.





  • -w File is writable by the current user.





  • -x File is executable by the current user.








Looping


All programming languages have the ability to repeat blocks of code for a specified number of iterations. This makes performing repetitive actions very easy for a well-written program. The Bourne shell is no exception. It features a for loop, which repeats the actions of a code block for a specified number of iterations as defined by a set of consecutive arguments to the for command. In addition, an iterator is available within the code block to indicate which of the sequence of iterations that will be performed is currently being performed. If that sounds a little complicated, let’s have a look at a concrete example, which uses a for loop to generate a set of filenames. These filenames are then tested using the test facility to determine whether or not they exist:


#!/bin/sh
for i in apple orange lemon kiwi guava
do
DATAFILE=$i".dat"
echo "Checking" $DATAFILE
if test -s $DATAFILE
then
echo $DATAFILE "is OK"

else
echo $DATAFILE "has zero-length"

fi
done

The for loop is repeated five times, with the variable $i taking on the values apple, orange, lemon, kiwi, and guava. Thus, when on the first iteration, when $i=apple, the shell interprets the for loop in the following way:


FILENAME="apple.dat"
echo "Checking apple.dat"
if test -s apple.dat
then
echo "apple.dat has zero-length"
else
echo "apple.dat is OK"
fi

If we run this script in a directory with files of zero length, we would expect to see the following output:


./zero_length_check.sh
Checking apple.dat
apple.dat is zero-length
Checking orange.dat
orange.dat is zero-length
Checking lemon.dat
lemon.dat is zero-length
Checking kiwi.dat
kiwi.dat is zero-length
Checking guava.dat
guava.dat is zero-length

However, if we entered data into each of the files, we should see them receive the “OK” message:


./zero_length_check.sh
Checking apple.dat
apple.dat is OK
Checking orange.dat
orange.dat is OK
Checking lemon.dat
lemon.dat is OK
Checking kiwi.dat
kiwi.dat is OK
Checking guava.dat
guava.dat is OK





Using Shell Variables


In the previous example, we assigned different values to a shell variable, which was used to generate filenames for checking. It is common to modify variables within scripts by using export, and to attach error codes to instances where variables are not defined within a script. This is particularly useful if a variable that is available within a user’s interactive shell is not available in their noninteractive shell. For example, we can create a script called show_errors.sh that returns an error message if the PATH variable is not set:


#!/bin/sh
echo ${PATH:?PATH_NOT_SET}

Of course, since the PATH variable is usually set, we should see output similar to the following:



# ./path_set.sh
/sbin:/bin:/usr/games/bin:/usr/sbin:/root/bin:/usr/local/bin:/usr/local/sbin/
:/usr/bin:
/usr/X11R6/bin: /usr/games:/opt/gnome/bin:/opt/kde/bin


However, if the PATH was not set, we would see the following error message:


./show_errors.sh: PATH_NOT_SET

It is also possible to use system-supplied error messages, by not specifying the optional error string:


#!/bin/sh
echo ${PATH:?}

Thus, if the PATH variable is not set, we would see the following error message:


# ./path_set.sh
./showargs: PATH: parameter null or not set

We can also use the numbered shell variables ($1, $2, $3, and so forth) to capture the space-delimited output of certain commands, and perform actions based on the value of these variables using the set command. For example, the command


# set `ls`

will sequentially assign each of the fields within the returned directory listing to a numbered shell variable. So, if our directory listing contained the entries


apple.dat   guava.dat   kiwi.dat   lemon.dat   orange.dat

We could retrieve the values of these filenames by using the echo command:


# echo $1
apple.dat
# echo $2
guava.dat
# echo $3
kiwi.dat
# echo $4
lemon.dat
# echo $5
orange.dat

This approach is very useful if your script needs to perform some action based on only one component of the date. For example, if you wanted to create a unique filename to assign to a compressed file, you could combine the values of each variable with a .Z extension to produce a set of strings like orange.dat.Z.













No comments: