Skip to main content

Lesson: Conditionals & Tests

What you'll learn

  • How to branch your script with if, elif, and else.
  • How to write tests with [[ ]] for strings, numbers, and files.
  • How exit codes drive conditionals, and how to chain commands with && and ||.
  • How to handle many possible values cleanly with case.

By the end you can make scripts that react to their environment — checking whether a file exists, whether a service is up, or whether a value is what you expect.

The lesson

Real automation has to make decisions: if the disk is full, warn; if the service is down, restart it. In Bash, decisions are built on exit codes (from the last lesson) and the if statement.

Run everything on the Jumpbox (10.100.100.254, user ubuntu).

1. if is driven by exit codes

An if statement runs a command and branches on its exit code: 0 (success) is "true", non-zero is "false". This surprises newcomers — there is no boolean type, just exit codes.

if grep -q "ubuntu" /etc/passwd; then
  echo "user ubuntu exists"
else
  echo "no such user"
fi

grep -q prints nothing but exits 0 if it found a match. The structure is always:

if  <command>; then
  <runs when command succeeds / exit 0>
elif <other command>; then
  <runs when that one succeeds>
else
  <runs when none succeeded>
fi

Note fi (if backwards) closes the block, and the then needs a ; or a newline before it.

2. The test command [[ ]]

Most of the time you are not testing a real command but a condition — is this number bigger, does this file exist? Bash gives you [[ ... ]] for that. It is itself a command that exits 0 (true) or 1 (false):

count=5
if [[ $count -gt 3 ]]; then
  echo "more than three"
fi

You may also see single-bracket [ ... ] (the older test command). Prefer [[ ]] in Bash: it is safer with empty variables, supports &&/|| inside, and does pattern matching. Always put spaces inside the brackets — [[$count is an error.

3. String tests

name="ubuntu"

[[ $name == "ubuntu" ]]      # equal
[[ $name != "root" ]]        # not equal
[[ -z $name ]]               # true if string is EMPTY (zero length)
[[ -n $name ]]               # true if string is NOT empty
[[ $name == ub* ]]           # pattern match: starts with "ub"

Inside [[ ]], the right side of == is treated as a glob pattern unless quoted. So [[ $f == *.log ]] is true for any name ending in .log. Quote it ("*.log") to match literally.

A very common safety check:

read -rp "Enter a username: " u
if [[ -z $u ]]; then
  echo "You must enter a name" >&2
  exit 1
fi

4. Number tests

Numeric comparisons use letter operators, not symbols:

  -eq   equal              -ne   not equal
  -gt   greater than       -lt   less than
  -ge   greater or equal   -le   less or equal
disk_used=82
if [[ $disk_used -ge 90 ]]; then
  echo "CRITICAL: disk over 90%"
elif [[ $disk_used -ge 75 ]]; then
  echo "WARNING: disk getting full"
else
  echo "Disk OK"
fi

Do not use > or < for numbers inside [[ ]] — there they mean string ordering, not numeric. Use -gt, -lt, etc. (For arithmetic you can also use (( )): if (( disk_used >= 90 )); then.)

5. File tests

These are the bread and butter of sysadmin scripts:

  -e PATH   exists (file or directory)
  -f PATH   exists AND is a regular file
  -d PATH   exists AND is a directory
  -r PATH   is readable        -w writable     -x executable
  -s PATH   exists AND is non-empty
config="/etc/hosts"
if [[ -f $config && -r $config ]]; then
  echo "$config is a readable file"
else
  echo "Cannot read $config" >&2
  exit 1
fi

Notice && inside [[ ]] means "both conditions true". You can also use || for "either", and ! for "not": if [[ ! -d /backups ]]; then mkdir /backups; fi.

6. Chaining with && and || (outside brackets)

Between whole commands, && and || are shortcuts based on exit codes:

mkdir -p /tmp/work && echo "ready"        # echo runs ONLY if mkdir succeeded
ping -c1 example.com || echo "host down"  # echo runs ONLY if ping failed
  • A && B runs B only if A succeeded (exit 0).
  • A || B runs B only if A failed (non-zero).

This is a concise alternative to a full if for one-liners. A handy pattern for "do this or bail out":

cd /var/log || exit 1

If cd fails, the script stops instead of running the rest in the wrong directory — a real bug-preventer.

7. case for many options

When you are comparing one value against many possibilities, a chain of elif gets ugly. case is cleaner:

read -rp "Action (start/stop/status): " action
case "$action" in
  start)
    echo "starting..." ;;
  stop)
    echo "stopping..." ;;
  status)
    echo "checking..." ;;
  *)
    echo "Unknown action: $action" >&2
    exit 1 ;;
esac

Each branch is a pattern, then ), then commands, ending in ;;. The *) at the end is the catch-all (default), like else. Patterns are globs, so you can match *.log) or y|Y|yes) for several values at once. case closes with esac (case backwards).

8. Putting it together

#!/usr/bin/env bash
# Warn if a log directory is missing or its disk usage is high.
logdir="/var/log"
if [[ ! -d $logdir ]]; then
  echo "No $logdir directory" >&2
  exit 1
fi

used=$(df --output=pcent "$logdir" | tail -1 | tr -dc '0-9')
if [[ $used -ge 90 ]]; then
  echo "CRITICAL: $logdir at ${used}%"
  exit 2
else
  echo "$logdir usage ${used}% — OK"
fi

This checks a file condition, computes a number from a command, and branches on it — the shape of the health-check you will build in the assignment.

Dig deeper

Search terms

  • bash if statement exit code true false
  • bash double bracket test string comparison
  • bash numeric comparison -gt -lt -eq
  • bash file test -f -d -e operators
  • bash && || short circuit command chaining
  • bash case statement esac example

Check yourself

  1. In an if, what exit code counts as "true" and which counts as "false"?
  2. Why is [[ ]] preferred over single-bracket [ ] in Bash?
  3. Which operator tests whether a string is empty, and which tests numeric "greater than"?
  4. What is the difference between A && B and A || B?
  5. In a case statement, what does the *) branch do, and what closes the whole block?