Making decisions in Bash

This is the eleventh episode in the Bash Tips sub-series. It is the third of a group of shows about making decisions in Bash.

In the last two episodes we saw the types of test Bash provides, and we looked briefly at some of the commands that use these tests. Now we want to start examining the expressions that can be used in these tests, and how to combine them. We will also start looking at string comparisons in extended tests.

Bash Conditional Expressions

This section is based very closely on the section of the GNU Bash Manual of the same name (and the Bash manpage). The list below is essentially the same except that the explanations are a little longer where it seemed necessary to add more detail.

Conditional expressions are used by the '[[' and ']]'extended test operators and the test and '[' and ']' builtin commands (see part 1, episode 2639).

Expressions may be unary or binary. Unary operators take a single argument to the right, whereas binary operators take two arguments to the left and right. Unary expressions are often used to examine the status of a file. There are string operators and numeric comparison operators as well.

When used with '[[', the '<' and '>' operators sort lexicographically using the current locale. The test command uses ASCII ordering.

Unless otherwise specified, primaries that operate on files follow symbolic links and operate on the target of the link, rather than the link itself.

-afile

True if file exists. This is identical in effect to -e. It has been “deprecated,” and its use is discouraged.

-bfile

True if file exists and is a block special file. (A block device reads and/or writes data in chunks, or blocks, in contrast to a character device, which acesses data in character units. Examples of block devices are hard drives, CDROM drives, and flash drives. Examples of character devices are keyboards, modems, sound cards.)

-cfile

True if file exists and is a character special file. (See the -b description for an explanation)

-dfile

True if file exists and is a directory.

-efile

True if file exists.

-ffile

True if file exists and is a regular file. (Not a directory or any of the other special files)

-gfile

True if file exists and its set-group-id bit is set. (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)

-hfile

True if file exists and is a symbolic link.

-kfile

True if file exists and its “sticky” bit is 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. However, note that on Linux systems, the sticky bit is no longer used for files, only on directories)

-pfile

True if file exists and is a named pipe (FIFO).

-rfile

True if file exists and is readable.

-sfile

True if file exists and has a size greater than zero.

-tfd

True if file descriptor fd is open and refers to a terminal. (This test option may be used to check whether the stdin[ -t 0 ] or stdout[ -t 1 ] in a given script is a terminal)

-ufile

True if file exists and its set-user-id bit is set. (A binary owned by root with set-user-id flag set runs with root privileges, even when an ordinary user invokes it. A file with the suid flag set shows an s in its permissions)

-wfile

True if file exists and is writable.

-xfile

True if file exists and is executable.

-Gfile

True if file exists and is owned by the effective group id.

-Lfile

True if file exists and is a symbolic link.

-Nfile

True if file exists and has been modified since it was last read.

-Ofile

True if file exists and is owned by the effective user id.

-Sfile

True if file exists and is a socket.

file1-effile2

True if file1 and file2 refer to the same device and inode numbers. (Files file1 and file2 are hard links to the same file)

file1-ntfile2

True if file1 is newer (according to modification date) than file2, or if file1 exists and file2 does not.

file1-otfile2

True if file1 is older than file2, or if file2 exists and file1 does not.

-ooptname

True if the shell option optname is enabled. The list of options appears in the description of the -o option to the set builtin (see The Set Builtin).

-vvarname

True if the shell variable varname is set (has been assigned a value).

-Rvarname

True if the shell variable varname is set and is a name reference.

-zstring

True if the length of string is zero.

-nstringorstring

True if the length of string is non-zero.

string1==string2orstring1=string2

True if the strings are equal. When used with the [[ command, this performs pattern matching as described below
The '=' operator should be used with the test command for POSIX conformance.

string1!=string2

True if the strings are not equal.

string1<string2

True if string1 sorts before string2 lexicographically.

string1>string2

True if string1 sorts after string2 lexicographically.

arg1OParg2

OP is one of -eq, -ne, -lt, -le, -gt, or -ge. These arithmetic binary operators return true if arg1 is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to arg2, respectively. Arg1 and arg2 may be positive or negative integers.

Combining expressions

Operators used with test and '[...]'

! expr

True if expr is false.

( expr )

Returns the value of expr. This may be used to override the normal precedence of operators.

expr1-aexpr2

True if both expr1 and expr2 are true.

expr1-oexpr2

True if either expr1 or expr2 is true.

Operators used with '[[...]]'

expr1&&expr2

True if both expr1 and expr2 are true. Differs from -a in that if expr1 returns False then expr2 is never invoked. This operator short circuits.

expr1||expr2

True if either expr1 or expr2 is true. Differs from -o in that if expr1 returns True then expr2 is never invoked. This operator short circuits.

Conditional expression examples

Example 1

This is a typical use of the -e operator to test for the existence of a file that has been passed through as an argument, or is an expected constant name. It is good to make the script exit at this point and to do so with a failure result of 1. This way the caller, which may be a script, can take error action as well.

This can also be written as a command list as mentioned in the previous episode:

[ -e "$file" ] || { echo "File $file not found; aborting"; exit 1; }

Note that this time we test for existence of the file and if the -e operator returns False the next command will be executed. This is a compound command in curly braces, and as discussed in the previous episode the two commands in it must end with semicolons inside the braces. See the appropriate section of the GNU Bash Manual for further details.

Example 2

$ cat bash11_ex1.sh
#!/bin/bash
#
# A directory we want to create if it doesn't exist
#
BASEDIR="/tmp/testdir"
#
# Check for the existence of the directory and create it if not found
#
if [[ ! -d "$BASEDIR" ]]; then
# Create directory and take action on failure
mkdir "$BASEDIR" || { echo "Failed to create $BASEDIR"; exit 1; }
echo "Created $BASEDIR"
fi
$ ./bash11_ex1.sh
Created /tmp/testdir

This might be a way to determine if a particular directory exists, and if not, create it. Note how the mkdir command is part of a command list using a logical OR. If this command fails the following command is executed. As in Example 1 this is a compound command in curly braces, containing two individual commands, echo and exit and between them they will produce an error message and exit the script.

Example 3

Finding the length of a string is often something a script needs to do. The string may be the output from a command, or input from the script user for example. One way to check if the string is empty is:

String comparisons

Because the string comparisons mentioned above are more complex (and more powerful) than other expressions, we will look at them in more detail. There also exists a binary operator which performs a regular expression match as a kind of string matching not listed in the above section. We will look at this subject in the next episode.

Pattern matching

When comparing strings with test and '[...]' (using == or the POSIX-compliant =, and !=) the two strings being compared are treated as plain strings. However, when using the extended test operators '[[...]]' it is possible to compare the left-hand argument with a pattern as the right-hand argument. See the appropriate section of the GNU Bash Manual for full details.

The pattern and pattern comparison were discussed in episodes 2278 and 2293 in the context of pathname expansion. However, in this case of string comparison the pattern is treated as if the extglob option were enabled. It is also possible to enable another shopt option called 'nocasematch' to make the pattern case-insensitive.

It is possible to perform some quite sophisticated pattern matching this way, but the pattern must not be quoted. Doing so makes it a simple string in which the pattern features are not available. According to the documentation it is possible to quote part of a pattern however, if it is desired to treat pattern metacharacters as simple characters for example1.

This example uses the @(pattern-list) form to match any one of a list of patterns where each subsidiary pattern is separated by '|' characters (alternative patterns). By this means the pattern will match any of the strings "pig", "dog" or "cat" but nothing else, not even a blank string.

This example is available as a downloadable file (bash11_ex4.sh) which has been run in the demonstration above.