Skip to content

Intermediate · Automation basics: scheduling, archives and shell scripts · topic 38 of 57 · 35 min

Bash scripts: conditions, loops and safe habits

Explanation

if with [ ] or [[ ]] tests files, strings and numbers; case picks from several patterns. for loops over a list, and while read loops over the lines of a file. Functions group steps. Start scripts with set -euo pipefail so they stop at the first error instead of carrying on blindly.

if and tests

bash
#!/bin/bash
file="/etc/redhat-release"
if [ -f "$file" ]; then
    echo "This is a RHEL-family system"
elif [ -d /etc/apt ]; then
    echo "This looks like Debian or Ubuntu"
else
    echo "Unknown system"
fi
Spaces inside [ ] are required: [ -f "$file" ], not [-f "$file"].
TestTrue when
-f FILE / -d DIRa regular file / a directory exists
-e PATHanything exists at that path
-r / -w / -x FILEreadable / writable / executable by you
-z "$s" / -n "$s"string is empty / not empty
"$a" = "$b" / !=strings are equal / different
$a -eq $b, -ne, -lt, -le, -gt, -genumbers compare (equal, not equal, less than...)
! testthe test is false

[[ ]] is bash's improved test: it handles empty variables better, allows && and || inside, and supports pattern matching like [[ $host == web* ]]. In bash scripts [[ ]] is usually the safer choice; [ ] works in every POSIX shell. Any command can be the condition too: if grep -q asha /etc/passwd; then ... uses grep's exit code.

case: many choices

bash
case "$1" in
    start)   systemctl start httpd ;;
    stop)    systemctl stop httpd ;;
    status)  systemctl status httpd --no-pager ;;
    *)       echo "Usage: $0 {start|stop|status}" >&2; exit 2 ;;
esac

Loops

bash
# for: over a list
for server in web01 web02 db01; do
    ping -c1 -W1 "$server" > /dev/null && echo "$server up" || echo "$server DOWN"
done

# for: over numbers
for i in {1..5}; do
    useradd "trainee$i"
done

# while read: over the lines of a file (one username per line)
while read -r user; do
    id "$user" &> /dev/null || useradd "$user"
done < /root/new-users.txt
while read -r reads one line at a time; -r keeps backslashes as they are.

Avoid for line in $(cat file) for files: it splits on every space, not on lines. Use while read -r.

Functions

bash
log() {
    echo "$(date '+%F %T') $*" >> /var/log/backup.log
}

log "Backup started"
tar -czf "/backup/www-$(date +%F).tar.gz" /var/www || { log "tar failed"; exit 1; }
log "Backup finished"
A function is a named group of commands; $1, $2 and $* inside it are the function's own arguments.

Safe habits: set -euo pipefail

  • set -e: stop the script when a command fails (instead of carrying on with half the work done).
  • set -u: treat an unset variable as an error. This catches typos like $BACKUP_DRI that would otherwise be empty.
  • set -o pipefail: a pipeline fails if ANY part fails, not just the last command.
Careful: Why it matters: without set -u, rm -rf "$TARGET/" with a misspelled or empty TARGET becomes rm -rf "/". Real companies have lost servers this way. Use set -euo pipefail, quote variables, and check values before deleting anything.
bash
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backup"
[ -d "$BACKUP_DIR" ] || { echo "No $BACKUP_DIR" >&2; exit 1; }
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +14 -print -delete

Common mistakes

  • Missing spaces in tests: if [$a -eq 1] fails with "command not found".
  • Using = for numbers or -eq for text: [ "$a" -eq "yes" ] is an error. Use = for strings, -eq for numbers.
  • Forgetting ;; at the end of each case branch, or fi / done / esac at the end of a block.
  • Writing scripts that only work when run from one directory. Use absolute paths.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Bash scripts: conditions, loops and safe habits · 1/6

if [ -f /etc/redhat-release ]; then
    echo "RHEL family"
fi

if runs commands only when a test is true. Put spaces inside the brackets.

Prefer a video by a teacher? Find free videos on “bash if for while loop case functions set -euo pipefail tutorial” on YouTube ↗

In real life: A morning health check at a hospital IT team

The IT team of a hospital in Hyderabad runs a script every morning at 7:30 from a timer. It loops over a list of servers, checks that each answers ping and that its disk is below 85%, and writes one line per server to a log. If any check fails, the script exits with 1 and the timer's failure shows up in the monitoring dashboard before doctors start their rounds.

bash
#!/bin/bash
set -euo pipefail
status=0
while read -r host; do
    if ! ping -c1 -W2 "$host" &> /dev/null; then
        echo "$host DOWN"; status=1; continue
    fi
    use=$(ssh "$host" df --output=pcent / | tail -1 | tr -dc '0-9')
    if [ "$use" -ge 85 ]; then echo "$host disk $use%"; status=1; else echo "$host ok"; fi
done < /etc/healthcheck/hosts.txt
exit "$status"

Practice (0/4 done)

Task 1 · Question

Which test is true when /backup is a directory?

Task 2 · Question

Which line has the bug? 1: count=5 2: if [$count -gt 3]; then 3: echo "many" 4: fi

Task 3 · Code · Python

A script reads wanted usernames from a file and creates the missing ones. Model it: write plan_users(existing, wanted_text) where existing is a set of usernames and wanted_text is the file's text (one name per line; skip blank lines and lines starting with #; strip spaces). Return the list of commands the script would run, in file order, without duplicates: "useradd NAME" for each missing user.

Task 4 · Practice

Write /usr/local/bin/backup-www.sh that: uses set -euo pipefail; takes an optional destination directory (default /backup); creates it if missing; makes /DEST/www-YYYY-MM-DD.tar.gz of /var/www; deletes archives older than 7 days there; logs each step with a log function; and exits 0 on success. Run it twice and check the log.

Quick quiz

Question 1

1.Which loop reads a file line by line correctly?

Question 2

2.What does set -u do?

Question 3

3.Which compares two NUMBERS for equality?

Question 4

4.What ends a case branch?

Question 5

5.Why use set -o pipefail?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which loop reads a file line by line correctly?

Answer: while read -r l; do ... done < f

while read -r handles lines with spaces.

What does set -u do?

Answer: Treats unset variables as errors

It catches typos in variable names.

Which compares two NUMBERS for equality?

Answer: [ "$a" -eq "$b" ]

-eq is numeric; = is for strings.

What ends a case branch?

Answer: ;;

;; ends a branch; esac ends the whole case.

Why use set -o pipefail?

Answer: So a failure in any part of a pipeline makes the pipeline fail

Otherwise only the last command's status counts.

What does set -o pipefail change?Related

Answer: A pipeline fails if any command in it fails, not just the last one

Without pipefail, pg_dump failing in "pg_dump | gzip" would be hidden because gzip succeeded.

From Bash scripting, text tools and cron

What does $(hostname) do inside a script?Related

Answer: Runs hostname and puts its output there

Command substitution.

From Bash scripts: variables, quoting, arguments and exit codes

Which loop is best to visit every item of an array?Related

Answer: for...of

for...of gives you each item in turn, with no index to manage.

From Loops: repeating work

When does Python check the condition of a while loop?Related

Answer: Before every round of the loop

The condition is checked before each round. When it becomes False, the loop stops.

From while loops

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Automation basics: scheduling, archives and shell scripts”. The module exam comes next.
  2. Module 7 exam: Automation basics · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Boot, recovery and containers. 57 topics left in the course before the final exam.

← Previous: Bash scripts: variables, quoting, arguments and exit codes