Conditional Execution
in Bash Scripts

Conditionally execute code (use of: if, test, [], etc.)

CIS126RH | RHEL System Administration 1
Mesa Community College

Scripts that do the same thing every time regardless of circumstances are fragile. Conditional logic lets scripts check whether a file exists before editing it, whether a service is running before restarting it, whether the current user is root before attempting a privileged operation. This module covers the if statement, the test command and its bracket equivalents, and the full range of conditions used in RHEL administration scripts. These constructs are tested on the RHCSA exam.

Learning Objectives

  1. Explain exit codes and how bash uses them — Describe how every command returns an exit code and how if uses that code to decide what to execute
  2. Write if/then/else statements — Construct single-branch, two-branch, and multi-branch conditional blocks using if, then, elif, else, and fi
  3. Use test, [ ], and [[ ]] to evaluate conditions — Apply file tests, string comparisons, and integer comparisons
  4. Use conditional operators on the command line — Apply && and || for inline conditional execution

Exit Codes: The Foundation

Every command that runs in bash exits with a numeric exit code. This code is how the shell knows whether a command succeeded or failed — and it is what if evaluates.

Exit code Meaning if evaluates as
0Success — command completed without errorTrue — execute the then block
1–255Failure — non-zero means something went wrongFalse — skip the then block
# Check the exit code of the last command with $?
$ grep root /etc/passwd
$ echo $?
0    # match found — success

$ grep nobody99 /etc/passwd
$ echo $?
1    # no match — failure

$ ls /nonexistent
$ echo $?
2    # error — file not found
Zero is true in bash

This is the opposite of most programming languages where 0 is false. In bash, exit code 0 means success, which if treats as true. Any non-zero exit code means failure, treated as false.

The if Statement Structure

The if statement runs a command and executes one block of code if the command exits with 0 (success) and another block if it does not.

# Full structure with if, then, else, fi
if COMMAND; then
    # commands here run when COMMAND exits 0 (success)
else
    # commands here run when COMMAND exits non-zero (failure)
fi

# Single-branch — no else needed
if COMMAND; then
    # runs only on success
fi

# Multi-branch with elif
if COMMAND1; then
    # runs if COMMAND1 succeeds
elif COMMAND2; then
    # runs if COMMAND1 failed and COMMAND2 succeeds
else
    # runs if both commands failed
fi
fi closes every if block

Every if must end with fi (if spelled backwards). Missing fi is one of the most common syntax errors in bash scripts — the shell waits for more input or reports a syntax error on the next line.

A First Real Example

Any command that returns an exit code can be used directly in an if statement — no test brackets required.

#!/bin/bash
# Check if a user exists before taking action

if grep -q "^student:" /etc/passwd; then
    echo "User student exists"
else
    echo "User student does not exist"
fi
# Check whether a service is active
if systemctl is-active --quiet sshd; then
    echo "sshd is running"
else
    echo "sshd is not running — starting it"
    systemctl start sshd
fi
Use -q to suppress output inside if

The -q (quiet) flag on grep and --quiet on systemctl suppress output — the command still returns the correct exit code, but nothing is printed. This keeps script output clean.

The test Command

test evaluates an expression and exits with 0 if the expression is true or 1 if it is false. It is the primary tool for testing file attributes, string values, and numbers in scripts.

# test returns 0 (true) or 1 (false)
$ test -f /etc/hosts
$ echo $?
0    # /etc/hosts exists and is a regular file

$ test -f /etc/nofile
$ echo $?
1    # does not exist — false

# Used inside an if statement
if test -f /etc/hosts; then
    echo "/etc/hosts exists"
fi

# Negation with ! — true when the test is false
if test ! -f /tmp/lockfile; then
    echo "No lockfile — safe to proceed"
fi

Single Brackets: [ ]

[ ] is a synonym for test — it evaluates the same expressions and returns the same exit codes. It is the most common form in POSIX-compatible scripts.

# [ ] is identical to test — choose whichever is clearer
if [ -f /etc/hosts ]; then
    echo "/etc/hosts is a regular file"
fi

# Check if a variable is non-empty
NAME="student"
if [ -n "$NAME" ]; then
    echo "NAME is set to: $NAME"
fi

# Compare two strings
if [ "$USER" = "root" ]; then
    echo "Running as root"
fi
Spacing rules — spaces are required

The brackets must have a space after [ and before ]. [-f /etc/hosts] is a syntax error. Variables must be quoted: "$VAR" not $VAR — an unquoted empty variable causes a syntax error inside brackets.

Double Brackets: [[ ]]

[[ ]] is a bash built-in that extends [ ] with safer handling of variables and additional operators. It is the preferred form in bash-specific scripts.

# [[ ]] does not require quoting for empty variables
if [[ $NAME == "student" ]]; then
    echo "Hello, student"
fi

# Pattern matching with == (glob patterns, not regex)
if [[ $FILE == *.conf ]]; then
    echo "$FILE is a config file"
fi

# Regex matching with =~ operator
if [[ $IPADDR =~ ^[0-9]{1,3}\.[0-9]{1,3} ]]; then
    echo "Looks like an IP address"
fi

# Logical AND inside [[ ]] — no need for -a flag
if [[ -f /etc/hosts && -r /etc/hosts ]]; then
    echo "File exists and is readable"
fi
[ ] for portability, [[ ]] for bash scripts

Use [ ] when writing POSIX shell scripts that must run on any Unix system. Use [[ ]] when writing bash scripts on RHEL — it is safer and supports pattern and regex matching.

File Test Operators

File tests are the most common conditions in administration scripts. All work with both test, [ ], and [[ ]].

Operator True if… Example
-e fileFile exists (any type)[ -e /etc/hosts ]
-f fileFile exists and is a regular file[ -f /etc/ssh/sshd_config ]
-d dirPath exists and is a directory[ -d /var/log ]
-r fileFile exists and is readable[ -r /etc/shadow ]
-w fileFile exists and is writable[ -w /tmp/output.txt ]
-x fileFile exists and is executable[ -x /usr/bin/python3 ]
-s fileFile exists and is not empty (size > 0)[ -s /var/log/messages ]
-L fileFile exists and is a symbolic link[ -L /etc/localtime ]
-b fileFile is a block device[ -b /dev/sda ]
-z fileFile exists and is empty (size = 0)[ -z /tmp/lockfile ]

String Test Operators

String tests compare variable values or check whether they are set and non-empty.

Operator True if… Example
-z "$str"String is empty (zero length)[ -z "$NAME" ]
-n "$str"String is not empty (non-zero length)[ -n "$NAME" ]
"$a" = "$b"Strings are equal (POSIX)[ "$USER" = "root" ]
"$a" == "$b"Strings are equal (bash extended)[[ "$USER" == "root" ]]
"$a" != "$b"Strings are not equal[ "$USER" != "root" ]
"$a" < "$b"a comes before b alphabetically[[ "$a" < "$b" ]]
# Check if the script is run as root
if [ "$(id -u)" -ne 0 ]; then
    echo "Error: this script must be run as root"
    exit 1
fi

# Guard: check that an argument was provided
if [ -z "$1" ]; then
    echo "Usage: $0 username"
    exit 1
fi

Integer Comparison Operators

Integer comparisons use letter-based operators inside [ ] and [[ ]], or standard symbols inside arithmetic (( )).

[ ] operator Meaning (( )) equivalent
-eqEqual to==
-neNot equal to!=
-ltLess than<
-leLess than or equal to<=
-gtGreater than>
-geGreater than or equal to>=
# Count lines in a log and warn if too many
LINES=$(wc -l < /var/log/messages)
if [ "$LINES" -gt 10000 ]; then
    echo "Warning: /var/log/messages has $LINES lines"
fi

# Arithmetic form — natural symbols, no quotes needed
if (( LINES > 10000 )); then
    echo "Warning: $LINES lines"
fi

Compound Conditions

Multiple conditions can be combined with logical AND and OR operators.

Inside [ ] Inside [[ ]] Meaning
-a&&AND — both conditions must be true
-o||OR — at least one condition must be true
!!NOT — negates the condition
# Both conditions must be true — file exists AND is not empty
if [[ -f /var/log/messages && -s /var/log/messages ]]; then
    echo "Log file exists and has content"
fi

# Either condition may be true — missing arg OR bad arg
if [[ -z "$1" || "$1" == "--help" ]]; then
    echo "Usage: $0 username"
    exit 0
fi

# NOT — do something when directory does NOT exist
if [[ ! -d /srv/project ]]; then
    mkdir -p /srv/project
    echo "Created /srv/project"
fi

Inline Conditionals: && and ||

The && and || operators between commands provide a compact way to write simple conditional logic without a full if block.

# && — run second command only if first succeeds
mkdir -p /srv/data && echo "Directory created"

# || — run second command only if first fails
id student &> /dev/null || useradd student

# Chain: create dir, enter it, confirm — stop on any failure
mkdir -p /tmp/work && cd /tmp/work && echo "Ready"

# Guard pattern: exit if not root
[ "$(id -u)" -eq 0 ] || { echo "Must be root"; exit 1; }
&& and || vs if/then/else

Use && and || for simple one-action conditions. Use a full if/then/else block when the condition has multiple commands or when the logic is complex enough to benefit from clear structure.

Real Administration Script Examples

Backup a config file before editing

#!/bin/bash
CONFIG="/etc/ssh/sshd_config"
if [[ -f "$CONFIG" ]]; then
    cp "$CONFIG" "${CONFIG}.bak"
    echo "Backed up $CONFIG"
else
    echo "Error: $CONFIG not found"
    exit 1
fi

Ensure a directory exists before writing to it

#!/bin/bash
LOGDIR="/var/log/myscript"
[[ -d "$LOGDIR" ]] || mkdir -p "$LOGDIR"
echo "$(date): started" >> "$LOGDIR/run.log"

Script Structure Best Practices

A well-structured bash script with conditionals follows a consistent pattern.

#!/bin/bash
#
# Purpose: add a user and set up their home directory
# Usage:   ./setup-user.sh USERNAME

# 1. Check prerequisites
if [ "$(id -u)" -ne 0 ]; then
    echo "Error: must be run as root"; exit 1
fi

if [ -z "$1" ]; then
    echo "Usage: $0 USERNAME"; exit 1
fi

USERNAME="$1"

# 2. Main logic
if id "$USERNAME" &> /dev/null; then
    echo "User $USERNAME already exists — skipping"
else
    useradd -m "$USERNAME"
    echo "Created user $USERNAME"
fi
Guard clauses at the top

Put prerequisite checks — root check, argument validation, dependency checks — at the top of the script before any actions. This fails fast with a clear message rather than failing halfway through.

Common Mistakes

Mistake What goes wrong Correct form
Missing spaces inside [ ] [-f /etc/hosts] — bash error: command not found [ -f /etc/hosts ] — space after [ and before ]
Unquoted variables in [ ] [ $VAR = "x" ] fails if VAR is empty or has spaces [ "$VAR" = "x" ] — always quote variables
Missing fi Syntax error on the next line or unexpected end of file Every if must end with fi
Using = for numeric comparison [ "$COUNT" = "10" ] — string comparison, not numeric [ "$COUNT" -eq 10 ] — use -eq for numbers
Using == inside POSIX [ ] Some shells reject == inside single brackets Use = inside [ ] and == inside [[ ]]
No semicolon between condition and then on same line if [ -f /etc/hosts ] then — syntax error if [ -f /etc/hosts ]; then — semicolon before then

Knowledge Check

Answer these before moving to the next slide.

  1. What exit code does a command return on success? What does if do with that code?
  2. Write an if statement that prints "File found" if /etc/resolv.conf exists as a regular file, and "File missing" if it does not.
  3. What is wrong with this line? if [-d /tmp/work]; then
  4. Write a condition that is true only when the variable $USER is equal to root AND the file /etc/shadow exists.
  5. A script receives a username as $1. Write a guard clause that exits with a usage message if no argument was provided.
  6. What is the difference between [ ] and [[ ]]? Name one feature available in [[ ]] that is not in [ ].

Knowledge Check — Answers

  1. A successful command returns exit code 0. The if statement treats exit code 0 as true and executes the then block. Any non-zero exit code is treated as false and the then block is skipped.
  2. if [ -f /etc/resolv.conf ]; then
        echo "File found"
    else
        echo "File missing"
    fi
  3. Missing spaces inside the brackets. The correct form is if [ -d /tmp/work ]; then — a space is required after [ and before ].
  4. [[ "$USER" == "root" && -f /etc/shadow ]] — both conditions joined with && inside double brackets. Alternatively: [ "$USER" = "root" ] && [ -f /etc/shadow ]
  5. if [ -z "$1" ]; then
        echo "Usage: $0 username"
        exit 1
    fi
  6. [ ] is a POSIX-compatible synonym for test — it works in any shell but requires strict quoting of variables. [[ ]] is a bash built-in that handles unquoted variables safely and adds features such as pattern matching with ==, regex matching with =~, and the && and || logical operators inside the brackets.

Key Takeaways

  1. Exit code 0 means success (true); non-zero means failure (false). The if statement evaluates the exit code of any command. $? holds the exit code of the last command.
  2. The if block structure is: if COMMAND; then ... fi Every if ends with fi. Use elif for additional conditions. Use else for the fallback block.
  3. Use [ ] for POSIX compatibility; use [[ ]] for bash scripts. File tests (-f, -d, -e), string tests (-z, -n, =), and integer comparisons (-eq, -ne, -gt) work in both. Always quote variables inside [ ].
  4. Start scripts with guard clauses. Check for root ([ "$(id -u)" -ne 0 ]) and required arguments ([ -z "$1" ]) at the top. Fail fast with a clear message rather than failing partway through.

Graded Lab

  • Write a script that checks whether /etc/shadow is readable by the current user. Print an appropriate message for each outcome.
  • Write a script that accepts a filename as $1, exits with a usage message if no argument is given, and prints whether the file exists, is a regular file, is a directory, or is something else.
  • Write a script that checks whether the sshd service is active. If it is not, start it and print a message. If it is already running, say so.
  • Write a script with a root guard. If not root, print an error and exit 1. If root, create /srv/lab only if it does not already exist.
  • At the command line (not in a script), use && and || to create a user named labuser only if they do not already exist.
  • Run bash -n scriptname.sh on each script to check for syntax errors without executing it.
RHCSA Objective

"Conditionally execute code (use of: if, test, [], etc.)" Script tasks on the exam require if statements for safety checks, file tests, and service state validation. Guard clauses and file existence tests appear in nearly every script-writing task.