Lesson 02 of 8 · Modules
Conditions & loops
Make scripts decide and repeat: if with [[ ]], file and string tests, case, for and while loops, reading files line by line safely, arithmetic, and arrays.
Deciding with if
#!/usr/bin/env bash
config=/etc/myapp/config.yaml
if [[ -f "$config" ]]; then
echo "Using $config"
elif [[ -d /etc/myapp ]]; then
echo "Directory exists but no config: creating a default"
else
echo "No /etc/myapp at all" >&2 # >&2 sends the message to stderr
exit 1
fi
if is a fork in the road: if it's raining, take an umbrella, otherwise wear sunglasses. A loop is doing the same chore for every item on a list: for each plate on the table, wash it.
if runs a command and checks its exit status. [[ … ]] is a command built for tests, but any command works:
if grep -q "^deploy:" /etc/passwd; then
echo "user deploy exists"
fi
if systemctl is-active --quiet nginx; then
echo "nginx is up"
fi
Common tests
| Test | True when |
|---|---|
-f path / -d path / -e path |
regular file / directory / anything exists |
-r / -w / -x path |
readable / writable / executable |
-z "$s" / -n "$s" |
string empty / not empty |
"$a" == "$b" / != |
strings equal / differ |
$s == web-* |
matches a glob pattern (inside [[ ]]) |
$s =~ ^[0-9]+$ |
matches a regular expression |
(( n > 10 )) |
arithmetic comparison |
Combine with &&, || and !: [[ -f "$f" && -r "$f" ]].
case: many choices
case "$1" in
start) systemctl start myapp ;;
stop) systemctl stop myapp ;;
restart) systemctl restart myapp ;;
*) echo "usage: $0 {start|stop|restart}" >&2; exit 2 ;;
esac
for loops
for host in web-01 web-02 web-03; do
echo "== $host"
ssh "$host" uptime
done
for f in /var/log/nginx/*.log; do
[[ -e "$f" ]] || continue # skip if the glob matched nothing
echo "$f: $(wc -l < "$f") lines"
done
for i in {1..5}; do echo "attempt $i"; done
Reading files line by line
while IFS= read -r host; do
[[ -z "$host" || "$host" == \#* ]] && continue # skip blanks and comments
echo "checking $host"
done < hosts.txt
Don't loop over $(cat file)
for line in $(cat hosts.txt) splits on every space and expands *. Use while IFS= read -r for lines.
Arithmetic
count=0
count=$((count + 1))
(( count++ ))
if (( count >= 3 )); then echo "three or more"; fi
echo "$(( 17 / 5 )) remainder $(( 17 % 5 ))" # 3 remainder 2 (integers only)
Arrays
servers=(web-01 web-02 db-01)
echo "first: ${servers[0]}, count: ${#servers[@]}"
servers+=(cache-01)
for s in "${servers[@]}"; do echo "$s"; done
Always loop with "${array[@]}" (quoted) so elements containing spaces stay whole.
Try it: a service checker
Create services.txt with one service per line (ssh, cron, nginx, a comment line and a blank line). Write check.sh that:
- Reads the file with
while IFS= read -r, skipping blanks and comments. - For each service prints
OKorDOWNusingsystemctl is-active --quiet. - Counts the DOWN services and exits with status 1 if any were down (0 otherwise).
- Accepts an optional argument
--quiet(usecase) that prints only the DOWN ones.
Going deeper: loop subtleties
- A
while readloop at the end of a pipe runs in a subshell, so variables set inside are lost after it:cmd | while read …→ preferwhile read …; done < <(cmd)(process substitution). [[ ]]is Bash;[ ]is POSIXtest, with more quoting pitfalls. Use[[ ]]in Bash scripts.shopt -s nullglobmakes non-matching globs expand to nothing instead of the literal pattern.- For parallel loops over many hosts,
xargs -Por GNUparallelbeat backgrounding with&by hand.
Recap
ifchecks a command's exit status;[[ … ]]provides file, string and pattern tests;(( … ))does arithmetic.casefor multiple choices;forover words, globs and ranges.- Read lines with
while IFS= read -r line; do …; done < file. - Arrays:
arr=(…),"${arr[@]}",${#arr[@]}.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.