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
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
-
Always quote variables in [ ]:
[ -f "$file" ] -
[[ ]] doesn’t need quotes:
[[ -f $file ]](but still good practice) -
Use && and || for chaining:
[ -f file ] && cat file -
Check existence before type:
[ -e file ] && [ -f file ] -
Validate empty strings:
[ -z "$var" ]or[ -n "$var" ] -
Test numeric values:
[ "$count" -gt 0 ] -
Use -e for any file type: Catches files, dirs, devices, etc.