Skip to main content
p. 299

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:

  1. The shell runs the command after the if keyword and collects the exit code of that command.
  2. If the exit code is 0, the shell executes the commands that follow the then keyword, stopping when it reaches an else or fi keyword.
  3. If the exit code is not 0 and there’s an else clause, the shell runs the commands after the else keyword.
  4. 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.

note

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
p. 301

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 ]
p. 302

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
note

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

OperatorTests for
-fRegular file
-dDirectory
-hSymbolic link
-bBlock device
-cCharacter device
-pNamed pipe
-SSocket
p. 303
note

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

OperatorPermission
-rReadable
-wWritable
-xExecutable
-uSetuid
-gSetgid
-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

OperatorReturns true when the first argument is ___________ the second
-eqequal to

(continued)

p. 304

Table 11-3: Arithmetic Comparison Operators (continued)

OperatorReturns true when the first argument is ___________ the second
-nenot equal to
-ltless than
-gtgreater than
-leless than or equal to
-gegreater 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:

  1. The script matches $1 against each case value demarcated with the ) character.
  2. If a case value matches $1, the shell executes the commands below the case until it encounters ;;, at which point it skips to the esac keyword.
  3. 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.

note

End each case with a double semicolon (;;) to avoid a possible syntax error.