Bash has three distinct test syntaxes: the POSIX [ ], the bash-extended [[ ]], and the arithmetic (( )). Knowing which to use and why prevents entire classes of bugs — especially with strings containing spaces, empty variables, and regex matching.
1
[ ] vs [[ ]] — know the difference
BASH
# ── [ ] — POSIX test (also written as: test expr) ────────
# Slower, more portable, treats unquoted vars dangerously
name=""
[ -n $name ] # DANGER: unquoted empty var → [ -n ] → true!
[ -n "$name" ] # Safe: quoted → [ -n "" ] → false
# ── [[ ]] — Bash extended test (RECOMMENDED) ─────────────
# Faster, no word splitting on unquoted vars, supports regex
[[ -n $name ]] # Safe even unquoted
[[ -n "$name" ]] # Also fine
# [[ ]] supports pattern matching and regex
email="user@example.com"
[[ "$email" == *@*.* ]] && echo "looks like email"
[[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] \
&& echo "valid email format"
# [[ ]] supports && and || inside the brackets
[[ -n "$name" && "$name" != "root" ]] && echo "valid non-root user"
# RULE: Always use [[ ]] in bash scripts, [ ] only for sh portability
2
String comparison operators
BASH
s1="hello"
s2="world"
empty=""
# ── Equality ──────────────────────────────────────────────
[[ "$s1" == "hello" ]] && echo "equal"
[[ "$s1" != "$s2" ]] && echo "not equal"
# ── Empty / non-empty ─────────────────────────────────────
[[ -z "$empty" ]] && echo "empty is empty" # -z: zero length
[[ -n "$s1" ]] && echo "s1 is non-empty" # -n: non-zero length
# ── Lexicographic comparison ──────────────────────────────
[[ "apple" < "banana" ]] && echo "apple sorts before banana"
[[ "zebra" > "apple" ]] && echo "zebra sorts after apple"
# ── Pattern matching (glob) ───────────────────────────────
file="report_2026.csv"
[[ "$file" == *.csv ]] && echo "is a CSV file"
[[ "$file" == report_* ]] && echo "is a report file"
[[ "$file" == *2026* ]] && echo "is from 2026"
# ── Regex matching (=~) ───────────────────────────────────
ip="192.168.1.100"
[[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] \
&& echo "valid IP format"
# Capture groups from regex
date_str="2026-05-01"
[[ "$date_str" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]
echo "Year: ${BASH_REMATCH[1]}"
echo "Month: ${BASH_REMATCH[2]}"
echo "Day: ${BASH_REMATCH[3]}"
3
Integer / arithmetic comparison operators
BASH
a=10
b=20
# ── Inside [[ ]] — use -eq -ne -lt -le -gt -ge ───────────
[[ "$a" -eq 10 ]] && echo "a equals 10" # equal
[[ "$a" -ne "$b" ]] && echo "a != b" # not equal
[[ "$a" -lt "$b" ]] && echo "a less than b" # less than
[[ "$a" -le 10 ]] && echo "a <= 10" # less or equal
[[ "$b" -gt "$a" ]] && echo "b greater than a" # greater than
[[ "$b" -ge 20 ]] && echo "b >= 20" # greater or equal
# ── Inside (( )) — use == != < <= > >= ─────────────
# More natural C-style arithmetic — preferred for pure numbers
(( a == 10 )) && echo "a is 10"
(( a != b )) && echo "a != b"
(( a < b )) && echo "a < b"
(( b > a )) && echo "b > a"
# (( )) also works in if directly
cpu=85
threshold=80
if (( cpu > threshold )); then
echo "WARNING: CPU ${cpu}% exceeds ${threshold}%"
fi
# AVOID: comparing numbers with == inside [[ ]]
# [[ "$a" == "10" ]] ← string compare, works accidentally
# [[ "$a" == 10 ]] ← also string compare, may surprise you
# Use (( )) or -eq for guaranteed numeric comparison
4
File test operators — the complete reference
BASH
f="/etc/passwd"
d="/etc"
# ── Existence tests ───────────────────────────────────────
[[ -e "$f" ]] && echo "exists (file or dir)"
[[ -f "$f" ]] && echo "is a regular file"
[[ -d "$d" ]] && echo "is a directory"
[[ -L "$f" ]] && echo "is a symlink"
[[ -p "$f" ]] && echo "is a named pipe (FIFO)"
[[ -S "$f" ]] && echo "is a socket"
# ── Permission tests ──────────────────────────────────────
[[ -r "$f" ]] && echo "readable"
[[ -w "$f" ]] && echo "writable"
[[ -x "$f" ]] && echo "executable"
[[ -u "$f" ]] && echo "setuid bit set"
[[ -g "$f" ]] && echo "setgid bit set"
[[ -k "$f" ]] && echo "sticky bit set"
# ── Size tests ────────────────────────────────────────────
[[ -s "$f" ]] && echo "non-empty (size > 0)"
# ── Comparison between two files ─────────────────────────
[[ "$f" -nt "/etc/hosts" ]] && echo "passwd newer than hosts"
[[ "$f" -ot "/etc/hosts" ]] && echo "passwd older than hosts"
[[ "$f" -ef "/etc/passwd" ]] && echo "same file (inode)"
# ── Practical guard pattern ───────────────────────────────
require_file() {
[[ -f "${1}" ]] || { echo "ERROR: required file missing: ${1}" >&2; exit 1; }
[[ -r "${1}" ]] || { echo "ERROR: file not readable: ${1}" >&2; exit 1; }
}
require_file "/etc/myapp/config.env"
5
Logical operators — combining conditions
BASH
user="vriddh"
age=30
file="/etc/passwd"
# ── Inside [[ ]] ──────────────────────────────────────────
[[ -n "$user" && "$user" != "root" ]] && echo "valid non-root"
[[ -z "$user" || "$user" == "anonymous" ]] && echo "no real user"
[[ ! -f "$file" ]] && echo "file does not exist"
# ── Chaining with && and || outside brackets ──────────────
# cmd1 && cmd2 — run cmd2 only if cmd1 succeeded
# cmd1 || cmd2 — run cmd2 only if cmd1 failed
mkdir -p /tmp/mydir && echo "dir created"
rm /no/such/file 2>/dev/null || echo "file not found, continuing"
# Guard pattern: exit if command fails
cd "/app/deploy" || { echo "Cannot cd to deploy dir" >&2; exit 1; }
# ── Complex multi-condition ───────────────────────────────
if [[ "${EUID}" -eq 0 && -f "/etc/myapp.conf" && -n "${APP_ENV}" ]]; then
echo "All conditions met — proceeding"
fi
# ── Negation ──────────────────────────────────────────────
if ! command -v docker >/dev/null 2>&1; then
echo "Docker not installed"
exit 1
fi
Terminal output
Key
Condition true
Condition false / error
Warning
vriddh@prod-01:~/scripts$bash operators.sh
equal
not equal
looks like email
valid email format
a is 10
a < b
WARNING: CPU 85% exceeds 80%
is a regular file
readable
non-empty (size > 0)
Year: 2026 Month: 05 Day: 01
ERROR: required file missing: /etc/myapp/config.env
█
6
Production-grade validation function
BASH
#!/usr/bin/env bash
# validate.sh — Reusable validation functions
is_integer() { [[ "${1}" =~ ^-?[0-9]+$ ]]; }
is_positive() { [[ "${1}" =~ ^[0-9]+$ ]] && (( ${1} > 0 )); }
is_ip() { [[ "${1}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; }
is_email() { [[ "${1}" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; }
is_port() { is_integer "${1}" && (( ${1} >= 1 && ${1} <= 65535 )); }
file_exists() { [[ -f "${1}" && -r "${1}" ]]; }
dir_writable(){ [[ -d "${1}" && -w "${1}" ]]; }
# Usage
is_ip "192.168.1.1" && echo "✔ valid IP" || echo "✘ invalid IP"
is_port "3306" && echo "✔ valid port" || echo "✘ invalid port"
is_email "bad-email" && echo "✔ valid email"|| echo "✘ invalid email"
is_integer "abc" && echo "✔ integer" || echo "✘ not integer"
file_exists "/etc/passwd" && echo "✔ file ok" || echo "✘ file missing"
✔ Quick rule — Use
[[ ]] for string and file tests. Use (( )) for arithmetic. Use -eq -ne -lt -gt inside [[ ]] for integer comparison. Never use == inside [[ ]] for numbers. Always quote strings, but you can skip quotes inside (( )).