3.5. Tests

  • 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).

  • Bash also introduces the [[ ... ]] construct, 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 also return an exit status of 0 if the arithmetic expressions they evaluate expand to a non-zero value. These arithmetic expansion constructs may therefore be used to perform arithmetic comparisons.

       1 let "1<2" returns 0 (as "1<2" expands to "1")
       2 (( 0 && 1 )) returns 1 (as "0 && 1" expands to "0")

  • 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 if grep -q Bash file
       7 then echo "File contains at least one occurrence of Bash."
       8 fi
       9 
      10 if COMMAND_WHOSE_EXIT_STATUS_IS_0_UNLESS_ERROR_OCCURRED
      11 then echo "Command succeeded."
      12 else echo "Command failed."
      13 fi

  • An if/then construct can contain nested comparisons and tests.

       1 if echo "Next *if* is part of the comparison for the first *if*."
       2 
       3   if [[ $comparison = "integer" ]]
       4     then (( a < b ))
       5   else
       6     [[ $a < $b ]]
       7   fi
       8 
       9 then
      10   echo '$a is less than $b'
      11 fi

    This detailed "if-test" explanation courtesy of Stephane Chazelas.


Example 3-14. What is truth?

   1 #!/bin/bash
   2 
   3 echo
   4 
   5 echo "Testing \"0\""
   6 if [ 0 ]
   7 #zero
   8 then
   9   echo "0 is true."
  10 else
  11   echo "0 is false."
  12 fi
  13 
  14 echo
  15 
  16 echo "Testing \"NULL\""
  17 if [ ]
  18 #NULL (empty condition)
  19 then
  20   echo "NULL is true."
  21 else
  22   echo "NULL is false."
  23 fi
  24 
  25 echo
  26 
  27 echo "Testing \"xyz\""
  28 if [ xyz ]
  29 #string
  30 then
  31   echo "Random string is true."
  32 else
  33   echo "Random string is false."
  34 fi
  35 
  36 echo
  37 
  38 echo "Testing \"\$xyz\""
  39 if [ $xyz ]  # Tests if $xyz is null, but...
  40 #string
  41 then
  42   echo "Uninitialized variable is true."
  43 else
  44   echo "Uninitialized variable is false."
  45 fi
  46 
  47 echo
  48 
  49 echo "Testing \"-n \$xyz\""
  50 if [ -n "$xyz" ]  # ...this is better.
  51 #string
  52 then
  53   echo "Uninitialized variable is true."
  54 else
  55   echo "Uninitialized variable is false."
  56 fi
  57 
  58 echo
  59 
  60 # When is "false" true?
  61 
  62 echo "Testing \"false\""
  63 if [ "false" ]
  64 then
  65   echo "\"false\" is true."
  66 else
  67   echo "\"false\" is false."
  68 fi
  69 
  70 echo
  71 
  72 echo "Testing \"\$false\""  # Again, uninitialized variable.
  73 if [ "$false" ]
  74 then
  75   echo "\"\$false\" is true."
  76 else
  77   echo "\"\$false\" is false."
  78 fi
  79 
  80 
  81 echo
  82 
  83 exit 0

Exercise. Explain the behavior of Example 3-14, above.

   1 if [ condition-true ]
   2 then
   3    command 1
   4    command 2
   5    ...
   6 else
   7    # Optional (may be left out if not needed).
   8    # Adds default code block executing if original condition tests false.
   9    command 3
  10    command 4
  11    ...
  12 fi

Add a semicolon when 'if' and 'then' are on same line.

   1 if [ -x "$filename" ]; then

elif

This is a contraction for else if. The effect is to nest an inner if/then construction 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 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.

Note

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
 	      


Example 3-15. Equivalence of [ ] and test

   1 #!/bin/bash
   2 
   3 echo
   4 
   5 
   6 if test -z "$1"
   7 then
   8   echo "No command-line arguments."
   9 else
  10   echo "First command-line argument is $1."
  11 fi
  12 
  13 # Both code blocks are functionally identical.
  14 
  15 if [ -z "$1" ]
  16 # if [ -z "$1"
  17 # also works, but outputs an error message.
  18 then
  19   echo "No command-line arguments."
  20 else
  21   echo "First command-line argument is $1."
  22 fi
  23 
  24 
  25 echo
  26 
  27 exit 0

The [[ ]] construct is the shell equivalent of [ ]. No filename expansion or word splitting take place between [[ and ]], but there is parameter expansion and command substitution.

   1 file=/etc/passwd
   2 
   3 if [[ -e $file ]]
   4 then
   5   echo "Password file exists."
   6 fi

Tip

Using the newer [[ ... ]] test construct, rather than [ ... ] can prevent many logic errors in scripts.

Note

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 3-16. Arithmetic Tests using (( ))

   1 #!/bin/bash
   2 # Arithmetic tests.
   3 
   4 # The (( )) construct evaluates and tests numerical expressions.
   5 
   6 (( 0 ))
   7 echo "Exit status of \"(( 0 ))\" is $?."
   8 
   9 (( 1 ))
  10 echo "Exit status of \"(( 1\ ))" is $?."
  11 
  12 exit 0

3.5.1. File test operators

Returns true if...

-e

file exists

-f

file is a regular file (not a directory or device file)

-s

file is not zero size

-d

file is a directory

-b

file is a block device (floppy, cdrom, etc.)

-c

file is a character device (keyboard, modem, sound card, etc.)

-p

file is a pipe

-h

file is a symbolic link

-L

file is a symbolic link

-S

file is a socket

-t

file (descriptor) is associated with a terminal device

This test option may be used to check whether the stdin ([ -t 0 ]) or stdout ([ -t 1 ]) in a given script is a terminal.

-r

file has read permission (for the user running the test)

-w

file has write permission (for the user running the test)

-x

file has execute permission (for the user running the test)

-g

set-group-id (sgid) flag set on file or directory

If a directory has the sgid flag set, then a file created within that directory belongs to the group that owns the directory, not necessarily to the group of the user who created the file. This may be useful for a directory shared by a workgroup.

-u

set-user-id (suid) flag set on file

A binary owned by root with set-user-id flag set runs with root privileges, even when an ordinary user invokes it. [1] This is useful for executables (such as pppd and cdrecord) that need to access system hardware. Lacking the suid flag, these binaries could not be invoked by a non-root user.

 	      -rwsr-xr-t    1 root       178236 Oct  2  2000 /usr/sbin/pppd
 	      
A file with the suid flag set shows an s in its permissions.

-k

sticky bit set

Commonly known as the "sticky bit", the save-text-mode flag is a special type of file permission. If a file has this flag set, that file will be kept in cache memory, for quicker access. [2] If set on a directory, it restricts write permission. Setting the sticky bit adds a t to the permissions on the file or directory listing.

 	      drwxrwxrwt    7 root         1024 May 19 21:26 tmp/
 	      
If a user does not own a directory that has the sticky bit set, but has write permission in that directory, he can only delete files in it that he owns. This keeps users from inadvertently overwriting or deleting each other's files in a publicly accessible directory, such as /tmp.

-O

you are owner of file

-G

group-id of file same as yours

-N

file modified since it was last read

f1 -nt f2

file f1 is newer than f2

f1 -ot f2

file f1 is older than f2

f1 -ef f2

files f1 and f2 are hard links to the same file

!

"not" -- reverses the sense of the tests above (returns true if condition absent).

Example 3-158, Example 3-49, Example 3-45, and Example A-2 illustrate uses of the file test operators.

3.5.2. Comparison operators (binary)

integer comparison

-eq

is equal to

if [ "$a" -eq "$b" ]

-ne

is not equal to

if [ "$a" -ne "$b" ]

-gt

is greater than

if ["$a" -gt "$b" ]

-ge

is greater than or equal to

if [ "$a" -ge "$b" ]

-lt

is less than

if [ "$a" -lt "$b" ]

-le

is less than or equal to

if [ "$a" -le "$b" ]

string comparison

=

is equal to

if [ "$a" = "$b" ]

==

is equal to

if [ "$a" == "$b" ]

This is a synonym for =.

   1 [[ $a == z* ]]    # true if $a starts with an "z" (pattern matching)
   2 [[ $a == "z*" ]]  # true if $a is equal to z*
   3 
   4 [ $a == z* ]      # file globbing and word splitting take place
   5 [ "$a" == "z*" ]  # true if $a is equal to z*
   6 
   7 # Thanks, S.C.

!=

is not equal to

if [ "$a" != "$b" ]

This operator uses pattern matching within a [[ ... ]] construct.

\<

is less than, in ASCII alphabetical order

if [ "$a" \< "$b" ]

Note that the "<" needs to be escaped.

\>

is greater than, in ASCII alphabetical order

if [ "$a" \> "$b" ]

Note that the ">" needs to be escaped.

See Example 3-152 for an application of this comparison operator.

-z

string is "null", that is, has zero length

-n

string is not "null".

Caution

The -n test absolutely requires that the string be quoted within the test brackets. Using an unquoted string with ! -z, or even just the unquoted string alone within test brackets (see Example 3-18) normally works, however, this is an unsafe practice. Always quote a tested string. [3]


Example 3-17. arithmetic and string comparisons

   1 #!/bin/bash
   2 
   3 a=4
   4 b=5
   5 
   6 # Here a and b can be treated either as integers or strings.
   7 # There is some blurring between the arithmetic and string comparisons.
   8 # Be careful.
   9 
  10 if [ "$a" -ne "$b" ]
  11 then
  12   echo "$a is not equal to $b"
  13   echo "(arithmetic comparison)"
  14 fi
  15 
  16 echo
  17 
  18 if [ "$a" != "$b" ]
  19 then
  20   echo "$a is not equal to $b."
  21   echo "(string comparison)"
  22 fi
  23 
  24 echo
  25 
  26 exit 0


Example 3-18. testing whether a string is null

   1 #!/bin/bash
   2 # Testing null strings and unquoted strings.
   3 # But not strings and sealing wax, not to mention cabbages and kings...
   4 
   5 # Using   if [ ... ]
   6 
   7 
   8 
   9 # If a string has not been initialized, it has no defined value.
  10 # This state is called "null" (not the same as zero).
  11 
  12 if [ -n $string1 ]   # $string1 has not been declared or initialized.
  13 then
  14   echo "String \"string1\" is not null."
  15 else  
  16   echo "String \"string1\" is null."
  17 fi  
  18 # Wrong result.
  19 # Shows $string1 as not null, although it was not initialized.
  20 
  21 
  22 echo
  23 
  24 
  25 # Lets try it again.
  26 
  27 if [ -n "$string1" ]  # This time, $string1 is quoted.
  28 then
  29   echo "String \"string1\" is not null."
  30 else  
  31   echo "String \"string1\" is null."
  32 fi      # Quote strings within test brackets!
  33 
  34 
  35 echo
  36 
  37 
  38 if [ $string1 ]  # This time, $string1 stands naked.
  39 then
  40   echo "String \"string1\" is not null."
  41 else  
  42   echo "String \"string1\" is null."
  43 fi  
  44 # This works fine.
  45 # The [ ] test operator alone detects whether the string is null.
  46 # However it is good practice to quote it ("$string1").
  47 #
  48 # As Stephane Chazelas points out,
  49 #    if [ $string 1 ]   has one argument, "]"
  50 #    if [ "$string 1" ]  has two arguments, the empty "$string1" and "]" 
  51 
  52 
  53 
  54 echo
  55 
  56 
  57 
  58 string1=initialized
  59 
  60 if [ $string1 ]  # Again, $string1 stands naked.
  61 then
  62   echo "String \"string1\" is not null."
  63 else  
  64   echo "String \"string1\" is null."
  65 fi  
  66 # Again, gives correct result.
  67 # Still, it is better to quote it ("$string1"), because...
  68 
  69 
  70 string1="a = b"
  71 
  72 if [ $string1 ]  # Again, $string1 stands naked.
  73 then
  74   echo "String \"string1\" is not null."
  75 else  
  76   echo "String \"string1\" is null."
  77 fi  
  78 # Not quoting "$string1" now gives wrong result!
  79 
  80 exit 0
  81 
  82 # Also, thank you, Florian Wisser, for the "heads-up".


Example 3-19. zmost

   1 #!/bin/bash
   2 
   3 #View gzipped files with 'most'
   4 
   5 NOARGS=65
   6 NOTFOUND=66
   7 NOTGZIP=67
   8 
   9 if [ $# -eq 0 ] # same effect as:  if [ -z "$1" ]
  10 # $1 can be empty but present:  zmost "" arg2 arg3
  11 then
  12   echo "Usage: `basename $0` filename" >&2
  13   # Error message to stderr.
  14   exit $NOARGS
  15   # Returns 65 as exit status of script (error code).
  16 fi  
  17 
  18 filename=$1
  19 
  20 if [ ! -f "$filename" ]   # Quoting $filename allows for possible spaces.
  21 then
  22   echo "File $filename not found!" >&2
  23   # Error message to stderr.
  24   exit $NOTFOUND
  25 fi  
  26 
  27 if [ ${filename##*.} != "gz" ]
  28 # Using bracket in variable substitution.
  29 then
  30   echo "File $1 is not a gzipped file!"
  31   exit $NOTGZIP
  32 fi  
  33 
  34 zcat $1 | most
  35 
  36 # Uses the file viewer 'most' (similar to 'less').
  37 # Later versions of 'most' have file decompression capabilities.
  38 # May substitute 'more' or 'less', if desired.
  39 
  40 
  41 exit $?   # Script returns exit status of pipe.
  42 # Actually "exit $?" unnecessary, as the script will, in any case,
  43 # return the exit status of the last command executed.

compound comparison

-a

logical and

exp1 -a exp2 returns true if both exp1 and exp2 are true.

-o

logical or

exp1 -o exp2 returns true if either exp1 or exp2 are true.

These are similar to the Bash comparison operators && and ||, used within double brackets.

   1 [[ condition1 && condition2 ]]
The -o and -a operators work with the test command or occur within test brackets.
   1 if [ "$exp1" -a "$exp2" ]

Refer to Example 3-21 and Example 3-155 to see compound comparison operators in action.

3.5.3. Nested if/then Condition Tests

Condition tests using the if/then construct may be nested. The net result is identical to using the && compound comparison operator above.

   1 if [ condition1 ]
   2 then
   3   if [ condition2 ]
   4   then
   5     do-something  # But only if both "condition1" and "condition2" valid.
   6   fi  
   7 fi

See Example 3-168 for an example of nested if/then condition tests.

Notes

[1]

Be aware that suid binaries may open security holes and that the suid flag should not be set on shell scripts.

[2]

On modern UNIX systems, the sticky bit is no longer used for files, only on directories.

[3]

As S.C. points out, in a compound test, even quoting the string variable might not suffice. [ -n "$string" -o "$a" = "$b" ] may cause an error with some versions of Bash if $string is empty. The safe way is to append an extra character to possibly empty variables, [ "x$string" != x -o "x$a" = "x$b" ] (the "x's" cancel out).