| Advanced Bash-Scripting Guide: A complete guide to shell scripting, using Bash | ||
|---|---|---|
| Prev | Chapter 3. Tutorial / Reference | Next |
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.
![]() | 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 |
![]() | Using the newer [[ ... ]] test construct, rather than [ ... ] can prevent many logic errors in scripts. |
![]() | Following an if, neither the test command nor the test brackets ( [ ] or [[ ]] ) are strictly necessary.
Similarly, a condition within test brackets may stand alone without an if, when used in combination with a list construct.
|
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
- -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.
A file with the suid flag set shows an s in its permissions.-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd
- -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.
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.drwxrwxrwt 7 root 1024 May 19 21:26 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).
Returns true if...
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)
- -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" ]
- =
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".

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. |
- -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.
compound comparison
These are similar to the Bash comparison operators && and ||, used within double brackets.
1 [[ condition1 && condition2 ]] |
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). |


