Further ancillary Bash tips - 11 (HPR Show 2659)

Dave Morriss


Table of Contents

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.

-a file
True if file exists. This is identical in effect to -e. It has been “deprecated,” and its use is discouraged.
-b file
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.)
-c file
True if file exists and is a character special file. (See the -b description for an explanation)
-d file
True if file exists and is a directory.
-e file
True if file exists.
-f file
True if file exists and is a regular file. (Not a directory or any of the other special files)
-g file
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)
-h file
True if file exists and is a symbolic link.
-k file
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)
-p file
True if file exists and is a named pipe (FIFO).
-r file
True if file exists and is readable.
-s file
True if file exists and has a size greater than zero.
-t fd
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)
-u file
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)
-w file
True if file exists and is writable.
-x file
True if file exists and is executable.
-G file
True if file exists and is owned by the effective group id.
-L file
True if file exists and is a symbolic link.
-N file
True if file exists and has been modified since it was last read.
-O file
True if file exists and is owned by the effective user id.
-S file
True if file exists and is a socket.
file1 -ef file2
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 -nt file2
True if file1 is newer (according to modification date) than file2, or if file1 exists and file2 does not.
file1 -ot file2
True if file1 is older than file2, or if file2 exists and file1 does not.
-o optname
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).
-v varname
True if the shell variable varname is set (has been assigned a value).
-R varname
True if the shell variable varname is set and is a name reference.
-z string
True if the length of string is zero.
-n string  or  string
True if the length of string is non-zero.
string1 == string2  or  string1 = 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.
arg1 OP arg2
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 -a expr2
True if both expr1 and expr2 are true.
expr1 -o expr2
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

if [[ ! -e "$file" ]]; then
    echo "File $file not found; aborting"
    exit 1
fi

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.

This example is available as a downloadable file (bash11_ex1.sh).

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:

if [[ ${#reply} -eq 0 ]]; then
    echo "Please provide a non-empty reply"
fi

A better way is to use the -z operator:

if [[ -z $reply ]]; then
    echo "Please provide a non-empty reply"
fi

The following script demonstrates these two alternatives:

$ cat bash11_ex2.sh
#!/bin/bash

#
# Read a reply from the user, then check it's not zero length
#
read -r -p "Please enter a string: " reply
if [[ ${#reply} -eq 0 ]]; then
    echo "Please provide a non-empty reply"
else
    echo "You said: $reply"
fi

#
# Read a reply from the user, then check it's not zero length
#
read -r -p "Please enter a string: " reply
if [[ -z $reply ]]; then
    echo "Please provide a non-empty reply"
else
    echo "You said: $reply"
fi
$ ./bash11_ex2.sh
Please enter a string: OK
You said: OK
Please enter a string: 
Please provide a non-empty reply

This example is available as a downloadable file (bash11_ex2.sh).

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.

Pattern matching examples

Example 4

animal="grizzly bear"
if [[ $animal == *bear ]]; then
    echo "Detected a type of bear: $animal"
fi

In this example the test is for any string which ends with 'bear'. It also matches 'bear' with no earlier string.

Example 5

str="Further ancillary Bash tips - 11"
if [[ $str == +([[:alnum:] -]) ]]; then
    echo "Matched"
fi

Here we try to match the title of this show with a pattern. The pattern consists of:

  • a POSIX character class which matches any alphabetic or numeric character - [:alnum:] (allowed only inside a character range expression)
  • a range expression which also includes a space and a hyphen as well as the character class
  • a sub-pattern (normally only allowed when extglob is enabled) which specifies one or more occurrences of the given pattern - +(pattern-list)

The result is a pattern which matches one or more alphanumeric characters, space and hyphen. This matches the show title.

This example is available as a downloadable file: bash11_ex3.sh

Example 6

$ cat bash11_ex4.sh
#!/bin/bash

#
# String comparison with a pattern, using one of a list of patterns
#
for str in 'dog' 'pig' 'rat' 'cat' ''; do
    if [[ $str == @(pig|dog|cat) ]]; then
        echo "Matched '$str'"
    else
        echo "Didn't match '$str'"
    fi
done
$ ./bash11_ex4.sh
Matched 'dog'
Matched 'pig'
Didn't match 'rat'
Matched 'cat'
Didn't match ''

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.

Example 7

$ cat bash11_ex5.sh
#!/bin/bash

#
# String comparison with a pattern in a variable. The pattern matches any
# word that ends with 'man' that is longer than 3 letters.
#
pattern="+([[:word:]])man"
echo "Pattern is: $pattern"
for str in 'man' 'woman' 'German' 'Xman' 'romance' ''; do
    if [[ $str == $pattern ]]; then
        echo "Matched '$str'"
    else
        echo "Didn't match '$str'"
    fi
done
$ ./bash11_ex5.sh
Pattern is: +([[:word:]])man
Didn't match 'man'
Matched 'woman'
Matched 'German'
Matched 'Xman'
Didn't match 'romance'
Didn't match ''

This example matches any word that ends with 'man' with letters before 'man'. The expression '+([[:word:]])' specifies one or more characters that that match letters, numbers and the underscore

As an aside, I use the shellcheck tool inside vim. It checks that any scripts I type are valid, and flags any issues. It has a problem with:

if [[ $str == $pattern ]]; then

It tells me that I should quote $pattern because otherwise it might be subject to “glob matching”. Since that is precisely what I’m trying to do, this is mildly amusing.

In ./bash11_ex5.sh line 10:
    if [[ $str == $pattern ]]; then
                  ^-- SC2053: Quote the rhs of == in [[ ]] to prevent glob matching.

This specific error can be turned off if desired.

The example is available as a downloadable file (bash11_ex5.sh).


  1. At the time of writing I have not been able to get this to work and have not found any detailed documentation about how it is meant to work.