Previous Section  < Day Day Up >  Next Section

5.1. if/else

The simplest type of flow control construct is the conditional, embodied in bash's if statement. You use a conditional when you want to choose whether or not to do something, or to choose among a small number of things to do, according to the truth or falsehood of conditions. Conditions test values of shell variables, characteristics of files, whether or not commands run successfully, and other factors. The shell has a large set of built-in tests that are relevant to the task of shell programming.

The if construct has the following syntax:

if condition

then

    statements

[elif condition

    then statements...]

[else 

 statements]

fi

The simplest form (without the elif and else parts, or clauses) executes the statements only if the condition is true. If you add an else clause, you get the ability to execute one set of statements if a condition is true or another set of statements if the condition is false. You can use as many elif (a contraction of "else if") clauses as you wish; they introduce more conditions, and thus more choices for which set of statements to execute. If you use one or more elifs, you can think of the else clause as the "if all else fails" part.

5.1.1. Exit Status

Perhaps the only aspect of this syntax that differs from that of conventional languages like C and Pascal is that the "condition" is really a list of statements rather than the more usual Boolean (true or false) expression. How is the truth or falsehood of the condition determined? It has to do with a general UNIX concept that we haven't covered yet: the exit status of commands.

Every UNIX command, whether it comes from source code in C, some other language, or a shell script/function, returns an integer code to its calling process—the shell in this case—when it finishes. This is called the exit status. 0 is usually the OK exit status, while anything else (1 to 255) usually denotes an error. [1]

[1] Because this is a convention and not a "law," there are exceptions. For example, diff (find differences between two files) returns 0 for "no differences," 1 for "differences found," or 2 for an error such as an invalid filename argument.

if checks the exit status of the last statement in the list following the if keyword. The list is usually just a single statement. If the status is 0, the condition evaluates to true; if it is anything else, the condition is considered false. The same is true for each condition attached to an elif statement (if any).

This enables us to write code of the form:

if command ran successfully

then

   normal processing

else

   error processing

fi

More specifically, we can now improve on the pushd function that we saw in the last chapter:

pushd ( )

{

    dirname=$1

    DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}"

    cd ${dirname:?"missing directory name."}

    echo $DIR_STACK

}

This function requires a valid directory as its argument. Let's look at how it handles error conditions: if no argument is given, the third line of code prints an error message and exits. This is fine.

However, the function reacts deceptively when an argument is given that isn't a valid directory. In case you didn't figure it out when reading the last chapter, here is what happens: the cd fails, leaving you in the same directory you were in. This is also appropriate. But the second line of code has pushed the bad directory onto the stack anyway, and the last line prints a message that leads you to believe that the push was successful. Even placing the cd before the stack assignment won't help because it doesn't exit the function if there is an error.

We need to prevent the bad directory from being pushed and to print an error message. Here is how we can do this:

pushd ( )

{

  dirname=$1

  if cd ${dirname:?"missing directory name."}    # if cd was successful

  then

      DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}" # push the directory        

      echo $DIR_STACK

  else

      echo still in $PWD.                        # else do nothing

  fi

}

The call to cd is now inside an if construct. If cd is successful, it will return 0; the next two lines of code are run, finishing the pushd operation. But if the cd fails, it returns with exit status 1, and pushd will print a message saying that you haven't gone anywhere.

Notice that in providing the check for a bad directory, we have slightly altered the way pushd functions. The stack will now always start out with two copies of the first directory pushed onto it. That is because $PWD is expanded after the new directory has been changed to. We'll fix this in the next section.

You can usually rely on built-in commands and standard UNIX utilities to return appropriate exit statuses, but what about your own shell scripts and functions? For example, what if you wrote a cd function that overrides the built-in command?

Let's say you have the following code in your .bash_profile.

cd ( )

{

    builtin cd "$@"

    echo "$OLDPWD --> $PWD"

}

The function cd simply changes directories and prints a message saying where you were and where you are now. Because functions have higher priority than most built-in commands in the shell's order of command look-up, we need to make sure that the built-in cd is called, otherwise the shell will enter an endless loop of calling the function, known as infinite recursion.

The builtin command allows us to do this. builtin tells the shell to use the built-in command and ignore any function of that name. Using builtin is easy; you just give it the name of the built-in you want to execute and any parameters you want to pass. If you pass in the name of something which isn't a built-in command, builtin will display an appropriate message. For example: builtin: alice: not a shell builtin.

We want this function to return the same exit status that the built-in cd returns. The problem is that the exit status is reset by every command, so it "disappears" if you don't save it immediately. In this function, the built-in cd's exit status disappears when the echo statement runs (and sets its own exit status).

Therefore, we need to save the status that cd sets and use it as the entire function's exit status. Two shell features we haven't seen yet provide the way. First is the special shell variable ?, whose value ($?) is the exit status of the last command that ran. For example:

cd baddir

echo $?

causes the shell to print 1, while the following command causes it to print 0:

cd gooddir

echo $?

So, to save the exit status we need to assign the value of ? to a variable with the line es=$? right after the cd is done.

5.1.2. Return

The second feature we need is the statement return N, which causes the surrounding function to exit with exit status N. N is actually optional; it defaults to the exit status of the last command. Functions that finish without a return statement (i.e., every one we have seen so far) return whatever the last statement returns. return can only be used inside functions, and shell scripts that have been executed with source. In contrast, the statement exit N exits the entire script, no matter how deeply you are nested in functions.

Getting back to our example: if the call to the built-in cd were last in our cd function, it would behave properly. Unfortunately, we really need the assignment statement where it is. Therefore we need to save cd's exit status and return it as the function's exit status. Here is how to do it:

cd ( )

{

    builtin cd "$@"

    es=$?

    echo "$OLDPWD --> $PWD"

    return $es

}

The second line saves the exit status of cd in the variable es; the fourth returns it as the function's exit status. We'll see a substantial cd "wrapper" in Chapter 7.

Exit statuses aren't very useful for anything other than their intended purpose. In particular, you may be tempted to use them as "return values" of functions, as you would with functions in C or Pascal. That won't work; you should use variables or command substitution instead to simulate this effect.

5.1.3. Combinations of Exit Statuses

One of the more obscure parts of bash syntax allows you to combine exit statuses logically, so that you can test more than one thing at a time.

The syntax statement1 && statement2 means, "execute statement1, and if its exit status is 0, execute statement2." The syntax statement1 || statement2 is the converse: it means, "execute statement1, and if its exit status is not 0, execute statement2." At first, these look like "if/then" and "if not/then" constructs, respectively. But they are really intended for use within conditions of if constructs—as C programmers will readily understand.

It's much more useful to think of these constructs as "and" and "or," respectively. Consider this:

if statement1 && statement2

then

    ...

fi

In this case, statement1 is executed. If it returns a 0 status, then presumably it ran without error. Then statement2 runs. The then clause is executed if statement2 returns a 0 status. Conversely, if statement1 fails (returns a non-zero exit status), then statement2 doesn't even run; the last statement that actually ran was statement1, which failed—so the then clause doesn't run, either. Taken all together, it's fair to conclude that the then clause runs if statement1 and statement2 both succeeded.

Similarly, consider this:

if statement1 || statement2

then

    ...

fi

If statement1 succeeds, then statement2 does not run. This makes statement1 the last statement, which means that the then clause runs. On the other hand, if statement1 fails, then statement2 runs, and whether the then clause runs or not depends on the success of statement2. The upshot is that the then clause runs if statement1 or statement2 succeeds.

bash also allows you to reverse the return status of a statement with the use of !, the logical "not". Preceding a statement with ! will cause it to return 0 if it fails and 1 if it succeeds. We'll see an example of this at the end of this chapter.

As a simple example of testing exit statuses, assume that we need to write a script that checks a file for the presence of two words and just prints a message saying whether either word is in the file or not. We can use grep for this: it returns exit status 0 if it found the given string in its input, non-zero if not:

filename=$1

word1=$2

word2=$3

     

if grep $word1 $filename || grep $word2 $filename

then

    echo "$word1 or $word2 is in $filename."

fi

The then clause of this code runs if either grep statement succeeds. Now assume that we want the script to say whether the input file contains both words. Here's how to do it:

filename=$1

word1=$2

word2=$3

     

if grep $word1 $filename && grep $word2 $filename

then

    echo "$word1 and $word2 are both in $filename."

fi

We'll see more examples of these logical operators later in this chapter.

5.1.4. 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.

[2] The built-in command test is synonymous with [...]. For example, to test the equivalence of two strings you can either put [ string1 = string2 ] or test string1 = string2.

[3] [[...]] is not available in versions of bash prior to 2.05.

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.

[ 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.

5.1.4.1 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 5-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]

[5] NetPBM is a free, portable graphics conversion utility package. Further details can be found on the NetPBM homepage http://netpbm.sourceforge.net/

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.

5.1.4.2 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:

[7] Remember that the same permission flag that determines execute permission on a regular file determines search permission on a directory. This is why the -x operator checks both things depending on file type.

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.

5.1.5. Integer Conditionals

The shell also provides a set of arithmetic tests. These are different from character string comparisons like < and >, which compare lexicographic values of strings,[8] not numeric values. For example, "6" is greater than "57" lexicographically, just as "p" is greater than "ox," but of course the opposite is true when they're compared as integers.

[8] "Lexicographic order" is really just "dictionary order."

The integer comparison operators are summarized in Table 5-3.

Table 5-3. Arithmetic test operators

Test

Comparison

-lt

Less than

-le

Less than or equal

-eq

Equal

-ge

Greater than or equal

-gt

Greater than

-ne

Not equal


You'll find these to be of the most use in the context of the integer variables we'll see in the next chapter. They're necessary if you want to combine integer tests with other types of tests within the same conditional expression.

However, the shell has a separate syntax for conditional expressions that involve integers only. It's considerably more efficient, so you should use it in preference to the arithmetic test operators listed above. Again, we'll cover the shell's integer conditionals in the next chapter.

    Previous Section  < Day Day Up >  Next Section