Branches in Bash often execute the wrong path when string comparisons use the numeric operator -eq or when variables are left unquoted. Switching to Bash’s [[ ]] test with the correct operators fixes those logic errors and makes scripts easier to maintain.

Method 1: Write if...else with Bash [[ ]] (strings, numbers, and files)

Step 1: Create a new script and add a minimal conditional using the modern Bash test syntax [[ ]]. This form avoids accidental glob expansion and word splitting, which reduces surprising behavior compared to single brackets.

#!/usr/bin/env bash
set -euo pipefail

read -r -p "Enter a value: " val

if [[ "$val" == "admin" ]]; then
  echo "Welcome, admin."
else
  echo "Access limited."
fi

Step 2: Compare strings with == or !=. Quote variables to keep empty values and spaces from breaking your tests. This prevents unintended matches caused by unquoted expansions.

user="alice"
target="alice"

if [[ "$user" == "$target" ]]; then
  echo "Usernames match."
else
  echo "Usernames differ."
fi

Step 3: Use numeric operators for integers inside [[ ]]: -eq, -ne, -gt, -ge, -lt, -le. These operate on numbers only and avoid lexicographic comparisons.

read -r -p "Enter a number: " n

if [[ "$n" -gt 10 ]]; then
  echo "Greater than 10."
elif [[ "$n" -eq 10 ]]; then
  echo "Equal to 10."
else
  echo "Less than 10."
fi

Step 4: Check files with file-test operators. These help you branch based on the filesystem state without running external commands.

  • -f path: regular file exists.
  • -d path: directory exists.
  • -e path: path exists (any type).
  • -x path: executable file exists.
  • -r path / -w path: readable / writable.
path="./data.txt"

if [[ -f "$path" ]]; then
  echo "File exists."
else
  echo "Creating file..."
  : > "$path"
fi

Step 5: Combine conditions with && (AND), || (OR), and ! (NOT). Group them with parentheses for clarity and correct precedence.

file="./report.log"
size=5

# True when the file exists AND size is at least 5
if [[ -f "$file" ]] && [[ "$size" -ge 5 ]]; then
  echo "Process report."
fi

# True when user is admin OR staff
role="staff"
if [[ "$role" == "admin" || "$role" == "staff" ]]; then
  echo "Privileged."
fi

# Negation: path exists but is not a directory
if [[ -e "$file" && ! -d "$file" ]]; then
  echo "Path is not a directory."
fi

Step 6: Use elif to short-circuit checks. Once one condition is true, later branches are skipped, which speeds up evaluation and reduces unnecessary commands.

status="warning"

if [[ "$status" == "error" ]]; then
  echo "Exit immediately."
  exit 1
elif [[ "$status" == "warning" ]]; then
  echo "Log and continue."
else
  echo "All good."
fi

Method 2: Compare integers with (( )) arithmetic evaluation

Step 1: Use arithmetic contexts for numeric logic. Inside (( )), you can write math-like comparisons (<, >, ==) without quoting variables. This is concise and fast for integer-only checks.

a=12 b=8

if (( a > b )); then
  echo "a is larger."
elif (( a == b )); then
  echo "Equal."
else
  echo "b is larger."
fi

Step 2: Combine boolean operators directly in arithmetic context. This reduces boilerplate when evaluating multiple numeric conditions.

x=7 y=3 z=10

if (( (x > y) && (z >= 10) )); then
  echo "Threshold met."
fi

Step 3: Validate input. Arithmetic contexts treat non-numeric values as zero. If input may be non-numeric, guard it with a regex test in [[ ]] before evaluating.

read -r n
if [[ "$n" =~ ^-?[0-9]+$ ]] && (( n >= 0 )); then
  echo "Non-negative integer."
else
  echo "Invalid number."
fi

Method 3: Use POSIX-portable [ ] for broader shell compatibility

Step 1: Prefer single brackets when targeting shells beyond Bash (e.g., /bin/sh). Remember that [ is a command: spaces and quoting are required.

#!/bin/sh

a="hello"
b="hello"

if [ "$a" = "$b" ]; then
  echo "Match."
else
  echo "No match."
fi

Step 2: Use numeric operators for integers and quote variables to prevent word splitting. This avoids bugs when variables are empty or contain spaces.

a=5
b=30

if [ "$a" -lt "$b" ]; then
  echo "a is less than b."
fi

Step 3: When doing lexicographic string comparisons with < or >, escape the operators to prevent redirection. Many errors stem from forgetting this.

x="apple" y="banana"

if [ "$x" \< "$y" ]; then
  echo "apple comes before banana."
fi

Step 4: Use file tests the same way as in [[ ]], but keep everything quoted to avoid edge cases.

file="./cfg.ini"
if [ -r "$file" ] && [ -w "$file" ]; then
  echo "Config is readable and writable."
fi

Method 4: Replace long chains with case for value-based branching

Step 1: Use case to simplify multiple equality checks on a single variable. This reduces nested if blocks and improves readability and speed when matching patterns.

read -r -p "Enter mode (start|stop|status): " mode

case "$mode" in
  start)  echo "Starting...";;
  stop)   echo "Stopping...";;
  status) echo "Service is running.";;
  *)      echo "Unknown mode."; exit 1;;
esac

Step 2: Match multiple patterns easily. This keeps your logic compact and avoids repeated variable comparisons.

level="warn"

case "$level" in
  error|err)  echo "Exit with failure."; exit 1;;
  warn|warning) echo "Log warning.";;

  info|debug) echo "Proceed normally.";;
  *)          echo "Unrecognized level.";;
esac

Method 5: Test multiple conditions correctly and debug failures

Step 1: Group boolean logic to reflect your truth table. For example, “require Z or both X and Y” becomes if [[ "$Z" == "TRUE" || ( "$X" == "TRUE" && "$Y" == "TRUE" ) ]]. Parentheses clarify precedence and prevent subtle bugs.

X="TRUE" Y="FALSE" Z="TRUE"

if [[ "$Z" == "TRUE" || ( "$X" == "TRUE" && "$Y" == "TRUE" ) ]]; then
  echo "Conditions met."
else
  echo "Conditions not met."
fi

Step 2: Choose the right operators. Use == for strings and -eq for integers. Mixing them causes incorrect branches (e.g., string compared with -eq always fails).

Step 3: Quote variable expansions in tests, especially with single brackets. Quoting avoids crashes when variables are empty and prevents wildcard expansion against filesystem entries.

Step 4: Trace logic when results look wrong. Enable xtrace to see each expanded command and condition as Bash evaluates it.

#!/usr/bin/env bash
set -x  # turn on trace
# ... your conditionals here ...
set +x  # turn off trace

Step 5: Keep scripts consistent and readable. Indent the body under then/else, leave spaces around brackets, and favor early exits on error branches to reduce nesting.


Reference: Common test operators at a glance

This quick list helps you pick the right operator for the job.

  • Strings: ==, !=, -n var (length > 0), -z var (empty).
  • Integers: -eq, -ne, -gt, -ge, -lt, -le; or use (( )) with <, >, ==.
  • Files: -f (regular), -d (directory), -e (exists), -x (executable), -r (readable), -w (writable).
  • Logic: && (AND), || (OR), ! (NOT); group with ( ... ) inside [[ ]].

With the right test form, correct operators, and consistent quoting, your Bash conditionals execute the intended branch every time. Keep [[ ]] for Bash scripts, use (( )) for math, and fall back to [ ] only when portability is a must.