Shell Scripting Bash Operators Basics May 2026

Shell Scripting Operators & Test Expressions

Master every bash operator — arithmetic, string comparison, file tests, logical operators — and understand the critical differences between [ ], [[ ]], and (( )) that trip up even experienced scripters.

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.

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
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]}"
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
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"
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
Key Condition true Condition false / error Warning
bash — operators demo
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
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 (( )).