Bash Test Operators Complete Reference

Complete reference for bash test operators ([ ], [[ ]], and test). Essential for file validation, conditional logic, and script robustness.


Quick Reference Card

# ═══════════════════════════════════════════════════════════════════
# FILE TEST OPERATORS - Complete Reference
# ═══════════════════════════════════════════════════════════════════
# EXISTENCE & TYPE
[ -e FILE ]   # File exists (any type)
[ -f FILE ]   # Regular file exists
[ -d FILE ]   # Directory exists
[ -L FILE ]   # Symbolic link exists
[ -h FILE ]   # Symbolic link exists (same as -L)
[ -b FILE ]   # Block device exists
[ -c FILE ]   # Character device exists
[ -p FILE ]   # Named pipe (FIFO) exists
[ -S FILE ]   # Socket exists
# 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
# OWNERSHIP
[ -O FILE ]   # Owned by effective UID
[ -G FILE ]   # Owned by effective GID
# FILE STATE
[ -s FILE ]   # File exists and size > 0 (not empty)
[ -t FD ]     # File descriptor FD is a terminal
[ -N FILE ]   # File modified since last read
# FILE COMPARISONS
[ FILE1 -nt FILE2 ]  # FILE1 newer than FILE2
[ FILE1 -ot FILE2 ]  # FILE1 older than FILE2
[ FILE1 -ef FILE2 ]  # Same device/inode (hard links)
# ═══════════════════════════════════════════════════════════════════
# STRING TEST OPERATORS
# ═══════════════════════════════════════════════════════════════════
[ -z STRING ]        # String is empty (zero length)
[ -n STRING ]        # String is not empty (non-zero length)
[ STRING ]           # String is not empty (same as -n)
[ STR1 = STR2 ]      # Strings are equal
[ STR1 == STR2 ]     # Strings are equal (same as =)
[ STR1 != STR2 ]     # Strings are not equal
[ STR1 < STR2 ]      # STR1 sorts before STR2 (lexicographic)
[ STR1 > STR2 ]      # STR1 sorts after STR2 (lexicographic)
# [[ ]] only (pattern matching)
[[ STR =~ REGEX ]]   # String matches regex
[[ STR = PATTERN ]]  # String matches glob pattern
# ═══════════════════════════════════════════════════════════════════
# NUMERIC TEST OPERATORS
# ═══════════════════════════════════════════════════════════════════
[ NUM1 -eq NUM2 ]    # Equal
[ NUM1 -ne NUM2 ]    # Not equal
[ NUM1 -lt NUM2 ]    # Less than
[ NUM1 -le NUM2 ]    # Less than or equal
[ NUM1 -gt NUM2 ]    # Greater than
[ NUM1 -ge NUM2 ]    # Greater than or equal
# ═══════════════════════════════════════════════════════════════════
# LOGICAL OPERATORS
# ═══════════════════════════════════════════════════════════════════
[ ! EXPR ]           # Logical NOT
[ EXPR1 -a EXPR2 ]   # Logical AND (avoid, use && instead)
[ EXPR1 -o EXPR2 ]   # Logical OR (avoid, use || instead)
# PREFERRED: Chain with && and ||
[ EXPR1 ] && [ EXPR2 ]   # Logical AND
[ EXPR1 ] || [ EXPR2 ]   # Logical OR
# [[ ]] only (recommended)
[[ EXPR1 && EXPR2 ]]     # Logical AND
[[ EXPR1 || EXPR2 ]]     # Logical OR
[[ ! EXPR ]]             # Logical NOT

Real-World Examples

1. Encrypted File Validation (Secrets Vault)

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# VALIDATE ENCRYPTED DOCUMENTS IN SECRETS VAULT
# ═══════════════════════════════════════════════════════════════════

VAULT_ROOT="$HOME/.secrets/documents"
# Check if encrypted deployment guide exists
check_deployment_guide() {
    local doc="${VAULT_ROOT}/professional/infrastructure/DOMUS_IoT_deployment.adoc.age"

    [ -f "$doc" ] && echo "✅ Deployment guide exists" || echo "❌ Deployment guide missing"
}
# Check if file exists AND is readable AND not empty
validate_encrypted_file() {
    local file="$1"

    if [ -f "$file" ] && [ -r "$file" ] && [ -s "$file" ]; then
        echo "✅ Valid encrypted file: $file"
        return 0
    else
        echo "❌ Invalid or missing: $file"
        return 1
    fi
}
# Check multiple conditions
check_vault_integrity() {
    local doc="$VAULT_ROOT/professional/infrastructure/DOMUS_IoT_deployment.adoc.age"

    # All conditions must be true
    if [ -f "$doc" ] && [ -r "$doc" ] && [ -s "$doc" ]; then
        file_size=$(stat -f%z "$doc" 2>/dev/null || stat -c%s "$doc")
        echo "✅ File valid: $doc ($file_size bytes)"
    else
        [ ! -e "$doc" ] && echo "❌ File does not exist: $doc"
        [ -e "$doc" ] && [ ! -f "$doc" ] && echo "❌ Not a regular file: $doc"
        [ -f "$doc" ] && [ ! -r "$doc" ] && echo "❌ Not readable: $doc"
        [ -f "$doc" ] && [ ! -s "$doc" ] && echo "❌ Empty file: $doc"
        return 1
    fi
}
# Usage
check_deployment_guide
validate_encrypted_file "$VAULT_ROOT/professional/infrastructure/DOMUS_IoT_deployment.adoc.age"
check_vault_integrity

2. One-Liner File Existence Check

# Basic existence check
[ -f ~/.secrets/documents/professional/infrastructure/DOMUS_IoT_deployment.adoc.age ] && \
    echo "exists" || echo "does not exist"
# With full validation
[ -f ~/.secrets/documents/professional/infrastructure/DOMUS_IoT_deployment.adoc.age ] && \
    [ -r ~/.secrets/documents/professional/infrastructure/DOMUS_IoT_deployment.adoc.age ] && \
    [ -s ~/.secrets/documents/professional/infrastructure/DOMUS_IoT_deployment.adoc.age ] && \
    echo "✅ Valid encrypted document" || \
    echo "❌ Missing or invalid"
# Shorter variable version
DOC=~/.secrets/documents/professional/infrastructure/DOMUS_IoT_deployment.adoc.age
[ -f "$DOC" ] && [ -r "$DOC" ] && [ -s "$DOC" ] && echo "✅ Valid" || echo "❌ Invalid"

3. Age Encrypted File Workflow

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# AGE ENCRYPTED FILE OPERATIONS WITH VALIDATION
# ═══════════════════════════════════════════════════════════════════

AGE_KEY="$HOME/.secrets/.metadata/keys/master.age.key"
DOC_VAULT="$HOME/.secrets/documents"
# View encrypted document
view_encrypted() {
    local encrypted_file="$1"

    # Validate encrypted file
    if [ ! -f "$encrypted_file" ]; then
        echo "❌ File not found: $encrypted_file"
        return 1
    fi
    if [ ! -r "$encrypted_file" ]; then
        echo "❌ File not readable: $encrypted_file"
        return 1
    fi
    if [ ! -s "$encrypted_file" ]; then
        echo "❌ File is empty: $encrypted_file"
        return 1
    fi
    # Validate age key
    if [ ! -f "$AGE_KEY" ]; then
        echo "❌ Age key not found: $AGE_KEY"
        return 1
    fi
    # Decrypt and view
    age -d -i "$AGE_KEY" "$encrypted_file" | less
}
# Edit encrypted document
edit_encrypted() {
    local encrypted_file="$1"

    # Check if file exists
    if [ -f "$encrypted_file" ]; then
        # Decrypt, edit, re-encrypt
        local temp=$(mktemp)
        age -d -i "$AGE_KEY" "$encrypted_file" > "$temp" || {
            echo "❌ Decryption failed"
            rm -f "$temp"
            return 1
        }
        ${EDITOR:-nvim} "$temp"
        # Re-encrypt if modified
        if [ -s "$temp" ]; then
            age -R "${HOME}/.secrets/.metadata/keys/master.age.pub" -o "$encrypted_file" "$temp"
            echo "✅ File encrypted and saved"
        fi
        shred -u "$temp"
    else
        echo "❌ File does not exist: $encrypted_file"
        return 1
    fi
}
# List all encrypted files in vault
list_vault() {
    if [ ! -d "$DOC_VAULT" ]; then
        echo "❌ Vault directory not found: $DOC_VAULT"
        return 1
    fi
    echo "📁 Encrypted documents in vault:"
    find "$DOC_VAULT" -type f -name "*.age" | while read -r file; do
        if [ -r "$file" ] && [ -s "$file" ]; then
            size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")
            echo "  ✅ $file ($size bytes)"
        else
            echo "  ⚠️  $file (invalid)"
        fi
    done
}

4. Device and Special File Detection

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# DETECT DEVICE FILES FOR SERIAL CONSOLE ACCESS
# ═══════════════════════════════════════════════════════════════════
# Find USB serial devices
find_usb_serial() {
    for dev in /dev/ttyUSB* /dev/ttyACM*; do
        if [ -c "$dev" ]; then
            echo "📟 Character device found: $dev"
            [ -r "$dev" ] && [ -w "$dev" ] && echo "  ✅ Readable and writable" || echo "  ⚠️  Permission denied"
        fi
    done
}
# Check if device is a terminal
is_terminal() {
    if [ -t 0 ]; then
        echo "✅ stdin is a terminal"
    else
        echo "❌ stdin is NOT a terminal (piped/redirected)"
    fi
    if [ -t 1 ]; then
        echo "✅ stdout is a terminal"
    else
        echo "❌ stdout is NOT a terminal (piped/redirected)"
    fi
}
# Check block devices (disks)
check_block_devices() {
    for dev in /dev/sd[a-z]; do
        if [ -b "$dev" ]; then
            echo "💽 Block device: $dev"
            [ -r "$dev" ] && echo "  ✅ Readable" || echo "  ⚠️  Not readable"
        fi
    done
}
# Check named pipes (FIFOs)
check_fifos() {
    local fifo="/tmp/my_fifo"

    if [ -p "$fifo" ]; then
        echo "📡 Named pipe exists: $fifo"
    else
        echo "Creating named pipe..."
        mkfifo "$fifo"
        [ -p "$fifo" ] && echo "✅ FIFO created" || echo "❌ Failed to create FIFO"
    fi
}
# Check sockets
check_sockets() {
    local sock="/var/run/docker.sock"

    if [ -S "$sock" ]; then
        echo "🔌 Socket exists: $sock"
        [ -r "$sock" ] && [ -w "$sock" ] && echo "  ✅ Accessible" || echo "  ⚠️  Permission denied"
    else
        echo "❌ Socket not found: $sock"
    fi
}

5. File Comparison and Timestamps

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# FILE COMPARISON AND TIMESTAMP CHECKS
# ═══════════════════════════════════════════════════════════════════
# Check if backup is newer than original
check_backup_freshness() {
    local original="$1"
    local backup="$2"

    if [ ! -f "$original" ]; then
        echo "❌ Original file not found: $original"
        return 1
    fi
    if [ ! -f "$backup" ]; then
        echo "⚠️  No backup exists for: $original"
        return 1
    fi
    if [ "$backup" -nt "$original" ]; then
        echo "✅ Backup is newer than original"
        return 0
    else
        echo "⚠️  Backup is OLDER than original - needs update!"
        return 1
    fi
}
# Check if files are hard links
check_hard_links() {
    local file1="$1"
    local file2="$2"

    if [ "$file1" -ef "$file2" ]; then
        echo "✅ Files are hard links (same inode)"
        ls -li "$file1" "$file2"
    else
        echo "❌ Files are NOT hard links"
    fi
}
# Find files modified since last backup
find_modified_files() {
    local dir="$1"
    local backup_marker="/var/backups/last_backup.marker"

    if [ ! -f "$backup_marker" ]; then
        echo "❌ Backup marker not found"
        return 1
    fi
    echo "Files modified since last backup:"
    find "$dir" -type f -newer "$backup_marker" | while read -r file; do
        [ -N "$file" ] && echo "  📝 $file (modified since last read)"
    done
}

6. Permission and Ownership Checks

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# PERMISSION AND OWNERSHIP VALIDATION
# ═══════════════════════════════════════════════════════════════════
# Check for SUID/SGID binaries (security audit)
find_setuid_files() {
    local dir="${1:-/usr/bin}"

    echo "🔍 Searching for SUID/SGID binaries in $dir..."
    find "$dir" -type f | while read -r file; do
        if [ -u "$file" ]; then
            echo "  🔴 SUID: $file"
            ls -l "$file"
        fi
        if [ -g "$file" ]; then
            echo "  🟡 SGID: $file"
            ls -l "$file"
        fi
    done
}
# Check if file is owned by current user
check_ownership() {
    local file="$1"

    if [ ! -e "$file" ]; then
        echo "❌ File does not exist: $file"
        return 1
    fi
    if [ -O "$file" ]; then
        echo "✅ You own this file: $file"
    else
        owner=$(stat -f%Su "$file" 2>/dev/null || stat -c%U "$file")
        echo "❌ File owned by: $owner"
    fi
    if [ -G "$file" ]; then
        echo "✅ File group matches your primary group"
    else
        group=$(stat -f%Sg "$file" 2>/dev/null || stat -c%G "$file")
        echo "⚠️  File group: $group"
    fi
}
# Check sticky bit (like /tmp)
check_sticky_bit() {
    local dir="$1"

    if [ -k "$dir" ]; then
        echo "✅ Sticky bit set on: $dir"
        echo "   (Only owner can delete files)"
    else
        echo "⚠️  No sticky bit on: $dir"
    fi
}
# Validate script permissions
validate_script() {
    local script="$1"

    [ ! -f "$script" ] && { echo "❌ Not a file: $script"; return 1; }
    [ ! -r "$script" ] && { echo "❌ Not readable: $script"; return 1; }
    [ ! -x "$script" ] && { echo "⚠️  Not executable: $script"; return 1; }

    echo "✅ Script is valid and executable: $script"
}

7. String and Numeric Comparisons

#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# STRING AND NUMERIC VALIDATION
# ═══════════════════════════════════════════════════════════════════
# Validate environment variables
check_env_vars() {
    # Check if variable is set and not empty
    [ -n "${ISE_HOST:-}" ] || { echo "❌ ISE_HOST not set"; return 1; }
    [ -n "${ISE_USER:-}" ] || { echo "❌ ISE_USER not set"; return 1; }
    [ -n "${ISE_PASS:-}" ] || { echo "❌ ISE_PASS not set"; return 1; }

    echo "✅ All ISE environment variables set"
}
# Validate MAC address format
validate_mac() {
    local mac="$1"

    # Check if empty
    [ -z "$mac" ] && { echo "❌ MAC address is empty"; return 1; }
    # Regex match (requires [[ ]])
    if [[ "$mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then
        echo "✅ Valid MAC address: $mac"
    else
        echo "❌ Invalid MAC address format: $mac"
        return 1
    fi
}
# Numeric comparison
check_threshold() {
    local value="$1"
    local threshold=80

    if [ "$value" -gt "$threshold" ]; then
        echo "🔴 CRITICAL: Value ($value) exceeds threshold ($threshold)"
        return 1
    elif [ "$value" -ge 70 ]; then
        echo "🟡 WARNING: Value ($value) approaching threshold"
        return 0
    else
        echo "✅ OK: Value ($value) within limits"
        return 0
    fi
}
# Version comparison
compare_versions() {
    local ver1="$1"
    local ver2="$2"

    if [ "$ver1" = "$ver2" ]; then
        echo "Versions are equal"
    elif [ "$ver1" \< "$ver2" ]; then
        echo "$ver1 is older than $ver2"
    else
        echo "$ver1 is newer than $ver2"
    fi
}

[ ] vs [[ ]] vs test

Differences

# ═══════════════════════════════════════════════════════════════════
# COMPARISON: [ ] vs [[ ]] vs test
# ═══════════════════════════════════════════════════════════════════
# 1. [ ] - POSIX compliant, portable
[ -f /etc/passwd ] && echo "exists"
# 2. test - Same as [ ], explicit command
test -f /etc/passwd && echo "exists"
# 3. [[ ]] - Bash extension, more features
[[ -f /etc/passwd ]] && echo "exists"

When to Use [[ ]]

# Pattern matching
[[ "$filename" = *.txt ]] && echo "Text file"
# Regex matching
[[ "$string" =~ ^[0-9]+$ ]] && echo "All digits"
# Logical operators (cleaner)
[[ -f file1 && -f file2 ]] && echo "Both exist"
# No word splitting issues
var="hello world"
[[ -n $var ]]  # Works without quotes
[ -n "$var" ]  # Quotes required

When to Use [ ]

# POSIX scripts (portability)
#!/bin/sh
[ -f /etc/passwd ] && echo "exists"
# Strict compatibility requirements
# [[ ]] not available in: dash, busybox sh, old shells

Common Patterns

Safe File Operations

# Check before reading
[ -f "$config" ] && [ -r "$config" ] && source "$config"
# Check before writing
[ -w "$dir" ] && echo "data" > "$dir/file.txt"
# Create if missing
[ -d "$dir" ] || mkdir -p "$dir"
# Backup if exists
[ -f "$file" ] && cp "$file" "${file}.bak"

Error Handling

# Exit on missing file
[ -f "$required_file" ] || { echo "Missing: $required_file"; exit 1; }
# Return from function
validate_file() {
    local file="$1"
    [ -f "$file" ] || return 1
    [ -r "$file" ] || return 1
    [ -s "$file" ] || return 1
    return 0
}

Multiple Conditions

# All must be true (AND)
if [ -f "$file" ] && [ -r "$file" ] && [ -s "$file" ]; then
    cat "$file"
fi
# Any can be true (OR)
if [ -f "$file1" ] || [ -f "$file2" ]; then
    echo "At least one exists"
fi
# Complex logic
if [ -f "$config" ] && { [ -w "$config" ] || [ -O "$config" ]; }; then
    echo "Can modify config"
fi

Quick Tips

  1. Always quote variables in [ ]: [ -f "$file" ]

  2. [[ ]] doesn’t need quotes: [[ -f $file ]] (but still good practice)

  3. Use && and || for chaining: [ -f file ] && cat file

  4. Check existence before type: [ -e file ] && [ -f file ]

  5. Validate empty strings: [ -z "$var" ] or [ -n "$var" ]

  6. Test numeric values: [ "$count" -gt 0 ]

  7. Use -e for any file type: Catches files, dirs, devices, etc.


See Also

  • man test - Full test command documentation

  • man bash - Section on conditional expressions

  • help test - Bash built-in help

  • help [[ - Extended test help


Last Updated: 2026-01-26 Tested On: Arch Linux, Bash 5.2