Featured image of post Bash Conditionals: if, case, and DevOps Logic Handling

Bash Conditionals: if, case, and DevOps Logic Handling

Learn how to use if/elif/else, case…esac, numeric comparisons, string checks, and file tests in Bash. Includes practical DevOps examples for service checks, disk thresholds, and deploy decisions.

Bash Conditionals for DevOps

In the first article we wrote linear scripts — commands that run from top to bottom. In real operations, scripts often need to make decisions: skip work if a service is already running, raise an alert if disk usage crosses a threshold, or require confirmation before deploying to production.

Bash conditionals are what make a script react to the current system state. This article walks through if/elif/else, comparison operators, file checks, and case…esac — with DevOps examples you can reuse immediately.


if / elif / else

Basic syntax:

1
2
3
4
5
6
7
if [[ <condition> ]]; then
  <command when true>
elif [[ <another_condition> ]]; then
  <command when the other condition is true>
else
  <command when all conditions are false>
fi

A few notes:

  • In modern Bash, prefer [[ ... ]] over [ ... ]. [[ ]] is Bash’s conditional expression syntax. It supports extra features such as pattern matching and is safer when variables contain spaces.
  • Every if block must end with fi. If you forget it, bash -n script.sh will report a syntax error.
  • Indent the commands inside the block, commonly with 2 spaces, to keep the script readable.

A tiny example — check whether the script is running as root:

1
2
3
4
5
6
7
#!/usr/bin/env bash

if [[ "${EUID}" -eq 0 ]]; then
  echo "Running as root."
else
  echo "Not root, some commands may be denied."
fi

EUID is a Bash built-in variable that stores the effective user ID. The root user always has EUID=0.


Numeric comparisons

When comparing integers, use operators such as -eq, -ne, and -gt.

OperatorMeaning
-eqequal
-nenot equal
-gtgreater than
-gegreater than or equal
-ltless than
-leless than or equal

Example — check disk usage for /:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/bin/env bash
set -euo pipefail

DISK_USAGE_PERCENT="$(df -P / | awk 'NR==2 {gsub("%", "", $5); print $5}')"

if [[ "${DISK_USAGE_PERCENT}" -ge 90 ]]; then
  echo "CRITICAL: Disk / is ${DISK_USAGE_PERCENT}% used"
  exit 2
elif [[ "${DISK_USAGE_PERCENT}" -ge 80 ]]; then
  echo "WARNING: Disk / is ${DISK_USAGE_PERCENT}% used"
  exit 1
else
  echo "OK: Disk / is at ${DISK_USAGE_PERCENT}%"
fi

The script returns exit codes 0/1/2 for OK/WARN/CRIT. This is a familiar convention in monitoring systems such as Nagios/Icinga, and it is also easy to use from cron or CI/CD pipelines.

Tip: You can use (( ... )) for arithmetic expressions, for example if (( DISK_USAGE_PERCENT >= 80 )); then. Inside (( )), you do not need $ before variable names and can use familiar operators such as >=, <=, and ==.


String comparisons

For strings, use a different set of operators:

OperatorMeaning
= / ==equal
!=not equal
-z STRstring is empty
-n STRstring is not empty
< / >lexical ordering, only inside [[ ]]

Example — gate an environment before deployment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail

ENVIRONMENT="${1:-}"

if [[ -z "${ENVIRONMENT}" ]]; then
  echo "Usage: $0 <dev|staging|production>"
  exit 1
fi

if [[ "${ENVIRONMENT}" == "production" ]]; then
  read -r -p "Are you sure you want to deploy to PRODUCTION? (yes/no): " CONFIRM
  if [[ "${CONFIRM}" != "yes" ]]; then
    echo "Deployment cancelled."
    exit 1
  fi
fi

echo "Starting deployment to ${ENVIRONMENT}..."

Important note: always quote variables ("${ENVIRONMENT}"). If you leave $ENVIRONMENT unquoted and the variable is empty, [[ $ENVIRONMENT == "production" ]] still works, but similar scripts using [ ... ] may fail with syntax errors.


File and directory checks

Bash provides built-in operators for checking file state:

OperatorMeaning
-eexists, file or directory
-fexists and is a regular file
-dexists and is a directory
-rreadable
-wwritable
-xexecutable
-sfile exists and has size > 0
-Lis a symbolic link

Example — ensure the log directory exists and the config file is valid before starting an app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash
set -euo pipefail

APP_NAME="manager-blog"
CONFIG_FILE="/etc/${APP_NAME}/app.env"
LOG_DIR="/var/log/${APP_NAME}"

if [[ ! -f "${CONFIG_FILE}" ]]; then
  echo "ERROR: Config file not found: ${CONFIG_FILE}"
  exit 1
fi

if [[ ! -r "${CONFIG_FILE}" ]]; then
  echo "ERROR: No read permission for ${CONFIG_FILE}"
  exit 1
fi

if [[ ! -d "${LOG_DIR}" ]]; then
  echo "Creating log directory: ${LOG_DIR}"
  mkdir -p "${LOG_DIR}"
fi

echo "Pre-check OK, ready to start ${APP_NAME}."

The ! at the beginning of an expression is negation — [[ ! -f ... ]] reads as “if the file does not exist”.


Combining multiple conditions (AND / OR)

There are two common ways to combine conditions.

Option 1 — combine inside one [[ ]] with && and ||:

1
2
3
if [[ -f "/etc/nginx/nginx.conf" && -r "/etc/nginx/nginx.conf" ]]; then
  echo "Nginx config exists and is readable."
fi

Option 2 — chain two separate test commands with shell-level && / ||:

1
2
command -v docker >/dev/null 2>&1 && echo "Docker is installed"
command -v docker >/dev/null 2>&1 || echo "Docker is not installed"

In automation scripts, option 1 is easier to read when the conditions belong to the same decision. Option 2 is useful when you want “run this command, and only if it succeeds, run the next one”, for example:

1
mkdir -p /backup/db && tar -czf /backup/db/dump.tar.gz /var/lib/db

If mkdir fails, tar will not run.


case…esac — when if/elif gets too long

When you need to match a variable against many possible values, case is usually shorter and easier to read than a long if/elif chain.

Syntax:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case <variable> in
  <pattern1>)
    <command>
    ;;
  <pattern2>|<pattern3>)
    <command>
    ;;
  *)
    <default_command>
    ;;
esac

Example — route service operations from one script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env bash
set -euo pipefail

ACTION="${1:-}"
SERVICE_NAME="nginx"

case "${ACTION}" in
  start)
    echo "Starting ${SERVICE_NAME}..."
    sudo systemctl start "${SERVICE_NAME}"
    ;;
  stop)
    echo "Stopping ${SERVICE_NAME}..."
    sudo systemctl stop "${SERVICE_NAME}"
    ;;
  restart|reload)
    echo "Restarting ${SERVICE_NAME}..."
    sudo systemctl restart "${SERVICE_NAME}"
    ;;
  status)
    sudo systemctl status "${SERVICE_NAME}" --no-pager
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|reload|status}"
    exit 1
    ;;
esac

case supports glob-style patterns, so you can write dev*), *.log), [0-9]*), and similar branches. This is convenient when branching by environment name or file type.


DevOps practice: check_service.sh

Now combine the concepts above into a practical script that checks a service and disk usage.

Create check_service.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env bash
set -euo pipefail

SERVICE_NAME="${1:-nginx}"
DISK_THRESHOLD="${2:-80}"

echo "========================================"
echo "  Service & Disk Check"
echo "  Service   : ${SERVICE_NAME}"
echo "  Threshold : ${DISK_THRESHOLD}%"
echo "========================================"

# 1) Check whether systemctl is available
if ! command -v systemctl >/dev/null 2>&1; then
  echo "ERROR: systemctl is not available on this system, stopping."
  exit 1
fi

# 2) Check whether the service is running
if systemctl is-active --quiet "${SERVICE_NAME}"; then
  echo "OK: Service ${SERVICE_NAME} is running."
else
  echo "WARN: Service ${SERVICE_NAME} is not running. Trying to restart..."
  if sudo systemctl restart "${SERVICE_NAME}"; then
    echo "OK: Restarted ${SERVICE_NAME}."
  else
    echo "ERROR: Failed to restart ${SERVICE_NAME}."
    exit 2
  fi
fi

# 3) Check disk usage against the provided threshold
DISK_USAGE_PERCENT="$(df -P / | awk 'NR==2 {gsub("%", "", $5); print $5}')"

case 1 in
  $(( DISK_USAGE_PERCENT >= 90 )) )
    echo "CRITICAL: Disk / is ${DISK_USAGE_PERCENT}% used."
    exit 2
    ;;
  $(( DISK_USAGE_PERCENT >= DISK_THRESHOLD )) )
    echo "WARNING: Disk / is ${DISK_USAGE_PERCENT}% used (threshold ${DISK_THRESHOLD}%)."
    exit 1
    ;;
  *)
    echo "OK: Disk / is at ${DISK_USAGE_PERCENT}%."
    ;;
esac

Try it:

1
2
chmod +x check_service.sh
./check_service.sh nginx 80

Notable points:

  • Use systemctl is-active --quiet for status checks — this is cleaner than parsing command output with grep.
  • Return meaningful exit codes (0/1/2) so cron, alertmanager, or CI/CD pipelines can understand severity.
  • The case 1 in $(( ... )) ) pattern is a small trick: an arithmetic expression returns 1 when the condition is true and 0 when it is false, so the branch matching 1 runs. If you find this hard to read, use if/elif/else instead for clarity.

Common mistakes

  • Missing spaces in [[ ]]: Bash requires spaces around [[, ]], and operators. [[$A == "x"]] is a syntax error.
  • Using = for integers: [[ "${COUNT}" = 1 ]] performs a string comparison. For numbers, use -eq or move the expression into (( )).
  • Not quoting variables: If a variable is empty, Bash may parse the expression incorrectly in [ ] (POSIX test). [[ ]] is safer, but keeping the habit of "${VAR}" is still recommended.
  • Forgetting ;; in case: Each case branch must end with ;;. Without it, Bash continues parsing into the next branch after a match.
  • Confusing && / || with pipelines: cmd1 && cmd2 runs cmd2 only when cmd1 succeeds with exit code 0. Do not confuse this with the pipe |, which passes stdout from one command to another.

Implementation notes

  • When applying this to your own project, think about exit codes before writing the logic: when should the script return 0, and when should it return non-zero? This directly affects cron jobs and pipelines.
  • Best practices:
    • Prefer [[ ]] over [ ] in Bash-only scripts.
    • Always include a default *) branch in case to catch invalid values and print usage.
    • When a condition grows beyond 4–5 if/elif branches, consider switching to case or extracting logic into a function.
    • Put threshold variables near the top of the file or accept them from CLI arguments — do not scatter hardcoded values throughout the script.
  • Troubleshooting:
    • Script enters the wrong branch? → Run it with bash -x script.sh to inspect expanded variable values.
    • Seeing unary operator expected? → This is often caused by an empty variable in [ -f $FILE ]. Use [[ -f "${FILE}" ]] instead.
    • case does not match? → Check whether your pattern needs quoting. In case, entries after in are patterns (globs), not plain strings.

🎯 Conclusion

Conditionals are the first step that make your Bash scripts “think” — reacting to state instead of running blindly. With if/elif/else, case…esac, numeric comparisons, string checks, and file tests, you now have enough tools to write practical operations scripts such as health checks, deployment gates, and service dispatchers.

In the next article, we will cover loops in Bash with for, while, and until, including automation examples for iterating over server lists and reading log files line by line. 🚀


References