Tests

Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting
Prev |  | Next

# Chapter 7. Tests

Every reasonably complete programming language can test for a condition, then act according to the result of the test. Bash has the test command, various bracket and parenthesis operators, and the if/then construct.

# 7.1. Test Constructs

- An if/then construct tests whether the exit status of a list of commands is 0 (since 0 means "success" by UNIX convention), and if so, executes one or more commands.

- There exists a dedicated command called [ (left bracket special character). It is a synonym for test, and a builtin for efficiency reasons. This command considers its arguments as comparison expressions or file tests and returns an exit status corresponding to the result of the comparison (0 for true, 1 for false).

- With version 2.02, Bash introduced the [[ ... ]] extended test command, which performs comparisons in a manner more familiar to programmers from other languages. Note that [[ is a keyword, not a command.

  Bash sees `[[ $a -lt $b ]]` as a single element, which returns an exit status.

- The (( ... )) and let ... constructs return an exit status, according to whether the arithmetic expressions they evaluate expand to a non-zero value. These arithmetic-expansion constructs may therefore be used to perform arithmetic comparisons.

         1 (( 0 && 1 ))                 # Logical AND
         2 echo $?     # 1     ***
         3 # And so ...
         4 let "num = (( 0 && 1 ))"
         5 echo $num   # 0
         6 # But ...
         7 let "num = (( 0 && 1 ))"
         8 echo $?     # 1     ***
         9
        10
        11 (( 200 || 11 ))              # Logical OR
        12 echo $?     # 0     ***
        13 # ...
        14 let "num = (( 200 || 11 ))"
        15 echo $num   # 1
        16 let "num = (( 200 || 11 ))"
        17 echo $?     # 0     ***
        18
        19
        20 (( 200 | 11 ))               # Bitwise OR
        21 echo $?                      # 0     ***
        22 # ...
        23 let "num = (( 200 | 11 ))"
        24 echo $num                    # 203
        25 let "num = (( 200 | 11 ))"
        26 echo $?                      # 0     ***
        27
        28 # The "let" construct returns the same exit status
        29 #+ as the double-parentheses arithmetic expansion.

  | Again, note that the exit status of an arithmetic expression is not an error value.
         1 var=-2 && (( var+=2 ))
         2 echo $?                   # 1
         3
         4 var=-2 && (( var+=2 )) && echo $var
         5                           # Will not echo $var!

- An if can test any command, not just conditions enclosed within brackets.

         1 if cmp a b &> /dev/null  # Suppress output.
         2 then echo "Files a and b are identical."
         3 else echo "Files a and b differ."
         4 fi
         5
         6 # The very useful "if-grep" construct:
         7 # -----------------------------------
         8 if grep -q Bash file
         9   then echo "File contains at least one occurrence of Bash."
        10 fi
        11
        12 word=Linux
        13 letter_sequence=inu
        14 if echo "$word" | grep -q "$letter_sequence"
        15 # The "-q" option to grep suppresses output.
        16 then
        17   echo "$letter_sequence found in $word"
        18 else
        19   echo "$letter_sequence not found in $word"
        20 fi
        21
        22
        23 if COMMAND_WHOSE_EXIT_STATUS_IS_0_UNLESS_ERROR_OCCURRED
        24   then echo "Command succeeded."
        25   else echo "Command failed."
        26 fi

- These last two examples courtesy of Stéphane Chazelas.

Example 7-1. What is truth?

       1 #!/bin/bash
       2
       3 #  Tip:
       4 #  If you're unsure how a certain condition might evaluate,
       5 #+ test it in an if-test.
       6
       7 echo
       8
       9 echo "Testing \"0\""
      10 if [ 0 ]      # zero
      11 then
      12   echo "0 is true."
      13 else          # Or else ...
      14   echo "0 is false."
      15 fi            # 0 is true.
      16
      17 echo
      18
      19 echo "Testing \"1\""
      20 if [ 1 ]      # one
      21 then
      22   echo "1 is true."
      23 else
      24   echo "1 is false."
      25 fi            # 1 is true.
      26
      27 echo
      28
      29 echo "Testing \"-1\""
      30 if [ -1 ]     # minus one
      31 then
      32   echo "-1 is true."
      33 else
      34   echo "-1 is false."
      35 fi            # -1 is true.
      36
      37 echo
      38
      39 echo "Testing \"NULL\""
      40 if [ ]        # NULL (empty condition)
      41 then
      42   echo "NULL is true."
      43 else
      44   echo "NULL is false."
      45 fi            # NULL is false.
      46
      47 echo
      48
      49 echo "Testing \"xyz\""
      50 if [ xyz ]    # string
      51 then
      52   echo "Random string is true."
      53 else
      54   echo "Random string is false."
      55 fi            # Random string is true.
      56
      57 echo
      58
      59 echo "Testing \"\$xyz\""
      60 if [ $xyz ]   # Tests if $xyz is null, but...
      61               # it's only an uninitialized variable.
      62 then
      63   echo "Uninitialized variable is true."
      64 else
      65   echo "Uninitialized variable is false."
      66 fi            # Uninitialized variable is false.
      67
      68 echo
      69
      70 echo "Testing \"-n \$xyz\""
      71 if [ -n "$xyz" ]            # More pedantically correct.
      72 then
      73   echo "Uninitialized variable is true."
      74 else
      75   echo "Uninitialized variable is false."
      76 fi            # Uninitialized variable is false.
      77
      78 echo
      79
      80
      81 xyz=          # Initialized, but set to null value.
      82
      83 echo "Testing \"-n \$xyz\""
      84 if [ -n "$xyz" ]
      85 then
      86   echo "Null variable is true."
      87 else
      88   echo "Null variable is false."
      89 fi            # Null variable is false.
      90
      91
      92 echo
      93
      94
      95 # When is "false" true?
      96
      97 echo "Testing \"false\""
      98 if [ "false" ]              #  It seems that "false" is just a string ...
      99 then
     100   echo "\"false\" is true." #+ and it tests true.
     101 else
     102   echo "\"false\" is false."
     103 fi            # "false" is true.
     104
     105 echo
     106
     107 echo "Testing \"\$false\""  # Again, uninitialized variable.
     108 if [ "$false" ]
     109 then
     110   echo "\"\$false\" is true."
     111 else
     112   echo "\"\$false\" is false."
     113 fi            # "$false" is false.
     114               # Now, we get the expected result.
     115
     116 #  What would happen if we tested the uninitialized variable "$true"?
     117
     118 echo
     119
     120 exit 0

Exercise. Explain the behavior of Example 7-1, above.

       1 if [ condition-true ]
       2 then
       3    command 1
       4    command 2
       5    ...
       6 else  # Or else ...
       7       # Adds default code block executing if original condition tests false.
       8    command 3
       9    command 4
      10    ...
      11 fi

| When if and then are on same line in a condition test, a semicolon must terminate the if statement. Both if and then are keywords. Keywords (or commands) begin statements, and before a new statement on the same line begins, the old one must terminate.
       1 if [ -x "$filename" ]; then

Else if and elif

elif

  `elif` is a contraction for else if. The effect is to nest an inner if/then construct within an outer one.

         1 if [ condition1 ]
         2 then
         3    command1
         4    command2
         5    command3
         6 elif [ condition2 ]
         7 # Same as else if
         8 then
         9    command4
        10    command5
        11 else
        12    default-command
        13 fi

The `if test condition-true` construct is the exact equivalent of `if [ condition-true ]`. As it happens, the left bracket, [ , is a token [1] which invokes the test command. The closing right bracket, ] , in an if/test should not therefore be strictly necessary, however newer versions of Bash require it.

| The test command is a Bash builtin which tests file types and compares strings. Therefore, in a Bash script, test does not call the external `/usr/bin/test` binary, which is part of the sh-utils package. Likewise, [ does not call `/usr/bin/[`, which is linked to `/usr/bin/test`.
     bash$ type test
     test is a shell builtin
     bash$ type '['
     [ is a shell builtin
     bash$ type '[['
     [[ is a shell keyword
     bash$ type ']]'
     ]] is a shell keyword
     bash$ type ']'
     bash: type: ]: not found
If, for some reason, you wish to use `/usr/bin/test` in a Bash script, then specify it by full pathname.

Example 7-2. Equivalence of test, `/usr/bin/test`, [ ], and `/usr/bin/[`

       1 #!/bin/bash
       2
       3 echo
       4
       5 if test -z "$1"
       6 then
       7   echo "No command-line arguments."
       8 else
       9   echo "First command-line argument is $1."
      10 fi
      11
      12 echo
      13
      14 if /usr/bin/test -z "$1"      # Equivalent to "test" builtin.
      15 #  ^^^^^^^^^^^^^              # Specifying full pathname.
      16 then
      17   echo "No command-line arguments."
      18 else
      19   echo "First command-line argument is $1."
      20 fi
      21
      22 echo
      23
      24 if [ -z "$1" ]                # Functionally identical to above code blocks.
      25 #   if [ -z "$1"                should work, but...
      26 #+  Bash responds to a missing close-bracket with an error message.
      27 then
      28   echo "No command-line arguments."
      29 else
      30   echo "First command-line argument is $1."
      31 fi
      32
      33 echo
      34
      35
      36 if /usr/bin/[ -z "$1" ]       # Again, functionally identical to above.
      37 # if /usr/bin/[ -z "$1"       # Works, but gives an error message.
      38 #                             # Note:
      39 #                               This has been fixed in Bash, version 3.x.
      40 then
      41   echo "No command-line arguments."
      42 else
      43   echo "First command-line argument is $1."
      44 fi
      45
      46 echo
      47
      48 exit 0

| Following an if, neither the test command nor the test brackets ( [ ] or [[ ]] ) are strictly necessary.
       1 dir=/home/bozo
       2
       3 if cd "$dir" 2>/dev/null; then   # "2>/dev/null" hides error message.
       4   echo "Now in $dir."
       5 else
       6   echo "Can't change to $dir."
       7 fi
The "if COMMAND" construct returns the exit status of COMMAND. Similarly, a condition within test brackets may stand alone without an if, when used in combination with a list construct.
       1 var1=20
       2 var2=22
       3 [ "$var1" -ne "$var2" ] && echo "$var1 is not equal to $var2"
       4
       5 home=/home/bozo
       6 [ -d "$home" ] || echo "$home directory does not exist."

The (( )) construct expands and evaluates an arithmetic expression. If the expression evaluates as zero, it returns an exit status of 1, or "false". A non-zero expression returns an exit status of 0, or "true". This is in marked contrast to using the test and [ ] constructs previously discussed.

Example 7-3. Arithmetic Tests using (( ))

       1 #!/bin/bash
       2 # arith-tests.sh
       3 # Arithmetic tests.
       4
       5 # The (( ... )) construct evaluates and tests numerical expressions.
       6 # Exit status opposite from [ ... ] construct!
       7
       8 (( 0 ))
       9 echo "Exit status of \"(( 0 ))\" is $?."         # 1
      10
      11 (( 1 ))
      12 echo "Exit status of \"(( 1 ))\" is $?."         # 0
      13
      14 (( 5 > 4 ))                                      # true
      15 echo "Exit status of \"(( 5 > 4 ))\" is $?."     # 0
      16
      17 (( 5 > 9 ))                                      # false
      18 echo "Exit status of \"(( 5 > 9 ))\" is $?."     # 1
      19
      20 (( 5 == 5 ))                                     # true
      21 echo "Exit status of \"(( 5 == 5 ))\" is $?."    # 0
      22 # (( 5 = 5 ))  gives an error message.
      23
      24 (( 5 - 5 ))                                      # 0
      25 echo "Exit status of \"(( 5 - 5 ))\" is $?."     # 1
      26
      27 (( 5 / 4 ))                                      # Division o.k.
      28 echo "Exit status of \"(( 5 / 4 ))\" is $?."     # 0
      29
      30 (( 1 / 2 ))                                      # Division result < 1.
      31 echo "Exit status of \"(( 1 / 2 ))\" is $?."     # Rounded off to 0.
      32                                                  # 1
      33
      34 (( 1 / 0 )) 2>/dev/null                          # Illegal division by 0.
      35 #           ^^^^^^^^^^^
      36 echo "Exit status of \"(( 1 / 0 ))\" is $?."     # 1
      37
      38 # What effect does the "2>/dev/null" have?
      39 # What would happen if it were removed?
      40 # Try removing it, then rerunning the script.
      41
      42 # ======================================= #
      43
      44 # (( ... )) also useful in an if-then test.
      45
      46 var1=5
      47 var2=4
      48
      49 if (( var1 > var2 ))
      50 then #^      ^      Note: Not $var1, $var2. Why?
      51   echo "$var1 is greater than $var2"
      52 fi     # 5 is greater than 4
      53
      54 exit 0

### Notes

[1] | A token is a symbol or short string with a special meaning attached to it (a meta-meaning). In Bash, certain tokens, such as [ and . (dot-command), may expand to keywords and commands.

Prev | Home | Next
Exit and Exit Status | Up | File test operators