The Bourne shell has special constructs for conditionals, such as if/then/else and case statements. Here is a simple script with an if conditional that checks to see if 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 is an extremely important point, because one of the commands is [ $1 = "hi" ]. The [ character is an actual program on a Unix system; it is not special shell syntax. All Unix systems have a command called [ that performs tests for shell script conditionals. This program is also known as test — careful examination of [ and test should reveal that they share an inode, or one is a symbolic link to the other.
Now you can see why the exit codes in Section 7.4 are so important, because this is how the whole process works:
The shell runs the command after the if keyword and collects the exit code of that command.
If the exit code is 0, the shell executes whatever commands follow the then keyword, stopping when it reaches an else or fi keyword.
If the exit code is not 0 and there is an else clause, the shell runs the commands after the else keyword.
The conditional ends at fi.
Note? |
There is a slight problem with the conditional in the preceding example — a very common mistake. $1 could be empty, because the user might not enter a parameter. Without a parameter, the test reads [ = "hi" ], and the [ command aborts with an error. You can fix it by enclosing the parameter in quotes in one of the two following ways (both are common): if [ "$1" = hi ]; then if [ x"$1" = x"hi" ]; then |
It is worth repeating that the stuff following if is always a command. You need a semicolon (;) after the test command — if you skip the semicolon, the shell passes then as a parameter to the test command. (If you don't like the semicolon, you can put the then keyword on a separate line.)
Here is an example that uses grep instead of the [ command:
#!/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
There is also an elif keyword that lets you string if conditionals together like this:
#!/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
Don't get too carried away with elif, because the case construct that you will see in Section 7.5.3 is usually more appropriate.
There are two quick one-line conditional constructs that you may see from time to time: && ("and") and || ("or").
The && construct works like this:
command1 && command2
Here, the shell runs command1, and if the exit code is 0, it also runs command2.
The || construct is similar; if the command before a || returns a nonzero exit code, the shell runs the second command.
&& and || often find their way into use in if tests. In both cases, the exit code of the last command run determines how the shell processes the conditional. In &&, if the first command fails, the shell uses its exit code for the if statement, but if the first command is successful, the shell uses the exit code of the second command for the conditional. For ||, the shell uses the exit code of the first command if successful, or the exit code of the second if the first is unsuccessful.
Here is an example:
#!/bin/sh if [ "$1" = hi ] || [ "$1" = bye ]; then echo 'The first argument was "'$1'"' fi
If your conditionals include the test ([) command (as here), you can use -a and -o instead of && and ||, as described in the next section.
You have already 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. An important reason for this is that the most 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 next one, which tests all of the items in the current working directory:
for filename in *; do if [ -f $filename ]; then ls -l $filename file $filename else echo $filename is not a regular file. fi done
You can invert a test by placing the ! operator before the test. For example, [ ! -f file ] returns true if file is not a regular file. Furthermore, the -a and -o flags are the and and or operators (for example, [ -f file1 -a file2 ]).
There are dozens of test operations that fall into three general categories: file tests, string tests, and arithmetic tests. The info pages contain complete online documentation, but the test(1) manual page is also a fast reference. The following sections outline the main tests (some of the less common tests have been omitted).
Most file tests, like -f, are called unary operations because they require only one argument — the file to test.
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. Those operations are listed in Table 7-1. There are also a number of unary operations that check a file's permissions, as listed in Table 7-2 on the next page (see Section 1.17 for an overview of permissions).
Operator |
Tests For |
---|---|
-f |
Regular file |
-d |
Directory |
-h |
Symbolic link |
-b |
Block device |
-c |
Character device |
-p |
Named pipe |
-S |
Socket |
Operator |
Returns True if the File Is: |
---|---|
-r |
Readable |
-w |
Writable |
-x |
Executable |
-u |
Setuid |
-g |
Setgid |
-k |
"Sticky" |
Note? |
Except for -h, test follows symbolic links. That is, if link is a symbolic link to a regular file, [ -f link ] returns an exit code of true (0). |
Finally, there are three binary operators (tests that need two files as arguments) that are used in file tests, but they are not terribly common. Consider this command that 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.
You have already seen the binary string operator =, which returns true if its operands are equal. The != operator returns true if its operands are not equal.
There are two 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)
It's important to recognize that = looks for string equality, not numeric equality. Therefore, [ 1 = 1 ] returns 0 (true), but [ 01 = 1 ] returns false. If you want to work with numbers, use -eq instead — [ 01 -eq 1 ] returns true. Table 7-3 provides the full list of numeric comparison operators.
Operator |
Returns True When the First Argument Is . . . the Second |
---|---|
-eq |
Equal to |
-ne |
Not equal to |
-lt |
Less than |
-gt |
Greater than |
-le |
Less than or equal to |
-ge |
Greater than or equal to |
The case keyword forms another conditional construct that is exceptionally useful for matching strings. case does not execute any test commands and therefore does not evaluate exit codes. Here is an example that should tell 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 $1 against each case value demarcated with the) character.
If a case value matches $1, the shell executes the commands below the case until encountering ;;. The shell then skips to the esac keyword.
The conditional ends with esac.
For each case value, you can match a single string (such as bye in the preceding example), multiple strings with | (hi|hello returns true if $1 equals hi or hello), or make use of the * or ? wildcards (what*). If you want to make a 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.
Note? |
Each case must end with a double semicolon (;;). You risk a syntax error otherwise. |