Conditionals & Tests

Tests & Exit Status

Bash has no boolean type. A condition is simply a command, and its exit status (0 = true, non-zero = false) decides the branch. Everything else is sugar around that idea.

The test Command

test -f /etc/passwd        # true if it is a regular file
[ -f /etc/passwd ]         # identical — '[' is a command, ']' closes it
[[ -f /etc/passwd ]]       # Bash's extended test (preferred)
[ is an ordinary command. That is why the spaces are mandatory: [ -f x ] works, [-f x] does not.

File Tests

TestTrue when
-e ff exists (any type)
-f ff exists and is a regular file
-d ff exists and is a directory
-r / -w / -x ff is readable / writable / executable
-s ff exists and is not empty
[[ -d "$dir" ]] && echo "directory exists"
[[ -s "$log" ]] || echo "log is empty or missing"
[[ -x "$HOME/bin/tool" ]] && "$HOME/bin/tool"

String Tests

[[ -z "$s" ]]        # true if s is empty
[[ -n "$s" ]]        # true if s is non-empty
[[ "$a" == "$b" ]]   # equal (== is a pattern match inside [[ ]])
[[ "$a" != "$b" ]]   # not equal
[[ "$a" < "$b" ]]    # lexicographic less-than
[[ "$f" == *.txt ]]  # glob pattern match — only inside [[ ]]

Numeric Tests

[[ "$n" -eq 0 ]]     # equal
[[ "$n" -ne 0 ]]     # not equal
[[ "$n" -lt 10 ]]    # less than
[[ "$n" -le 10 ]]    # less or equal
[[ "$n" -gt 10 ]]    # greater than
[[ "$n" -ge 10 ]]    # greater or equal

(( n > 10 ))         # arithmetic context — often clearer for numbers

[[ ]] vs [ ]

Inside [[ ]] word splitting and globbing do not apply to the operands, so unquoted variables are safer, and the operators &&, ||, < and > work without escaping.

# POSIX: must quote, and must use -a / -o
[ -n "$user" ] && [ -f "$cfg" ]

# Bash: cleaner and safer
[[ -n $user && -f $cfg ]]
Danger in [ ]: an empty variable makes [ $x -gt 3 ] become [ -gt 3 ], a syntax error. Inside [[ ]] it is handled correctly. Prefer [[ ]] for Bash scripts.

if / elif / else

Syntax

if [[ "$1" == "--help" ]]; then
  usage; exit 0
elif [[ "$1" == "--version" ]]; then
  echo "v1.0"
else
  echo "unknown option: $1" >&2
  exit 2
fi

The then keyword is required; put it on the same line as if (after ;) or on the next line. fi closes the block — a reminder that this is a language, not a C-like.

Combining Conditions

if [[ -f "$cfg" && -r "$cfg" ]]; then
  echo "readable config"
fi

if ! command -v jq >/dev/null; then
  echo "jq is required" >&2; exit 1
fi

case Statements

Matching Patterns

case is a multi-way branch that matches globs, not regular expressions. It is the cleanest way to dispatch on a word.

case "$cmd" in
  start|up)    echo "starting" ;;
  stop|down)   echo "stopping" ;;
  status)      echo "running" ;;
  *.log)       echo "that is a log file" ;;
  *)           echo "unknown: $cmd" >&2; exit 2 ;;
esac

Each clause ends with ;;. Alternatives are separated by |.

Fall-through

case "$n" in
  1) echo "one" ;&      # ;& falls through unconditionally (Bash)
  2) echo "two" ;;&     # ;;& retests the next patterns (Bash)
esac

Pitfalls

  • Missing spaces inside [ ] cause "command not found".
  • if [ $x = y ] with an empty x is a syntax error — quote it or use [[ ]].
  • -eq is numeric; == is textual. Comparing "01" and "1" gives different answers for each.
  • Every if needs exactly one fi; elif is one word, not else if.