Conditionals let a script choose. Bash has one if-statement with one quirky detail — the condition is a command, not an expression — and once that clicks, test, [ ], and [[ ]] fall into place.

The shape #

The if statement, with its then and fi
if [[ -f "$config" ]]; then
  echo "using $config"
else
  echo "no config found" >&2
  exit 1
fi

if runs the command between it and then; if the command exits 0, the then-branch runs. That is why [ ... ] and [[ ... ]] are commands that test things and exit accordingly — and why if grep -q error log; then works with no brackets at all.

test, [ ], and [[ ]] #

test and [ are the same command (the POSIX classic); [[ is the bash keyword with better behaviour: no word-splitting surprises inside, pattern matching, and && / || between tests.

TestTrue when
-f file / -d dirFile / directory exists
-r / -w / -xReadable / writable / executable
-z s / -n sString empty / non-empty
s1 = s2Strings equal
n -eq -lt -gt -ge -leNumeric comparisons
$s == *.log ([[ ]] only)Pattern match

The number one rule survives every dialect: quote the variables. Unquoted, an empty $var vanishes and [ -f $var ] becomes [ -f ] — a true statement about the two-character string "-f". Quoted, it stays a filename test. See quoting for why.

Combining and nesting #

And, or, and the else-if chain
if [[ -r "$f" && -s "$f" ]]; then
  echo "$f is readable and non-empty"
elif [[ -d "$f" ]]; then
  echo "$f is a directory"
else
  echo "$f is neither"
fi

For dispatching on values rather than conditions — subcommands like start and stop — case reads far better than an elif ladder.

When if is the wrong tool #

Checking whether a command worked does not need an if at all — && and || chain on success and failure, which is the backbone of exit-code discipline:

Run the next thing only if the last worked
mkdir -p out && cp app.conf out/

Failures deserve strict mode so unhandled ones stop the script; exit codes make the choices observable. The scripting series continues.