Result Codes, Chaining, and Flags

This chapter covers concepts related to the arguments that scripts take and the results that they return to their caller. It consists of three parts:

Working with Result Codes

Result codes, also known as return values, exit statuses, and probably several other names, are one of the more critical features of shell scripting, as they play a role in almost every aspect of script execution.

Whenever a command executes (including the open bracket shell builtin used as part of the if and while statements), a result code is generated. If the command exits successfully, the result is usually zero (0). If the command exits with an error, the result code will vary according to the tool. (See the documentation for the tool in question for a list of result codes.) The possible range of result codes is 0-255.

There are three ways of testing to see if a script executes correctly. The first is with an immediate test using the if statement. For example:

if ls mysillyfilename ; then
    echo "File exists."
fi

The second way is by testing the last exit status returned. The exit status is stored in the shell variable $?. For example:

ls mysillyfilename
if [ $? = 0 ] ; then
    echo "File exists."
fi

The third way is by taking advantage of the “and” operator:

ls mysillyfilename && echo "File exists."

These three code examples should generate the same output. The third technique is explained further in “Chaining Execution.”

Chaining Execution

The shell provides three operators for chaining execution:and (&&), or (||) and not (!).

And (&&)

If the command to the left succeeds (has a zero exit status), the command to the right executes. Otherwise, it does not. The result code returned by this operation is success (zero) only if both commands return zero. Otherwise, its result code is whatever was returned by whichever command failed.

Or (||)

If the command to the left succeeds (has a zero exit status), the command to the right does not execute. If the command to the left fails, the command to the right does execute. If the leftmost command succeeds, the exit status returned by this operator is zero. Otherwise, the exit status returned is the exit status of the command to the right of the operator.

Not (!)

Executes the command to the right of the operator. If the command returns a zero exit status, the operator returns a nonzero exit status. If the command returns a nonzero exit status, the operator returns a zero exit status.

The three operators are shown in the following snippet:

ls / || ! ls mysillyfilename && echo "Whatever."

The operator precedence rules in Bourne shell scripts are very different from those in C. Parentheses are evaluated first, as they can be used to override grouping of operators. After that, however, evaluation of operators occurs in order from left to right.

For example, the following line lists all of the files in the root directory, then echoes “It’s a boy”:

 ls / || ls /xy && echo "It's a boy"

The || operator takes precedence over the && operator because of left-to-right evaluation rules. The shell shortcuts evaluation of the || operator. Thus, because ls / always succeeds, the || operator causes the second ls to be skipped entirely, and the statement up to the && operator evaluates to true (0). This value is then combined with the echo statement after it by the && operator. Thus, the echo statement executes afterwards.

You can modify the order of operations (or clarify it to avoid confusing people who are not used to languages without operator precedence) by adding parentheses, as shown in the next snippet:

ls / || ( ls /nonexistentfile && echo "file exists" )

In this case, because the first ls statement is successful, the remainder of the statement is skipped. If you replace the ls / with false, the failed listing of nonexistentfile generates an error message and a nonzero exit status, which in turn causes the echo statement to still be skipped.

Of course, the existence of these operators also means that you could write an if statement without actually using the if keyword, as shown in the following snippet:

FOO=3
[ $FOO -eq 3 ] && echo "three"

Because this decreases readability, however, this syntax is not recommended. This form is presented here only to help with comprehension of existing scripts.

Handling Flags and Arguments

Throughout this chapter and previous chapters, examples have shown basic argument handling with variables such as $1, $2, and so on. This is fine for simple scripts, but some scripts call for more advanced argument processing. This section describes several techniques for processing arguments.

Special Multi-argument Variables

The shell provides a number of special variables associated with argument lists:

$#.

Contains the number of arguments.

$*.

Expands to the list of arguments, starting from $1.

If this variable appears outside double quotes, each argument is treated as a single indivisible field for field splitting purposes. For example, if used in the argument list to a command, each original argument is passed to that command as a separate argument.

If this variable appears within double quotes, each argument is separated by the value of the IFS variable, and no field splitting occurs within the resulting block. Thus, if this variable is used as part of the argument list to a command, this entire IFS-delimited string is passed in as a single argument. See “Variable Expansion and Field Separators” for more information about the IFS variable.

Compatibility Note: In AIX, if you surround this variable with quotes, the shell wraps each individual argument with quotes when it expands the variable.

$@.

Expands to the list of arguments, starting from $1.

If this variable appears outside double quotes, argument splitting behavior is not defined by the specification. However, in most shells, text is split as though the entire contents of each argument were inserted as-is, separated by spaces, and without any quotes.

If this variable appears within double quotes, each argument is treated as a single indivisible field for field splitting purposes. Thus, if this variable is used within double quotes as part of the argument list to a command, each original argument is passed as a separate argument to that command.

In addition, if this variable appears within double quote marks along with other text ("BLAH$@BLAH", for example), the portion of the string prior to the $@ is prepended to the first argument, and the portion of the string after the $@ is appended to the last argument.

C Shell Note: This variable does not exist in C shell. Use $* instead.

The following code listings demonstrate the use of these arguments and the subtle differences between them.

Listing 5-1  00_listargs.sh

#!/bin/sh
 
for i in "$@" ; do
echo ARG $i
done

Listing 5-2  01_testargs.sh

#!/bin/sh
 
IFS="
"
 
echo "COUNT: $#"
echo
echo '\$*'
./00_listargs.sh $*
echo
echo '"\$*"'
./00_listargs.sh "$*"
echo
echo '$@'
./00_listargs.sh $@
echo
echo '"$@"'
./00_listargs.sh "$@"
echo
echo '"foo bar$*bar foo"'
./00_listargs.sh "foo bar$*bar foo"
echo
echo '"foo bar$@bar foo"'
./00_listargs.sh "foo bar$@bar foo"

Save these scripts with the filenames shown, then run them by typing ./01_testargs.sh This is a "silly test" and note the differences in the way these variables behave.

The shift Builtin

The shift builtin provides a way to remove arguments from the argument list. Each time you call the shift builtin, the first argument is deleted and the remaining arguments are shifted down by one. You can also specify an optional numeric argument to indicate how many times you want to shift the argument list.

The following script demonstrates the shift builtin:

Listing 5-3  02_shift.sh

#!/bin/sh
 
echo "\$1: $1 \$2: $2 \$3: $3 \$4: $4 \$5: $5 \$6: $6"
 
shift
 
echo "\$1: $1 \$2: $2 \$3: $3 \$4: $4 \$5: $5 \$6: $6"
 
shift 2
 
echo "\$1: $1 \$2: $2 \$3: $3 \$4: $4 \$5: $5 \$6: $6"

Run this script by typing ./02_shift.sh The quick brown fox jumped over the lazy dog. and notice how the arguments change. Initially, the first six arguments are "The quick brown fox jumped over". After the first shift statement, the first six arguments are "quick brown fox jumped over the". After the second shift statement, the first six arguments are "fox jumped over the lazy dog".

The getopts builtin and the getopt command

The getopts builtin and the getopt command both process a list of arguments in a manner that is similar to the getopt function in C. If you are writing a Bourne shell script, the getopts builtin is strongly recommended because it is faster, safer, and more flexible. (If you are writing a C shell script, the getopts builtin is not available.)

Both getopt and getopts take an option string as an argument. This option string is constructed as follows:

Simple flag

Just use the letter of the flag. For example, to add the "-f" flag, add the letter "f" to the option string.

Flag with argument

Use the letter of the flag followed by a colon. For example, if you want to accept something like "-o filename", you would add "o:" to the option string.

As a special option, the getopts built-in supports detection of unknown flags and missing arguments. To enable this option, add a colon (:) as the first character of the option string.

The getopts Builtin

The getopts builtin puts your script in control of the argument parsing process. Each call to getopts returns a single flag and, where applicable, the argument to that flag. The syntax is as follows:

getopts opt_string user_specified_variable [args]

The option string is described above in “The getopts builtin and the getopt command.” The user-specified variable is described below. The getopts builtin can also optionally take a list of arguments to process. You should generally omit this.

The getopts builtin modifies the values of the following variables:

user_specified_variable

The first option you pass to getopts is the name of a variable. The getopts variable puts the flag itself into the specified variable (without the leading hyphen).

OPTARG

The argument value associated with the current flag (if applicable).

OPTERR

In some shells, if this variable is set to 1, error reporting by the underlying getopt function is enabled. If set to 0, error reporting is disabled. This is not portable, but it is relatively harmless to set this variable “just in case”. This variable is ignored if the first character of the option string is a colon (:), which tells getopts that the script knows how to handle and report errors.

OPTIND

The index of the current argument being processed. You should set this to 1 before calling the getopts builtin for the first time (or to start over, processing the arguments again using a different set of options).

For example, the following script is a crude variant of the ls command. It takes an optional -l flag that enables long listings and an optional -o flag that contains the name of a file into which it writes its output. If no output file is specified, it writes its output to standard output. It also takes an optional path or list of paths that are passed to ls as-is.

Listing 5-4  03_getopts.sh

#!/bin/sh
 
DO_LONG=""
 
# Start processing options at index 1.
OPTIND=1
# OPTERR=1
OUTPUT_FILE=""
while getopts ":hlo:" VALUE "$@" ; do
 
    echo "GOT FLAG $VALUE"
 
    if [ "$VALUE" = "h" ] ; then
        echo "Usage: $0 [-l] [-o outputfile] [path ...]"
        exit 1
    fi
    if [ "$VALUE" = "l" ] ; then
        DO_LONG="-l"
    fi
    if [ "$VALUE" = "o" ] ; then
        echo "Set output file to \"$OPTARG\""
        OUTPUT_FILE="$OPTARG"
    fi
    # The getopt routine returns a colon when it encounters
    # a flag that should have an argument but doesn't.  It
    # returns the errant flag in the OPTARG variable.
    if [ "$VALUE" = ":" ] ; then
        echo "Flag -$OPTARG requires an argument."
        echo "Usage: $0 [-l] [-o outputfile] [path ...]"
        exit 1
    fi
    # The getopt routine returns a question mark when it
    # encounters an unknown flag.  It returns the unknown
    # flag in the OPTARG variable.
    if [ "$VALUE" = "?" ] ; then
        echo "Unknown flag -$OPTARG detected."
        echo "Usage: $0 [-l] [-o outputfile] [path ...]"
        exit 1
    fi
done
 
# The first non-flag argument is at index $OPTIND, so shift one fewer
# to move it into $1
shift `expr $OPTIND - 1`
 
if [ "$OUTPUT_FILE" = "" ] ; then
    ls $DO_LONG "$@"
else
    ls $DO_LONG "$@" > $OUTPUT_FILE
fi
 
exit $?

You should notice two things about this script. First, it takes advantage of the leading colon in the option string. This tells getopts that the script knows how to handle errors. Second, it provides two additional options—one for the colon (:) flag and one for the question mark (?) flag. The colon flags is returned when getopts encounters a flag with a missing argument. The question mark flag is returned when getopts encounters an unknown flag. These two additional cases are enabled by the leading colon in the option string.

The getopt Command

The getopt command takes a different approach than the getopts builtin. It processes the entire argument list at once and lets you know whether the argument list matches the list of valid flags or not. If the argument list matches, getopt canonicalizes the argument list, putting the flags and their optional arguments first (prior to any non-flag arguments), followed by a single "--" argument to indicate that there are no more flags to process.

The syntax of the getopt command is as follows:

getopt opt_string args

The following snippet behaves much like the one in Listing 5-4. Unlike in that example, it is not possible to programmatically detect the nature of errors (missing arguments or invalid flags).

Also, as noted previously, filenames containing spaces are not handled correctly by getopt. This is not a problem with the script. It is a fundamental limitation of the getopt tool and the way its output is parsed.

Listing 5-5  01_getopt.csh

#!/bin/csh
 
set OUTPUT_FILE=""
set DO_LONG=""
 
set argv=`getopt "hlo:" $*`
 
if ( $status != 0 ) then
    echo "Usage: $0 [-l] [-o outputfile] [path ...]"
    exit 1
endif
 
while ( "$1" != "--" )
        echo "GOT FLAG $1"
    switch($1)
        case "-h":
            echo "Usage: $0 [-l] [-o outputfile] [path ...]"
            exit 1
        case "-o":
            set OUTPUT_FILE="$2"
            shift
            breaksw
        case "-l":
            set DO_LONG="-l"
            breaksw
    endsw
    shift
end
 
shift # remove trailing --
 
# echo "ARGS: $*"
 
if ( "$OUTPUT_FILE" == "" ) then
    ls $DO_LONG $*
else
    ls $DO_LONG $* > $OUTPUT_FILE
endif
 
exit $status