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" ]]