11.5 Conditionals
The Bourne shell has special constructs for conditionals, including if/then/else and case statements. For example, this simple script with an if conditional checks to see whether the script’s first argument is hi:
#!/bin/sh
if [ $1 = hi ]; then
echo 'The first argument was "hi"'
else
echo -n 'The first argument was not "hi" -- '
echo It was '"'$1'"'
fi
The words if, then, else, and fi in the preceding script are shell keywords; everything else is a command. This distinction is extremely important because it’s easy to mistake the conditional, [ $1 = "hi" ], for special shell syntax. In fact, the [ character is an actual program on a Unix system. All Unix systems have a command called [ that performs tests for shell script conditionals. This program is also known as test; the manual pages for test and [ are the same. (You’ll soon learn that the shell doesn’t always run [, but for now you can think of it as a separate command.)
Here’s where it’s vital to understand the exit codes as explained in Section 11.4. Let’s look at how the previous script actually works:
- The shell runs the command after the
ifkeyword and collects the exit code of that command. - If the exit code is
0, the shell executes the commands that follow thethenkeyword, stopping when it reaches anelseorfikeyword. - If the exit code is not
0and there’s anelseclause, the shell runs the commands after theelsekeyword. - The conditional ends at
fi.
We’ve established that the test following if is a command, so let’s look at the semicolon (;). It’s just the regular shell marker for the end of a command, and it’s there because we put the then keyword on the same line. Without the semicolon, the shell passes then as a parameter to the [ command, which often results in an error that isn’t easy to track. You can avoid the semicolon by placing the then keyword on a separate line as follows:
if [ $1 = hi ]
then
echo 'The first argument was "hi"'
fi
11.5.1 A Workaround for Empty Parameter Lists
There’s a potential problem with the conditional in the preceding example, due to a commonly overlooked scenario: $1 could be empty, because the p. 300 user might run the script with no parameters. If $1 is empty, the test reads [ = hi ], and the [ command will abort with an error. You can fix this by enclosing the parameter in quotes in one of two common ways:
if [ "$1" = hi ]; then
if [ x"$1" = x"hi" ]; then
11.5.2 Other Commands for Tests
There are many possibilities for using commands other than [ for tests. Here’s an example that uses grep:
#!/bin/sh
if grep -q daemon /etc/passwd; then
echo The daemon user is in the passwd file.
else
echo There is a big problem. daemon is not in the passwd file.
fi
11.5.3 elif
There is also an elif keyword that lets you string if conditionals together, as shown here:
#!/bin/sh
if [ "$1" = "hi" ]; then
echo 'The first argument was "hi"'
elif [ "$2" = "bye" ]; then
echo 'The second argument was "bye"'
else
echo -n 'The first argument was not "hi" and the second was not "bye"-- '
echo They were '"'$1'"' and '"'$2'"'
fi
Keep in mind that the control flows only through the first successful conditional, so if you run this script with the arguments hi bye, you’ll only get confirmation of the hi argument.
Don’t get too carried away with elif, because the case construct (which you’ll see in Section 11.5.6) is often more appropriate.
11.5.4 Logical Constructs
There are two quick, one-line conditional constructs that you may see from time to time, using the && (“and”) and || (“or”) syntax. The && construct works like this:
command1 && command2
Here, the shell runs command1, and if the exit code is 0, the shell also runs command2.
The || construct is similar; if the command before a || returns a nonzero exit code, the shell runs the second command.
The constructs && and || are often used in if tests, and in both cases, the exit code of the last command run determines how the shell processes the conditional. In the case of the && construct, if the first command fails, the shell uses its exit code for the if statement, but if the first command succeeds, the shell uses the exit code of the second command for the conditional. In the case of the || construct, the shell uses the exit code of the first command if successful, or the exit code of the second if the first is unsuccessful.
For example:
#!/bin/sh
if [ "$1" = hi ] || [ "$1" = bye ]; then
echo 'The first argument was "'$1'"'
fi
If your conditionals include the test command ([), as shown here, you can use -a and -o instead of && and ||, for example:
#!/bin/sh
if [ "$1" = hi -o "$1" = bye ]; then
echo 'The first argument was "'$1'"'
fi
You can invert a test (that is, a logical not) by placing the ! operator before a test. For example:
#!/bin/sh
if [ ! "$1" = hi ]; then
echo 'The first argument was not hi'
fi
In this specific case of comparisons, you might see != used as an alternative, but ! can be used with any of the condition tests described in the next section.
11.5.5 Testing Conditions
You’ve seen how [ works: the exit code is 0 if the test is true and nonzero when the test fails. You also know how to test string equality with [ str1 = str2 ]. However, remember that shell scripts are well suited to operations on entire files because many useful [ tests involve file properties. For example, the following line checks whether file is a regular file (not a directory or special file):
[ -f file ]
In a script, you might see the -f test in a loop similar to this one, which tests all of the items in the current working directory (you’ll learn more about loops in Section 11.6):
for filename in *; do
if [ -f $filename ]; then
ls -l $filename
file $filename
else
echo $filename is not a regular file.
fi
done
Because the test command is so widely used in scripts, it’s built in to many versions of the Bourne shell (including bash). This can speed up scripts because the shell doesn’t have to run a separate command for each test.
There are dozens of test operations, all of which fall into three general categories: file tests, string tests, and arithmetic tests. The info manual contains complete online documentation, but the test(1) manual page is a fast reference. The following sections outline the main tests. (I’ve omitted some of the less common ones.)
File Tests
Most file tests, like -f, are called unary operations because they require only one argument: the file to test. For example, here are two important file tests:
-e Returns true if a file exists
-s Returns true if a file is not empty
Several operations inspect a file’s type, meaning that they can determine whether something is a regular file, a directory, or some kind of special device, as listed in Table 11-1. There are also a number of unary operations that check a file’s permissions, as listed in Table 11-2. (See Section 2.17 for an overview of permissions.)
Table 11-1: File Type Operators
| Operator | Tests for |
|---|---|
-f | Regular file |
-d | Directory |
-h | Symbolic link |
-b | Block device |
-c | Character device |
-p | Named pipe |
-S | Socket |
If the test command is used on a symbolic link, it tests the actual object being linked to, not the link itself (except for the -h test). That is, if link is a symbolic link to a regular file, [ -f link ] returns an exit code of true (0).
Table 11-2: File Permissions Operators
| Operator | Permission |
|---|---|
-r | Readable |
-w | Writable |
-x | Executable |
-u | Setuid |
-g | Setgid |
-k | “Sticky” |
Finally, three binary operators (tests that need two files as arguments) are used in file tests, but they’re not terribly common. Consider this command, which includes -nt (“newer than”):
[ file1 -nt file2 ]
This exits true if file1 has a newer modification date than file2. The -ot (“older than”) operator does the opposite. And if you need to detect identical hard links, -ef compares two files and returns true if they share inode numbers and devices.
String Tests
You’ve seen the binary string operator =, which returns true if its operands are equal, and the != operator that returns true if its operands are not equal. There are two additional unary string operations:
-z Returns true if its argument is empty ([ -z "" ] returns 0)
-n Returns true if its argument is not empty ([ -n "" ] returns 1)
Arithmetic Tests
Note that the equal sign (=) looks for string equality, not numeric equality. Therefore, [ 1 = 1 ] returns 0 (true), but [ 01 = 1 ] returns false. When working with numbers, use -eq instead of the equal sign: [ 01 -eq 1 ] returns true. Table 11-3 provides the full list of numeric comparison operators.
Table 11-3: Arithmetic Comparison Operators
| Operator | Returns true when the first argument is ___________ the second |
|---|---|
-eq | equal to |
(continued)
Table 11-3: Arithmetic Comparison Operators (continued)
| Operator | Returns true when the first argument is ___________ the second |
|---|---|
-ne | not equal to |
-lt | less than |
-gt | greater than |
-le | less than or equal to |
-ge | greater than or equal to |
11.5.6 case
The case keyword forms another conditional construct that is exceptionally useful for matching strings. It does not execute any test commands and therefore does not evaluate exit codes. However, it can do pattern matching. This example tells most of the story:
#!/bin/sh
case $1 in
bye)
echo Fine, bye.
;;
hi|hello)
echo Nice to see you.
;;
what*)
echo Whatever.
;;
*)
echo 'Huh?'
;;
esac
The shell executes this as follows:
- The script matches
$1against each case value demarcated with the)character. - If a case value matches
$1, the shell executes the commands below the case until it encounters;;, at which point it skips to theesackeyword. - The conditional ends with
esac.
For each case value, you can match a single string (like bye in the preceding example) or multiple strings with | (hi|hello returns true if $1 equals hi or hello), or you can use the * or ? patterns (what*). To make a default case that catches all possible values other than the case values specified, use a single * as shown by the final case in the preceding example.
End each case with a double semicolon (;;) to avoid a possible syntax error.