Bash Test Expressions

Test expressions, conditionals, and comparison operators.

File Tests

# Existence and type
[[ -e "$file" ]]                             # Exists (any type)
[[ -f "$file" ]]                             # Regular file
[[ -d "$dir" ]]                              # Directory
[[ -L "$link" ]]                             # Symbolic link
[[ -h "$link" ]]                             # Same as -L
[[ -p "$pipe" ]]                             # Named pipe (FIFO)
[[ -S "$sock" ]]                             # Socket
[[ -b "$dev" ]]                              # Block device
[[ -c "$dev" ]]                              # Character device

# Permissions
[[ -r "$file" ]]                             # Readable
[[ -w "$file" ]]                             # Writable
[[ -x "$file" ]]                             # Executable
[[ -u "$file" ]]                             # SUID bit set
[[ -g "$file" ]]                             # SGID bit set
[[ -k "$file" ]]                             # Sticky bit set

# Size and content
[[ -s "$file" ]]                             # Size > 0 (not empty)
[[ ! -s "$file" ]]                           # Empty file

# Ownership
[[ -O "$file" ]]                             # Owned by current user
[[ -G "$file" ]]                             # Group matches current user

# Terminal
[[ -t 0 ]]                                   # stdin is terminal
[[ -t 1 ]]                                   # stdout is terminal
[[ -t 2 ]]                                   # stderr is terminal

# Comparison
[[ "$file1" -nt "$file2" ]]                  # file1 newer than file2
[[ "$file1" -ot "$file2" ]]                  # file1 older than file2
[[ "$file1" -ef "$file2" ]]                  # Same inode (hard links)

String Tests

# Empty/non-empty
[[ -z "$var" ]]                              # Zero length (empty)
[[ -n "$var" ]]                              # Non-zero length (not empty)
[[ "$var" ]]                                 # Same as -n (implicit)

# Comparison
[[ "$a" == "$b" ]]                           # Equal
[[ "$a" != "$b" ]]                           # Not equal
[[ "$a" < "$b" ]]                            # Less than (lexicographic)
[[ "$a" > "$b" ]]                            # Greater than (lexicographic)

# Pattern matching (glob)
[[ "$str" == *.txt ]]                        # Ends with .txt
[[ "$str" == vault-* ]]                      # Starts with vault-
[[ "$str" == *pattern* ]]                    # Contains pattern
[[ "$str" != *error* ]]                      # Does not contain

# Regex matching
[[ "$str" =~ ^[0-9]+$ ]]                     # All digits
[[ "$str" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]     # Valid identifier
[[ "$email" =~ ^[^@]+@[^@]+\.[^@]+$ ]]       # Basic email format
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] # IP address format

# Regex with capture groups
if [[ "$str" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
    major="${BASH_REMATCH[1]}"
    minor="${BASH_REMATCH[2]}"
    patch="${BASH_REMATCH[3]}"
fi

# Case-insensitive comparison
shopt -s nocasematch
[[ "$answer" == "yes" ]]                     # Matches YES, Yes, yes, etc.
shopt -u nocasematch

# Substring
[[ "${str:0:5}" == "hello" ]]                # First 5 chars are "hello"

Numeric Tests

# Integer comparison (use (( )) or [ ] with flags)
[[ "$a" -eq "$b" ]]                          # Equal
[[ "$a" -ne "$b" ]]                          # Not equal
[[ "$a" -lt "$b" ]]                          # Less than
[[ "$a" -le "$b" ]]                          # Less than or equal
[[ "$a" -gt "$b" ]]                          # Greater than
[[ "$a" -ge "$b" ]]                          # Greater than or equal

# Arithmetic context (preferred for numbers)
(( a == b ))                                 # Equal
(( a != b ))                                 # Not equal
(( a < b ))                                  # Less than
(( a <= b ))                                 # Less or equal
(( a > b ))                                  # Greater than
(( a >= b ))                                 # Greater or equal

# Arithmetic with expressions
(( (a + b) > c ))
(( a * 2 == b ))
(( a % 2 == 0 ))                             # Even number
(( a & 1 ))                                  # Odd number (bitwise AND)

# Range check
(( 0 <= x && x <= 100 ))                     # x in range [0, 100]

# Floating point (use bc or awk)
result=$(echo "$a > $b" | bc -l)
[[ "$result" -eq 1 ]]

# Or with awk
if awk "BEGIN {exit !($a > $b)}"; then
    echo "$a is greater than $b"
fi

Logical Operators

# AND
[[ -f "$file" && -r "$file" ]]               # File exists AND readable
[[ -f "$file" ]] && [[ -r "$file" ]]         # Same, separate tests

# OR
[[ -f "$file" || -d "$file" ]]               # File OR directory
[[ -f "$file" ]] || [[ -d "$file" ]]         # Same, separate tests

# NOT
[[ ! -f "$file" ]]                           # Does NOT exist as file
! [[ -f "$file" ]]                           # Same

# Grouping
[[ ( -f "$file" && -r "$file" ) || -d "$dir" ]]

# Complex conditions
[[ -f "$config" && -r "$config" && -s "$config" ]]  # File, readable, non-empty

# Short-circuit evaluation
[[ -n "$var" && "${var:0:1}" == "/" ]]       # Safe: second part only runs if var non-empty

# Command success
command && echo "Success" || echo "Failed"

# Chain commands
mkdir -p "$dir" && cd "$dir" && touch file

# Multiple conditions with case
is_valid_host() {
    local host="$1"
    [[ -n "$host" ]] || return 1
    [[ "$host" =~ ^[a-zA-Z0-9.-]+$ ]] || return 1
    host "$host" &>/dev/null || return 1
    return 0
}

Variable Tests

# Variable set/unset
[[ -v var ]]                                 # Variable is set (even if empty)
[[ ! -v var ]]                               # Variable is unset

# Set vs empty
[[ -n "${var+x}" ]]                          # Set (even if empty)
[[ -z "${var+x}" ]]                          # Unset
[[ -n "${var-x}" ]]                          # Set and non-empty
[[ -z "${var-}" ]]                           # Unset or empty

# Default value patterns
"${var:-default}"                            # Use default if unset/empty
"${var:=default}"                            # Set and use default if unset/empty
"${var:+alternate}"                          # Use alternate if set and non-empty
"${var:?error message}"                      # Error if unset/empty

# Array checks
[[ -v arr[@] ]]                              # Array is declared
[[ ${#arr[@]} -gt 0 ]]                       # Array has elements
[[ -v arr[0] ]]                              # Index 0 exists

# Associative array key exists
declare -A map
[[ -v map[key] ]]                            # Key exists

# Function exists
type -t myfunc &>/dev/null && echo "Function exists"
declare -f myfunc &>/dev/null && echo "Function exists"

# Command exists
command -v docker &>/dev/null && echo "Docker installed"
type -P kubectl &>/dev/null && echo "kubectl in PATH"

Exit Code Tests

# Check last command
command
if [[ $? -eq 0 ]]; then
    echo "Success"
else
    echo "Failed with code $?"
fi

# Inline check (preferred)
if command; then
    echo "Success"
fi

# Negated
if ! command; then
    echo "Failed"
fi

# Check specific exit codes
command
case $? in
    0) echo "Success" ;;
    1) echo "General error" ;;
    2) echo "Misuse of command" ;;
    126) echo "Permission denied" ;;
    127) echo "Command not found" ;;
    130) echo "Interrupted (Ctrl+C)" ;;
    *) echo "Other error: $?" ;;
esac

# Pipeline exit codes
set -o pipefail
cmd1 | cmd2 | cmd3
echo "Pipeline exit: $?"                     # First non-zero, or 0

# Individual pipeline exit codes
cmd1 | cmd2 | cmd3
echo "Exit codes: ${PIPESTATUS[@]}"          # All exit codes

# Check if command exists before running
if command -v docker &>/dev/null; then
    docker ps
else
    echo "Docker not installed"
    exit 1
fi

Infrastructure Testing Patterns

# Check host reachability
is_host_up() {
    ping -c1 -W2 "$1" &>/dev/null
}

# Check port open
is_port_open() {
    local host="$1" port="$2"
    timeout 2 bash -c "cat < /dev/null > /dev/tcp/$host/$port" 2>/dev/null
}
# Or with nc
is_port_open() {
    nc -z -w2 "$1" "$2" 2>/dev/null
}

# Check HTTP endpoint
is_endpoint_healthy() {
    local url="$1"
    local expected="${2:-200}"
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
    [[ "$code" == "$expected" ]]
}

# Check Vault status
is_vault_ready() {
    local status
    status=$(curl -s -o /dev/null -w "%{http_code}" "https://vault-01:8200/v1/sys/health")
    [[ "$status" =~ ^(200|429|472|473)$ ]]
}

# Check k8s pod ready
is_pod_ready() {
    local pod="$1" namespace="${2:-default}"
    kubectl get pod "$pod" -n "$namespace" -o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null | grep -q true
}

# Check certificate validity
is_cert_valid() {
    local cert="$1" days="${2:-7}"
    openssl x509 -in "$cert" -noout -checkend $((days * 86400)) &>/dev/null
}

# Full infrastructure health check
check_infrastructure() {
    local failures=0

    echo "Checking Vault..."
    is_host_up vault-01 && is_port_open vault-01 8200 || ((failures++))

    echo "Checking ISE..."
    is_host_up ise-01 && is_endpoint_healthy "https://ise-01/admin/" || ((failures++))

    echo "Checking DNS..."
    is_host_up bind-01 && is_port_open bind-01 53 || ((failures++))

    return $failures
}

Test Commands: [ ] vs [[ ]] vs

# [ ] - POSIX sh compatible (older, more portable)
[ -f "$file" ]                               # Must quote variables!
[ "$a" = "$b" ]                              # String comparison
[ "$a" -eq "$b" ]                            # Numeric comparison

# [[ ]] - Bash enhanced (preferred)
[[ -f $file ]]                               # Quoting optional (safer)
[[ $a == $b ]]                               # Pattern matching with ==
[[ $a == pattern* ]]                         # Glob works without quotes
[[ $a =~ regex ]]                            # Regex support
[[ -n $var && -f $file ]]                    # && and || work inside

# (( )) - Arithmetic context
(( a == b ))                                 # No $ needed for variables
(( a + b > c ))                              # Math expressions
(( count++ ))                                # Increment
(( x = y * 2 ))                              # Assignment

# When to use which:
# - [ ] : Shell scripts needing POSIX compatibility
# - [[ ]] : Bash scripts, string/file tests (PREFERRED)
# - (( )) : Numeric comparisons and arithmetic

# Examples of [[ ]] advantages
var="hello world"
[[ $var == "hello world" ]]                  # Works without quoting
[ $var == "hello world" ]                    # ERROR: too many arguments!

# Pattern matching only in [[ ]]
[[ $str == *.txt ]]                          # Works
[ $str == *.txt ]                            # Literal comparison!

# Regex only in [[ ]]
[[ $str =~ ^[0-9]+$ ]]                       # Works
# No equivalent in [ ]

Test Gotchas

# WRONG: Unquoted variable in [ ]
var="hello world"
[ -n $var ]                                  # Error: too many arguments

# CORRECT: Always quote in [ ]
[ -n "$var" ]

# In [[ ]], quoting is optional but still good practice
[[ -n $var ]]                                # Works
[[ -n "$var" ]]                              # Also works, more explicit

# WRONG: = vs == in [ ]
[ "$a" == "$b" ]                             # Works in bash, not POSIX!

# CORRECT: Use single = in [ ]
[ "$a" = "$b" ]

# WRONG: Using && inside [ ]
[ -f "$file" && -r "$file" ]                 # Syntax error!

# CORRECT: Use -a or separate tests
[ -f "$file" -a -r "$file" ]                 # Old style
[ -f "$file" ] && [ -r "$file" ]             # Better
[[ -f "$file" && -r "$file" ]]               # Best (bash)

# WRONG: Comparing numbers with == in [[ ]]
[[ 10 == 9 ]]                                # String comparison! "10" != "9"
[[ 10 > 9 ]]                                 # Also string! "10" < "9" lexically!

# CORRECT: Use (( )) for numbers
(( 10 == 9 ))                                # false
(( 10 > 9 ))                                 # true

# WRONG: Empty string in arithmetic
(( $empty_var + 1 ))                         # Error if unset

# CORRECT: Default to 0
(( ${empty_var:-0} + 1 ))

# WRONG: Regex stored in variable with quotes
pattern="^[0-9]+$"
[[ "$str" =~ "$pattern" ]]                   # Treats as literal string!

# CORRECT: No quotes around regex variable
[[ "$str" =~ $pattern ]]                     # Works as regex

# WRONG: File test on variable that might be empty
[[ -f $file ]]                               # If $file empty, tests current dir!

# CORRECT: Check variable first
[[ -n "$file" && -f "$file" ]]