Conditions & Loops

Linux · Lesson 28 of 32 7 min read Updated

Scripts become useful when they can make decisions ("is the disk nearly full?") and repeat work ("for every log file, compress it"). In Bash that means if and case for decisions, and for and while for loops.

This lesson covers the test syntax that confuses many beginners, the most useful checks on strings, numbers and files, and the safe way to read a file line by line. It ends with a small but real monitoring script.

if, elif and else

An if runs a command and checks whether it succeeded. The most common command to test with is [[ ]], Bash's test syntax:

Terminal
marks=72
if [[ $marks -ge 90 ]]; then
    echo "Grade A"
elif [[ $marks -ge 60 ]]; then
    echo "Grade B"
else
    echo "Needs improvement"
fi
Output
Grade B

Note the details: spaces inside the brackets are required ([[ $marks -ge 90 ]], not [[$marks -ge 90]]), then starts the block, and fi (if backwards) ends it. You can put then on its own line instead of after ;. Indenting the body by four spaces is not required, but makes scripts readable.

Tests: strings, numbers and files

Inside [[ ]], these operators are the ones you will use most:

TestTrue when
$a == $b / $a != $bStrings are equal / different (== also matches patterns such as k*)
-z $a / -n $aString is empty / not empty
$a =~ regexString matches a regular expression
$x -eq $y, -ne, -lt, -le, -gt, -geNumber comparisons: equal, not equal, less than, and so on
-f path / -d path / -e pathIs a regular file / a directory / exists at all
-r path, -w path, -x pathReadable / writable / executable by you
-s pathFile exists and is not empty
!, &&, ||NOT, AND, OR

For numbers, (( )) is often clearer, because it uses normal maths symbols: (( count > 10 )).

Terminal
name="kabir"
[[ $name == k* ]] && echo "starts with k"
[[ -f orders.csv && -r orders.csv ]] && echo "orders.csv is a readable file"
[[ -d /nope ]] || echo "/nope is not a directory"
[[ "10" > "9" ]] && echo "text: 10 > 9" || echo "text: 10 < 9"
(( 10 > 9 )) && echo "number: 10 > 9"
Output
starts with k
orders.csv is a readable file
/nope is not a directory
text: 10 < 9
number: 10 > 9

The short form test && command runs the command only if the test succeeds; test || command runs it only if the test fails. Watch the fourth line: inside [[ ]], > compares text, character by character, so "10" sorts before "9". Use -gt or (( )) for numbers.

if works with any command, not just tests. grep -q (quiet) is a favourite, because it succeeds only when it finds a match:

Terminal
if grep -q "Kochi" orders.csv; then
    echo "We have customers in Kochi"
fi

case: choose between many values

When one value can match several patterns, case is clearer than a chain of elif:

Terminal
action="restart"
case "$action" in
    start|stop|restart) echo "Running: systemctl $action nginx" ;;
    status)             echo "Showing status" ;;
    *)                  echo "Unknown action: $action" ;;
esac
Output
Running: systemctl restart nginx

Each pattern ends with ), each branch ends with ;;, | separates alternatives, and * catches everything else. esac is case backwards. Patterns use wildcards, so *.jpg|*.png) matches image file names.

for loops

A for loop runs its body once for each item in a list. The list can be written out, come from brace expansion, or come from a wildcard:

Terminal
for city in Pune Jaipur Kochi; do
    echo "Shipping to $city"
done
for i in {1..3}; do printf "%s " "$i"; done; echo
for (( i = 10; i > 0; i -= 4 )); do printf "%s " "$i"; done; echo
for f in /var/log/*.log; do echo "log file: $f"; done
Output
Shipping to Pune
Shipping to Jaipur
Shipping to Kochi
1 2 3
10 6 2
log file: /var/log/alternatives.log
log file: /var/log/auth.log
log file: /var/log/dpkg.log

The third form is the C-style loop with a counter. Looping over a wildcard like *.log is the safe way to process files, because each name arrives as one item even if it contains spaces. Never loop over the output of ls.

while and until

A while loop repeats as long as its command succeeds; until repeats until it succeeds:

Terminal
count=3
while (( count > 0 )); do
    echo "T-minus $count"
    (( count-- ))
done
until ping -c 1 -W 1 192.168.1.1 > /dev/null; do sleep 5; done
Output
T-minus 3
T-minus 2
T-minus 1

The second loop waits, checking every five seconds, until the router answers. while true; do ...; done loops forever, which is common for simple watchers (stop with Ctrl+C).

Reading a file line by line

The standard pattern combines while with read. IFS=, splits each line on commas into the named variables, and -r keeps backslashes intact. continue skips to the next line and break leaves the loop:

Terminal
total=0
while IFS=, read -r id customer city item amount; do
    [[ $id == order_id ]] && continue
    (( total += amount ))
    if [[ $city == Lucknow ]]; then
        echo "Stopping at first Lucknow order ($customer)"
        break
    fi
done < orders.csv
echo "Total so far: $total"
Output
Stopping at first Lucknow order (Rohan)
Total so far: 2138

The file is fed into the loop with < orders.csv after done. This is better than cat orders.csv | while ..., because with a pipe the loop runs in a child shell and total would be lost when the loop ends.

A real script: disk space checker

Putting it together, this script warns when any filesystem is above 80% full. It reads df output line by line and uses parameter expansion to strip the % sign:

Terminal
#!/usr/bin/env bash
# diskcheck.sh: warn when any filesystem is getting full
limit=80
df -P -x tmpfs | tail -n +2 | while read -r fs size used avail pcent mount; do
    usage=${pcent%\%}
    if (( usage >= limit )); then
        echo "WARNING: $mount is ${usage}% full ($fs)"
    else
        echo "ok: $mount ${usage}%"
    fi
done
Output
ok: / 25%
ok: /boot 14%
ok: /boot/efi 6%
WARNING: /srv/data is 91% full (/dev/sdb)

df -P prints one line per filesystem in a fixed format that is easy to parse. Run it from cron or a systemd timer and send the output to a log, and you have basic monitoring.

Common mistakes

  • Missing spaces in tests. [[$a == $b]] is an error; write [[ $a == $b ]].
  • Comparing numbers with > or == inside [[ ]]. Those compare text. Use -gt, -eq or (( )).
  • Looping over $(ls). File names with spaces break apart. Use for f in *.txt.
  • Piping into while and expecting variables to survive. Use done < file instead.
  • Forgetting ;; in case. Bash reports a syntax error near the next pattern.

Key points

  • if cmd; then ...; elif ...; else ...; fi runs a branch based on whether a command succeeds.
  • [[ ]] tests strings, files and patterns; use -eq/-gt or (( )) for numbers.
  • case matches one value against several wildcard patterns.
  • for x in list, for f in *.log and for (( i=0; i<n; i++ )) repeat over items; while and until repeat on a condition.
  • Read files with while IFS= read -r line; do ...; done < file; use break and continue to control loops.

Exercise

Write a script cleanup.sh that loops over every .log file in a folder given in the variable dir (default /tmp/applogs). For each file: skip it if it is empty, print "large" if it is bigger than 1 MB (use stat -c %s file for the size in bytes), otherwise print "small". At the end print how many files were checked.

Show solution
Terminal
#!/usr/bin/env bash
# cleanup.sh: classify log files by size
dir="${dir:-/tmp/applogs}"
checked=0
for f in "$dir"/*.log; do
    [[ -e $f ]] || continue
    [[ -s $f ]] || { echo "skip (empty): $f"; continue; }
    size=$(stat -c %s "$f")
    if (( size > 1024 * 1024 )); then
        echo "large: $f ($size bytes)"
    else
        echo "small: $f ($size bytes)"
    fi
    (( checked++ ))
done
echo "Checked $checked file(s)"
Output
large: /tmp/applogs/api.log (2306112 bytes)
skip (empty): /tmp/applogs/cron.log
small: /tmp/applogs/web.log (48211 bytes)
Checked 2 file(s)

[[ -e $f ]] || continue handles a folder with no .log files, where the pattern would otherwise stay as the literal text *.log. -s is true only for non-empty files, and the empty file is skipped before it is counted.

Quick Quiz

1. Which test is true when report.txt exists and is a regular file?

2. Why does [[ "10" > "9" ]] evaluate as false?

3. What does ;; do in a case statement?

4. What is the safe way to process every .txt file in a folder?

Learn Linux with a mentor

Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call