Conditions

Like any other programming languages, BASH supports conditions for logical operations. Unlike POSIX, bash supports both POSIX and BASH extended format.

This guide here is using BASH extended conditions. That also means it breaks POSIX compliance. If you want to maintain portability, you should use POSIX conditions.

if else if

If else if condition, like its name implies, is a value condition operator. It performs value checking sequentially based on the conditions list and executes the code block if the condition is right. A good way to write these conditions is:

if [[ condition ]]; then
        ....
elif [[ condition ]]; then
        ....
else
        ....
fi

Example:

#!/bin/sh
OS="$(uname -s)"

if [[ "$OS" == "FreeBSD" ]]; then
        echo "This is FreeBSD"
elif [[ "$OS" == "CYGWIN_NT-5.1" ]]; then
        echo "This is Cygwin"
elif [[ "$OS" == "SunOS" ]]; then
        echo "This is Solaris"
elif [[ "$OS" == "Darwin" ]]; then
        echo "This is MacOS"
elif [[ "$OS" == "AIX" ]]; then
        echo "This is AIX"
elif [[ "$OS" == "Minix" ]]; then
        echo "This is Minix"
elif [[ "$OS" == "Linux" ]]; then
        echo "This is Linux"
else
        echo "Unknown OS"
fi


Best Practice

  1. Keep the then keyword the same line as the if or elif.
    1. Rationale: cleaner one line execution statement and reduce test tool complications.
  2. Watch for your indentation
    1. Rationale: if you need more than 3 indents (including function indentation), it signals cyclomatic complexity and you should consider re-writing your code. Heed the warning.

Case

Case is also a conditional operator. The difference between if else if and case is that case relies only on a single value or a single condition to determine the next course of action. A good style of writing is:

case "$value"; do
value1|valueOne)
        ...
        ;;
value2|valueTwo)
        ...
        ;;
*)
        ...
        ;;
done

The last case is the default when no other specific case is fulfilled. Here's an example converting from if else if.

#!/bin/sh
OS="$(uname -s)"

case "$OS" in
"FreeBSD")
        echo "This is FreeBSD"
        ;;
"CYGWIN_NT-5.1")
        echo "This is CYGWIN"
        ;;
"SunOS")
        echo "This is Solaris"
        ;;
"Darwin")
        echo "This is MacOS"
        ;;
"AIX")
        echo "This is AIX"
        ;;
"Minix")
        echo "This is Minix"
        ;;
"Linux")
        echo "This is Linux"
        ;;
*)
        echo "Unknown OS"
        ;;
done


Best Practice

  1. Keep the ;; break case keyword as one line. It itself is an instruction.
    1. Rationale: cleaner one line execution statement and reduce test tool complications.
  2. Watch for your indentation
    1. Rationale: if you need more than 3 indents (including function indentation), it signals cyclomatic complexity and you should consider re-writing your code. Heed the warning.

New Test ([[)

BASH allows the use of new test, which is a double bracket ([[). This test is BASH specific feature, operates in a much familiar syntaxes. The idea is that everything is compared via strings. We will review each expression closely.


Filesystem Flags

Since everything in Unix is a file, there is definitely a set of filesystem flags. You can use these flags to check for file, directory, block device, character device, missing file, status, permission, group, etc.

  • [[ -b "/path/to/file" ]] - check /path/to/file is a block device. False if no.
  • [[ -c "/path/to/file" ]] - check /path/to/file is a character file. False is no.
  • [[ -d "/path/to/directory" ]] - check /path/to/directory is a directory. False is no.
  • [[ -e "/path/to/object" ]] - check /path/to/object exists. False is no.
  • [[ -f "/path/to/file" ]] - check /path/to/file is a file. False is no.
  • [[ -g "/path/to/file" ]] - check /path/to/file has Group-ID set. False is no or missing file.
  • [[ -h "/path/to/file" ]], [[ -L "/path/to/file" ]] - check /path/to/file is a symbolic link. Failed is no or missing file. If the final file is a symbolic link. It will not follow.
  • [[ -p "/path/to/file" ]] - check /path/to/file is a FIFO pipe. False is no or missing file.
  • [[ -S "/path/to/file" ]] - check /path/to/file is a socket. False is no or missing file.


File Flags

  • [[ -s "/path/to/file" ]] - check /path/to/file contain byte data. False is 0 byte or missing file.
  • [[ -r "/path/to/file" ]] - check /path/to/file has read permission. False means no read permission or missing file.
  • [[ -w "/path/to/file" ]] - check /path/to/file has write permission. False means no write permission or missing file.
  • [[ -x "/path/to/file" ]] - check /path/to/file has execute permission. False means no execute permission or missing file.
  • [[ -u "/path/to/file" ]] - check /path/to/file has user-ID description. False means no user-ID description is set or missing file.
  • [[ -t "/path/to/file" ]] - check /path/to/file has file descriptor. False means no or missing file.


String Operations

  • [[ "one" == "one" ]] - check strings are the same. False otherwise.
  • [[ "one" != "two" ]] - check strings are not the same. False otherwise.


Arithmetic Operations

  • [[ 1 -eq 1 ]] - check both integers are equal. False otherwise.
  • [[ 1 -ne 2 ]] - check both integers are not equal. False otherwise.
  • [[ 1 -gt 0 ]] - check the former integer is greater than the latter. False otherwise.
  • [[ 1 -ge 0 ]] - check the former integer is greater than or equal to the latter. False otherwise.
  • [[ 0 -lt 1 ]] - check the former integer is less than the latter. False otherwise.
  • [[ 0 -le 1 ]] - check the former integer is less than or equal to the latter. False otherwise.


Regular Expression

  • [[ "string" =~ regex ]] - check string for the regular expression matches. False is no.
  • [[ "string" == *pattern* ]] - check string matches the pattern. False is no.

NOTE:

both regex and *pattern* must not be quoted as a single string. Example:

re='some RE'
if [[ $foo =~ $re ]]

[[ $foo = "*.glob" ]]      # Wrong! *.glob is treated as a literal string.
[[ $foo = *.glob ]]        # Correct. *.glob is treated as a glob-style pattern.

Multiple Expressions

This is when a condition requires multiple expressions (all listed above), we usually use AND or OR cases. Example: if expression1 is true AND expression2 is true, then do this. Here are some practices:

AND logic ( && )

  • [[ expression1 && expression2 ]] - make logical checking for both expression1 and expression2 using AND logic. if either is false, the overall result is false.

OR logic ( || )

  • [[ expression1 || expression2 ]] - make logical checking for both expression1 and expression2 using OR logic. if either is true, the overall result is true.

Compare First ( (...) )

  • [[ expression3 ] && (expression1 || expression2) ]] - make logical checking inside ( ... ) to run first before any other comparisons. In this case, expression1 and expression2 OR logic gets evaluated first and produced the first result. Then, using that result, expression3 gets evaluated against with using AND logic.


Examples

#!/bin/sh
if [[ ("one" != "two") && ( (1 -gt 0) || (2 -lt 0) ) ]]; then
        ...
fi


Breaking Conditions into Multiple Lines

In some situations, you might encounter a need to break the conditions into multiple lines. There are various way to do it with forward slash. However, as a good practice, you should break it after the expression.

Rationale:

  • Leave a clear signal for line continuation reason.

Here is an example:

if [[ ("$very_long_variable_name" != "super long string that doesn't fit") && \
        ("$very_long_variable_name2" != "super long string that doesn't fit") || \
        ("$very_long_variable_name" != "super long string that doesn't fit") ]]; then
        ...
fi

That's all about the BASH conditions. Feel free to proceed to the new section.