Condition Tests

Exit statuses are the only things an if construct can test. But that doesn't mean you can check only whether commands ran properly. The shell provides two ways of testing a variety of conditions. The first is with the [...] construct, which is available in many different versions of the Bourne shell.[2] The second is by using the newer [[...]] construct.[3] The second version is identical to the first except that word splitting and pathname expansion are not performed on the words within the brackets. For the examples in this chapter we will use the first form of the construct.

You can use the construct to check many different attributes of a file (whether it exists, what type of file it is, what its permissions and ownership are, etc.), compare two files to see which is newer, and do comparisons on strings.

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

[ condition ] is actually a statement just like any other, except that the only thing it does is return an exit status that tells whether condition is true. (The spaces after the opening bracket "[" and before the closing bracket "]" are required.) Thus it fits within the if construct's syntax.

String comparisons

The square brackets ([]) surround expressions that include various types of operators. We will start with the string comparison operators, listed in Table 5-1. (Notice that there are no operators for "greater than or equal" or "less than or equal" comparisons.) In the table, str1 and str2 refer to expressions with a string value.

Table 5-1. String comparison operators

Operator

True if...

str1 = str2[4]

str1 matches str2

str1 != str2

str1 does not match str2

str1 < str2

str1 is less than str2

str1 > str2

str1 is greater than str2

-n str1

str1 is not null (has length greater than 0)

-z str1

str1 is null (has length 0)

[4] Note that there is only one equal sign (=). This is a common source of error.

We can use one of these operators to improve our popd function, which reacts badly if you try to pop and the stack is empty. Recall that the code for popd is:

popd ( )
{
    DIR_STACK=${DIR_STACK#* }
    cd ${DIR_STACK%% *}
    echo "$PWD"
}

If the stack is empty, then $DIR_STACK is the null string, as is the expression ${DIR_STACK%% }. This means that you will change to your home directory; instead, we want popd to print an error message and do nothing.

To accomplish this, we need to test for an empty stack, i.e., whether $DIR_STACK is null or not. Here is one way to do it:

popd ( )
{
    if [ -n "$DIR_STACK" ]; then
        DIR_STACK=${DIR_STACK#* }
        cd ${DIR_STACK%% *}
        echo "$PWD"
    else
        echo "stack empty, still in $PWD."
    fi
}

In the condition, we have placed the $DIR_STACK in double quotes, so that when it is expanded it is treated as a single word. If you don't do this, the shell will expand $DIR_STACK to individual words and the test will complain that it was given too many arguments.

There is another reason for placing $DIR_STACK in double quotes, which will become important later on: sometimes the variable being tested will expand to nothing, and in this example the test will become [ -n ], which returns true. Surrounding the variable in double quotes ensures that even if it expands to nothing, there will be an empty string as an argument (i.e., [ -n "" ]).

Also notice that instead of putting then on a separate line, we put it on the same line as the if after a semicolon, which is the shell's standard statement separator character.

We could have used operators other than -n. For example, we could have used -z and switched the code in the then and else clauses.

While we're cleaning up code we wrote in the last chapter, let's fix up the error handling in the highest script (Task 4-1). The code for that script was:

filename=${1:?"filename missing."}
howmany=${2:-10}
sort -nr $filename | head -$howmany

Recall that if you omit the first argument (the filename), the shell prints the message highest: 1: filename missing. We can make this better by substituting a more standard "usage" message. While we are at it, we can also make the command more in line with conventional UNIX commands by requiring a dash before the optional argument.

if [ -z "$1" ]; then
    echo 'usage: highest filename [-N]'
else
  filename=$1
  howmany=${2:--10}
  sort -nr $filename | head $howmany
fi

Notice that we have moved the dash in front of $howmany inside the parameter expansion ${2:—10}.

It is considered better programming style to enclose all of the code in the if-then-else, but such code can get confusing if you are writing a long script in which you need to check for errors and bail out at several points along the way. Therefore, a more usual style for shell programming follows.

if [ -z "$1" ]; then
    echo 'usage: highest filename [-N]'
    exit 1
fi
    
filename=$1
howmany=${2:--10}
sort -nr $filename | head $howmany

The exit statement informs any calling program whether it ran successfully or not.

As an example of the = operator, we can add to the graphics utility that we touched on in Task 4-2. Recall that we were given a filename ending in .pcx (the original graphics file), and we needed to construct a filename that was the same but ended in .jpg (the output file). It would be nice to be able to convert several other types of formats to JPEG files so that we could use them on a web page. Some common types we might want to convert besides PCX include XPM (X PixMap), TGA (Targa), TIFF (Tagged Image File Format), and GIF.

We won't attempt to perform the actual manipulations needed to convert one graphics format to another ourselves. Instead we'll use some tools that are freely available on the Internet, graphics conversion utilities from the NetPBM archive. [5]

Don't worry about the details of how these utilities work; all we want to do is create a shell frontend that processes the filenames and calls the correct conversion utilities. At this point it is sufficient to know that each conversion utility takes a filename as an argument and sends the results of the conversion to standard output. To reduce the number of conversion programs necessary to convert between the 30 or so different graphics formats it supports, NetPBM has its own set of internal formats. These are called Portable Anymap files (also called PNMs) with extensions .ppm (Portable Pix Map) for color images, .pgm (Portable Gray Map) for grayscale images, and .pbm (Portable Bit Map) for black and white images. Each graphics format has a utility to convert to and from this "central" PNM format.

The frontend script we are developing should first choose the correct conversion utility based on the filename extension, and then convert the resulting PNM file into a JPEG:

filename=$1
extension=${filename##*.}
pnmfile=${filename%.*}.pnm
outfile=${filename%.*}.jpg

if [ -z $filename ]; then
    echo "procfile: No file specified"
    exit 1
fi

if [ $extension = jpg ]; then
    exit 0
elif [ $extension = tga ]; then
    tgatoppm $filename > $pnmfile
elif [ $extension = xpm ]; then
    xpmtoppm $filename > $pnmfile
elif [ $extension = pcx ]; then
    pcxtoppm $filename > $pnmfile
elif [ $extension = tif ]; then
    tifftopnm $filename > $pnmfile
elif [ $extension = gif ]; then
    giftopnm $filename > $pnmfile
else
    echo "procfile: $filename is an unknown graphics file."
    exit 1
fi

pnmtojpeg $pnmfile > $outfile

rm $pnmfile

Recall from the previous chapter that the expression ${filename%.*} deletes the extension from filename; ${filename##*.} deletes the basename and keeps the extension.

Once the correct conversion is chosen, the script runs the utility and writes the output to a temporary file. The second to last line takes the temporary file and converts it to a JPEG. The temporary file is then removed. Notice that if the original file was a JPEG we just exit without having to do any processing.

This script has a few problems. We'll look at improving it later in this chapter.

File attribute checking

The other kind of operator that can be used in conditional expressions checks a file for certain properties. There are 24 such operators. We will cover those of most general interest here; the rest refer to arcana like sticky bits, sockets, and file descriptors, and thus are of interest only to systems hackers. Refer to Appendix B for the complete list. Table 5-2 lists those that we will examine.

Table 5-2. File attribute operators

Operator

True if...

-a file

file exists

-d file

file exists and is a directory

-e file

file exists; same as - a

-f file

file exists and is a regular file (i.e., not a directory or other special type of file)

-r file

You have read permission on file

-s file

file exists and is not empty

-w file

You have write permission on file

-x file

You have execute permission on file, or directory search permission if it is a directory

-N file

file was modified since it was last read

-O file

You own file

-G file

file 's group ID matches yours (or one of yours, if you are in multiple groups)

file1 -nt file2

file1 is newer than file2 [6]

file1 -ot file2

file1 is older than file2

[6] Specifically, the -nt and -ot operators compare modification times of two files.

Before we get to an example, you should know that conditional expressions inside [ and ] can also be combined using the logical operators && and ||, just as we saw with plain shell commands, in the previous section entitled Section 5.1.3 ." For example:

if [ condition ] && [ condition ]; then

It's also possible to combine shell commands with conditional expressions using logical operators, like this:

if command && [ condition ]; then
    ...

You can also negate the truth value of a conditional expression by preceding it with an exclamation point (!), so that ! expr evaluates to true only if expr is false. Furthermore, you can make complex logical expressions of conditional operators by grouping them with parentheses (which must be "escaped" with backslashes to prevent the shell from treating them specially), and by using two logical operators we haven't seen yet: -a (AND) and -o (OR).

The -a and -o operators are similar to the && and || operators used with exit statuses. However, unlike those operators, -a and -o are only available inside a test conditional expression.

Here is how we would use two of the file operators, a logical operator, and a string operator to fix the problem of duplicate stack entries in our pushd function. Instead of having cd determine whether the argument given is a valid directory—i.e., by returning with a bad exit status if it's not—we can do the checking ourselves. Here is the code:

pushd ( )
{
    dirname=$1
    if [ -n "$dirname" ] && [ \( -d "$dirname" \) -a \
            \( -x "$dirname" \) ]; then
        DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}"
        cd $dirname
        echo "$DIR_STACK"
    else
        echo "still in $PWD."
    fi
}

The conditional expression evaluates to true only if the argument $1 is not null (-n), a directory (-d) and the user has permission to change to it (-x).[7] Notice that this conditional handles the case where the argument is missing ($dirname is null) first; if it is, the rest of the condition is not executed. This is important because, if we had just put:

if [ \( -n "$dirname"\) -a  \( -d "$dirname" \) -a \
         \( -x "$dirname" \) ]; then

the second condition, if null, would cause test to complain and the function would exit prematurely.

Here is a more comprehensive example of the use of file operators.


Task 5-1

Write a script that prints essentially the same information as ls -l but in a more user-friendly way.


Although the code for this task looks at first sight quite complicated, it is a straightforward application of many of the file operators:

if [ ! -e "$1" ]; then
    echo "file $1 does not exist."
    exit 1
fi
if [ -d "$1" ]; then
    echo -n "$1 is a directory that you may "
    if [ ! -x "$1" ]; then
        echo -n "not "
    fi
    echo "search."
elif [ -f "$1" ]; then
    echo "$1 is a regular file."
else
    echo "$1 is a special type of file."
fi
if [ -O "$1" ]; then
    echo 'you own the file.'
else
    echo 'you do not own the file.'
fi
if [ -r "$1" ]; then
    echo 'you have read permission on the file.'
fi
if [ -w "$1" ]; then
    echo 'you have write permission on the file.'
fi
if [ -x "$1" -a ! -d "$1" ]; then
    echo 'you have execute permission on the file.'
fi

We'll call this script fileinfo. Here's how it works:

 

 
  • The first conditional tests if the file given as argument does not exist (the exclamation point is the "not" operator; the spaces around it are required). If the file does not exist, the script prints an error message and exits with error status.
  • The second conditional tests if the file is a directory. If so, the first echo prints part of a message; remember that the -n option tells echo not to print a LINEFEED at the end. The inner conditional checks if you do not have search permission on the directory. If you don't have search permission, the word "not" is added to the partial message. Then, the message is completed with "search." and a LINEFEED.
  • The elif clause checks if the file is a regular file; if so, it prints a message.
  • The else clause accounts for the various special file types on recent UNIX systems, such as sockets, devices, FIFO files, etc. We assume that the casual user isn't interested in details of these.
  • The next conditional tests to see if the file is owned by you (i.e., if its owner ID is the same as your login ID). If so, it prints a message saying that you own it.
  • The next two conditionals test for your read and write permission on the file.
  • The last conditional checks if you can execute the file. It checks to see if you have execute permission and that the file is not a directory. (If the file were a directory, execute permission would really mean directory search permission.) In this test we haven't used any brackets to group the tests and have relied on operator precedence. Simply put, operator precedence is the order in which the shell processes the operators. This is exactly the same concept as arithmetic precedence in mathematics, where multiply and divide are done before addition and subtraction. In our case, [ -x "$1" -a ! -d "$1" ] is equivalent to [\( -x "$1" \) -a \( ! -d "$1" \) ]. The file tests are done first, followed by any negations (!) and followed by the AND and OR tests.

As an example of fileinfo's output, assume that you do an ls -l of your current directory and it contains these lines:

-rwxr-xr-x   1 cam      users        2987 Jan 10 20:43 adventure
-rw-r--r--   1 cam      users          30 Jan 10 21:45 alice
-r--r--r--   1 root     root        58379 Jan 11 21:30 core
drwxr-xr-x   2 cam      users        1024 Jan 10 21:41 dodo

alice and core are regular files, dodo is a directory, and adventure is a shell script. Typing fileinfo adventure produces this output:

adventure is a regular file.
you own the file.
you have read permission on the file.
you have write permission on the file.
you have execute permission on the file.

Typing fileinfo alice results in this:

alice is a regular file.
you own the file.
you have read permission on the file.
you have write permission on the file.

Finally, typing fileinfo dodo results in this:

dodo is a directory that you may search.
you own the file.
you have read permission on the file.
you have write permission on the file.

Typing fileinfo core produces this:

core is a regular file.
you do not own the file.
you have read permission on the file.