|
DBA and Sysadmin: Linux
Improve Your Scripting with the test Command
by Emmett Dulaney
Underlying every conditional statement is a test to see what is true and what is not. Knowing how this works can make the difference between writing mediocre scripts and ones you'll be proud of.
The all too often underappreciated shell script is limited in power only by the ability of the person writing it. The more you know, the more you can conjure up a way to write a file that automates tasks and makes your administrative life easier.
Every operation undertaken in a shell script, beyond the simplest grouping of commands, requires an examination of conditions. All shell script "logic"to use the term in its loosest sensecan usually be broken into one of three following broad categories:
if {condition exists} then ...
while {condition exists} do ...
until {condition exists} do ...
Regardless of the subsequent action, each of these logic-based commands is dependent on knowing if a condition truly exists. The test command is the utility that makes it possible in every situation to determine whether or not the condition in question exists. For that reason, a thorough understanding of this command is crucial to writing successful shell scripts.
How It Works
The shortest-possible definition of the test command is that it appraises an expression and, if its condition is true, returns a 0 value. If the expression is not true, it returns a value greater than 0which can also be said to be a false value. The easiest method of checking the status of the last command performed is via the $? value, and examples throughout this article use that parameter for purposes of illustration.
The test command expects to find an argument on its command line, and when the shell has no value for a variable, the variable is considered null. This means that if a script is being processed, test reports the error whenever the script looks for an argument and does not find one.
When attempting to bulletproof scripts, you can solve this problem by enclosing all arguments within double quotes. The shell then expands the variables and, if no value exists, passes a null value to test. Another option is to add an extra check procedure within the script to see if the command-line arguments have been set. If not, the script can tell users that arguments are needed and then exit. All of this will make more sense as we work through some examples.
The test and [ Commands
Although the test command exists in every version of Linux and UNIX, there is an alias for it that is much more commonly used, the left bracket: [. Both test and its alias are usually found in /usr/bin or /bin, depending on the operating system version and vendor.
When you are using the left bracket instead of test, it must always be followed by a space, the condition to evaluate, a space, and the right bracket. The right bracket is not an alias for anything but signifies the end of the parameters that need evaluation. The spaces around the condition are needed to signify that test is being summoned, to distinguish this from a character/pattern-matching operation for which the brackets are also commonly used.
The syntax for test and [ is as follows:
test expression
[ expression ]
In both cases, test evaluates an expression and returns true or false. If it is used in conjunction with the if, while, or until command, you can have extensive control over the program flow. However, you do not need to use the test command with any other construct and can run it directly from the command line to check the status of virtually anything.
Because they are aliases for one another, use of either test or [ requires an expression. The expression is typically a comparison of text, numerals, or file and directory attributes and can consist of variables, constants, and operators. The operators can be string, integer, file, or Boolean operatorseach of which is examined, in turn, in the following sections.
The File test Operators
With these operators, you can take different actions within the program, depending on the outcome of an evaluation of the file type:
| -b file | True if the file is a block special file |
| -c file | True if the file is a character special file |
| -d file | True if the file is a directory |
| -e file | True if the file exists |
| -f file | True if the file is an ordinary file |
| -g file | True if the file has its SGID bit set |
| -G file | True if the file exists and is owned by the group |
| -k file | True if the file has its sticky bit set |
| -O file | True if the file exists and is owned by the user |
| -p file | True if the file is a named pipe |
| -r file | True if the file is readable |
| -s file | True if the file has nonzero length |
| -S file | True if the file is a socket special file |
| -t fd | True if fd is an open file descriptor associated with a terminal (fd is 1 by default) |
| -u file | True if the file has its SUID bit set |
| -w file | True if the file is writable |
| -x file | True if the file is executable |
The following example shows this simple operation in action:
$ ls -l
total 33
drwxr-xr-w 2 root root 1024 Dec 5 05:05 LST
-rw-rw-rw- 1 emmett users 27360 Feb 6 07:30 evan
-rwsrwsrwx 1 root root 152 Feb 6 07:32 hannah
drwxr-xr-x 2 emmett users 1024 Feb 6 07:31 karen
-rw------- 1 emmett users 152 Feb 6 07:29 kristin
-rw-r--r-- 1 emmett users 152 Feb 6 07:29 spencer
$
$ test -r evan
$ echo $?
0
$ test -r walter
$ echo $?
1
$
Because the first evaluation is truethe file does exist and is readablethe returned value is true, or 0. Because the file in the second evaluation does not exist, the value is false and the returned value is not zero. It is important to refer to the values as zero and nonzero, because a failure does not always return a 1 (although that is the usual value) and can return a number other than 0.
As mentioned at the beginning, instead of spelling out the word test, you can imply the same thing to the shell by surrounding a command with brackets "[ ]"as the following illustrates:
$ [ -w evan ]
$ echo $?
0
$ [ -x evan ]
$ echo $?
1
$
Once more, the first expression is true and the second is falseas indicated by the values returned. You can also compare two files with each other, using the following commands:
| file1 -ef file2 | Tests to see if the two files are associated with the same device and have the same inode number |
| file1 -nt file2 | Tests to see if the first file is newer than the second file, as determined by the modification date |
| file1 -ot file2 | Tests to see if the first file is older than the second file |
The following examples show the results of comparing files by using these operators:
$ [ evan -nt spencer ]
$ echo $?
0
$ [ karen -ot spencer ]
$ echo $?
1
$
The file named evan is newer than the file named spencer, and thus the evaluation is true. Similarly, the file named karen is newer than the one named spencer, and thus that evaluation is false.
The String Comparison Operators
As the title implies, this set of functions compares the value of strings. You can check to see if they exist, are the same, or are different.
| String | Tests to see if the string is not null |
| -n string | Tests to see if the string is not null; the string must be seen by test |
| -z string | Tests to see if the string is null; the string must be seen by test |
| string1 = string2 | Tests to see if string1 is identical to string2 |
| string1 != string2 | Tests to see if string1 is not identical to string2 |
One of the most useful tests on any variable is to see if its value is other than null, simply by placing it on the test command line, as in the following example:
$ test "$variable"
It is highly recommended that tests such as this be done with the variable enclosed in double quotation marks, which allows the shell to see the variable, even if it is equal to null. The basic string evaluation that is done by default and the -n tests are functionally equivalent, as illustrated in the following example:
#example1
if test -n "$1"
then
echo "$1"
fi
Executing the code in the preceding example gives the following results, based on whether or not $1 exists:
$ example1 friday
friday
$
$ example1
$
The result would be identical if the code were changed to
#example2
if test "$1"
then
echo "$1"
fi
as shown below:
$ example2 friday
friday
$
$ example2
$
All of this is to say that the -n is often not needed and represents the default action.
To look at the possibilities from a different angle, you can replace the -n with another option and check to see if the value is null (as opposed to not null). This can be accomplished with the -z option and is written as
#example3
if test -z "$1"
then
echo "no values were specified"
fi
This would run as follows:
$ example3
no values were specified
$ example3 friday
$
If the program is run with no command-line argument, the expression evaluates as true, so the text within the block is executed. If there is a value on the command line, the script exits, doing nothing. It is useful to put evaluation operations at the beginning of a script to check variable values up front before further processing that can generate errors.
The remaining string operators evaluate for an exact match, or lack thereof, between two variables/strings (equivalence and "inequivalence", if you will). The first example tests for a match:
$ env
LOGNAME=emmett
PAGER=less
SHELL=/bin/bash
TERM=linux
$
$ [ "$LOGNAME" = "emmett" ]
$ echo $?
0
$
$ [ "$LOGNAME" = "kristin" ]
$ echo $?
1
$
or, in the form of a script, this evaluation can be used to determine whether the script should execute or not:
#example4
if [ "$LOGNAME" = "emmett" ]
then
echo "processing beginning"
else
echo "incorrect user"
fi
This method can be used to look for any value, such as a terminal type or shell type, that must be matched before the script can be allowed to run. Note that when you are using the = or != operator, it has a higher priority than most other specifiable options and always expects expressions to follow. Therefore, neither = nor != can be used with the options that check for the existence of something such as a readable file, an executable file, or a directory, rather than compare strings.
Integer Comparison Operators
Just as the string comparison operators verify that strings are equal or different, the integer comparison operators perform the same functions for numbers. Expressions test as true if the variables match in value and false if they do not. Integer comparison operators do not work with strings, just as the string operators do not work with numbers:
| int1 -eq int2 | True if int1 is equal to int2 |
| int1 -ge int2 | True if int1 is greater than or equal to int2 |
| int1 -gt int2 | True if int1 is greater than int2 |
| int1 -le int2 | True if in1 is less than or equal to int2 |
| int1 -lt int2 | True if int1 is less than int2 |
| int1 -ne int2 | True if int1 is not equal to int2 |
The following example shows a snippet of code in which the value given on the command line must equal 7:
#example5
if [ $1 -eq 7 ]
then
echo "You've entered the magic number."
else
echo "You've entered the wrong number."
fi
In operation:
$ example5 6
You've entered the wrong number.
$
$ example5 7
You've entered the magic number.
$
As with strings, the values being compared can be variables assigned values outside of the script and don't always have to be provided on the command line. The following example illustrates one method of doing this:
#example6
if [ $1 -gt $number ]
then
echo "Sorry, but $1 is too high."
else
echo "$1 will work."
fi
$ set number=7
$ export number
$ example6 8
Sorry, but 8 is too high.
$ example6 7
7 will work.
$
One of the best uses for the integer comparison operators is to evaluate the number of command-line variables given and see that if it matches the required criteria. For example, if a particular command can run only with three or fewer variables,
#example7 - display variables, up to three
if [ "$#" -gt 3 ]
then
echo "You have given too many variables."
exit $#
fi
This will run without error (and return a value of 0) as long as three or fewer variables are given. If more than three variables are given, the error message will be displayed and the routine will exitreturning an exit code equal to the number of variables given on the command line.
A variation of this procedure might be to see if it is within the last few days of the month before allowing a report to run:
#example8 - to see if it is near the end of the month#
set `date` # use backward quotes
if [ "$3" -ge 21 ]
then
echo "It is close enough to the end of the month to proceed"
else
echo "This report cannot be run until after the 21st of the month"
exit $3
fi
In this example, six variables, differentiated from each other by blank space, are set:
$1 = Fri
$2 = Feb
$3 = 6
$4 = 08:56:30
$5 = EST
$6 = 2004
These values can be used in the script as if they had been fed in on the command line. Note that the exit command once again returns a valuein this case, the date it derives from the value of $3. This technique can be useful in troubleshootingif you think the script should run but it isn't running, look at the value of $?.
A similar idea might be to write a script that runs only on the third Thursday of each month. The third Thursday must fall between the 15th and 21st of the month. Using cron, you can call the script to execute at a specified time each day between the 15th and the 21st and then use the first lines of the script to see if the value of $1 (after the date is set) is Thu. If it is, execute the remainder of the script; if not, exit.
Yet another thought is to allow a script to run only when it is past 6:00 p.m. (18:00) and all the users have gone home. Simply write the script to exit if the value is less than 18, and obtain the hour (setting it to $1) by using the command
set `date +%H`
Boolean Operators
Boolean operators work the same in virtually every languageincluding shell scripting. In a nutshell, they check for multiple conditions to be true or false or take action on a false condition rather than a true one. The operators that work with test are
| ! expr | True if the expression evaluates false |
| expr1 -a expr2 | True if expr1 and expr2 evaluate true |
| expr1 -o expr2 | True if expr1 or expr2 evaluates true |
The != operator can be used in place of = for string evaluations. This is one of the simplest Boolean operators, negating the normal outcome of test.
The first of the other two operators is the -a, or AND, operator. For the test to finish as true, both expressions must evaluate as such. If either one evaluates as false, the entire test will evaluate as false. For example,
$ env
HOME=/
LOGNAME=emmett
MAIL=/usr/mail/emmett
PATH=:/bin:/usr/bin:/usr/lbin
TERM=linux
TZ=EST5:0EDT
$
$ [ "$LOGNAME" = "emmett" -a "$TERM" = "linux" ]
$ echo $?
0
$
$ [ "LOGNAME" = "karen" -a "$TERM" = "linux" ]
$ echo $?
1
$
In the first evaluation, both conditions test true (it is emmett who is logged in and on a linux terminal), so the entire evaluation is true. In the second evaluation, the terminal check is right but the user is wrong, so the whole thing evaluates as wrong.
In short, the AND operator can ensure that code is executed only when two conditions are met. In contrast, the OR (-o ) operator is true if either expression tests as true. Let's modify the earlier example and place it in a script to illustrate a point:
#example9
if [ "$LOGNAME" = "emmett" -o "$TERM" = "linux" ]
then
echo "Ready to begin."
else
echo "Incorrect user and terminal."
fi
$ env
HOME=/
LOGNAME=emmett
MAIL=/usr/mail/emmett
PATH=:/bin:/usr/bin:/usr/lbin
TERM=linux
TZ=EST5:0EDT
$ example9
Ready to begin.
$
$ LOGNAME=karen
$ example9
Ready to begin.
$
In the first running of the script, the evaluation sees if the user is equal to emmett. Discovering that it is, it goes to the echo statement and skips the rest of the check. It never checks to see if the terminal is equal to linux, because it needs to find only one true statement to make the whole operation true. In the second running of the script, it sees that the user is not emmett, so it then checks and discovers that the terminal is indeed linux. With one true condition, it now goes to the echo command. Both conditions would have to fail in order to elicit the second message.
In the earlier example that determined if it was the end of the month, a similar check could be done to stop the user from trying to run the script on the weekend:
#example10 - Do not let the script run over the weekend#
set `date` # use backward quotes
if [ "$1" = "Sat" -o "$1" = "Sun" ]
then
echo "This report cannot be run over the weekend."
fi
Some Useful Examples
Example 1: The simplest form of "logic" that appears in script files, as illustrated throughout, is "if ... then" statements. A snippet of earlier code checked for a certain number of variables and echoed them back. Suppose we modify this slightly, saying that we want to echo variables, and subtract the leftmost variable with each echo to make an inverted triangle of the display.
Although it may sound simplistic, it actually is not; it is how you would want to approach performing mass processing: work on the first variable, shift, work on the next variable, and so on.
For illustration purposes, the important lines of the script can be written as follows:
#example11 - display declining variables, up to three
if [ "$#" -gt 3 ] # see if more than three variables are given
then
echo "You have given more than three variables."
exit
fi
echo $*
if test -n "$2"
then
shift
echo $*
fi
if test -n "$2"
then
shift
echo $*
fi
It would perform as follows:
$ example11 one
one
$
$ example11 one two
one two
two
$
$ example11 one two three
one two three
two three
three
$
$ example11 one two three four
You have given more than three variables.
$
The reason for limiting the number to three variables, for purposes of examination, is to reduce the number of lines to be checked in the example. Here everything works as it should, although it is incredibly sloppy; the user gets a warning about the use of more variables than the program was designed for; and the script exits. If the number is 3 or less, the heart of the operations begins.
The variables are echoed, and a test is performed to see if another variable exists. If one does, a shift is done, the variables are echoed, another test is performed, and so forth. In total, 16 active lines are used, and the program still can handle no more than three variablessloppy, sloppy, sloppy. Assume that the variable-number limitation is removed and the program can handle any number of variables. With a few modifications, the script can be shortened (beautified) and be equipped to handle any number of variables:
#example12 - display declining variables, any number
while [ "$#" -gt 0 ]
do
echo $*
shift
done
$ example12 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
2 3 4 5 6 7 8 9 0
3 4 5 6 7 8 9 0
4 5 6 7 8 9 0
5 6 7 8 9 0
6 7 8 9 0
7 8 9 0
8 9 0
9 0
0
Now down to only five active lines, it is not burdened by the three-variable limitation of the first script and is much more effective in operation.
Example 2: Whenever a processing-dependent operation is carried out within a script, the next operation should always be to check the status of that operation and verify that it finished successfully. You can do this by checking the status of $? and verifying that it is equal to 0. For example, if it is crucial that a data directory be accessed,
#example13
TEMP=LST
cd $TEMP
if [ $? -ne 0 ]
then
echo "Data directory could not be found."
Exit
fi
Working Around Errors
|
Resources
Download Oracle Database 10g for Linux
Oracle Database 10g Release 1 (10.1.0.2) is currently available on Linux x86 and Linux Itanium platforms; download it from OTN for free here.
Visit the Linux Technology Center
Bookmark this page for technical information about Linux sysadmin best practices generally and the Oracle-on-Linux stack specifically.
Related Articles
Archive of Linux-related Technical Articles
|
There are truly only two types of errors that commonly occur with the test command. The first is a failure to use the correct type of evaluation, such as comparing a string variable against an integer or a string with padding with one without padding. Carefully evaluating the variables you are working with will culminate in an ah-ha moment and lets you work around those problems.
The second type of error involves mistaking brackets for something other than an alias. The brackets must have a space separating them from their contents; otherwise, they are unable to interpret the objects within. For example,
$ [ "$LOGNAME" -gt 9]
test: ] missing
$
Note that the error message indicates a problem with test, even though the alias ] was used. These problems are easy to spot, because the error messages identify them right away and you can add the needed spaces.
Conclusion
To build any logic into a shell script, you must add conditional statements. At the heart of every such statement lies an evaluation of a condition to see whether or not it existsan evaluation accomplished through the use of the test command. Understanding how this and its alias, the left bracket ([), work will enable you to write shell scripts that can do some remarkable things.
Emmett Dulaney (edulaney@iquest.net) has earned 18 vendor certifications. He has written several books on Linux, UNIX, and certification study; has spoken at numerous conferences; and is a former Mercury Technical Solutions partner.
|