Linux Sysadmin · Lesson 13 · 30 min

Shell scripting: if, loops & functions

A script is just the commands you already know, saved in a file, plus a few words that let it decide (if), repeat (for, while) and organize (functions). With those, you can turn a 20-minute chore into one command, or into a cron job that nobody has to remember.

You will learn

  • Script basics: the shebang, chmod +x, arguments $1 $# "$@", and exit codes
  • Tests with [ ], and decisions with if, &&/|| and case
  • Loops: for, while, reading a file line by line, break and continue
  • Functions, local, and debugging with bash -x and set -euo pipefail

Anatomy of a script

#!/bin/bash                        # 1. the shebang: which program runs this file
# checkdisk.sh - warn when the disk is getting full
set -euo pipefail                  # 2. strict mode (explained below)

limit=${1:-80}                     # 3. first argument, or 80 if none was given
used=$(df / | awk 'NR==2 {print $5}' | tr -d '%')

if [ "$used" -ge "$limit" ]; then  # 4. decide
  echo "WARNING: / is ${used}% full (limit ${limit}%)" >&2
  exit 1                           # 5. exit code: 0 = OK, anything else = problem
fi
echo "OK: / is ${used}% full"
Same on both
chmod +x checkdisk.sh      # allow it to run (see the permissions lesson)
./checkdisk.sh             # run it:  OK: / is 13% full
./checkdisk.sh 10; echo $? # with an argument: WARNING… and exit code 1

Create the file with an editor, or with a quoted here-document so the $ signs stay as typed: cat > checkdisk.sh <<'EOF' … EOF (lesson 11).

Inside a scriptMeans
$0the script's own name
$1, $2 …the first, second… argument
$#how many arguments
"$@"all arguments, each kept as a separate word (always quote it!)
$?the exit code of the last command
shiftdrop $1 and move the others down by one

Tests: true or false?

[ … ] (the same as the test command) checks a condition and answers with an exit code: 0 for true, 1 for false. The spaces inside the brackets are required.

FilesTextNumbers
-e exists
-f is a file
-d is a folder
-r -w -x can read / write / run
-s not empty
"$a" = "$b" same
"$a" != "$b" different
-z "$a" empty
-n "$a" not empty
-eq equal · -ne not equal
-lt -le less (or equal)
-gt -ge greater (or equal)
(< and > are redirects here!)
Same on both
[ -f /etc/passwd ] && echo "exists"          # && = only if it succeeded
[ -d /backup ] || mkdir /backup               # || = only if it failed
test 5 -gt 3; echo $?                         # 0 (true)
[ "$USER" = "root" ] || echo "run me with sudo" >&2

Always quote variables inside [ ]. If $name is empty, [ $name = x ] turns into [ = x ] and fails with an error. Bash also has [[ … ]], which is more forgiving and understands patterns ([[ $f == *.txt ]]), but it isn't available in plain sh (lesson 14).

Decisions: if and case

Same on both
if [ -d /var/log ]; then
  echo "logs are here"
elif [ -d /logs ]; then
  echo "odd place for logs"
else
  echo "no logs?!"
fi

if grep -q student /etc/passwd; then    # ANY command can be the test: its exit code decides
  echo "student exists"
fi

case compares one value against patterns. It's perfect for making one script work on both families:

Same on both: one script, both families
. /etc/os-release          # loads ID=rocky or ID=ubuntu (see the first lesson)
case "$ID" in
  rocky|rhel|almalinux|centos)  sudo dnf install -y tree ;;
  ubuntu|debian|linuxmint)      sudo apt install -y tree ;;
  *)                            echo "Unknown distro: $ID" >&2; exit 1 ;;
esac

Every if ends with fi, every case with esac (the words backwards), and each case branch ends with ;;.

Loops

Same on both
for name in Ana Ben Chloe; do        # over a list of words
  echo "Hello, $name"
done

for f in /etc/*.conf; do             # over files: use a glob, never $(ls)
  echo "$f has $(wc -l < "$f") lines"
done

n=1
while [ $n -le 3 ]; do               # while the test is true
  echo "round $n"
  n=$((n + 1))
done

while IFS=: read -r user x uid rest; do     # a file, line by line
  [ "$uid" -ge 1000 ] && echo "real user: $user"
done < /etc/passwd
Loops at the prompt

You don't need a file to use a loop. Type for i in 1 2 3 and press Enter. The prompt changes to > because bash knows you're not finished. Keep typing do, then the body, then done. Or write it all on one line: for i in 1 2 3; do echo $i; done.

Functions

Same on both
log() {                              # define it once…
  echo "$(date +%T) $*"
}
backup_folder() {
  local src="$1" dest="$2"           # local = only exists inside the function
  [ -d "$src" ] || { log "no such folder: $src"; return 1; }
  tar -czf "$dest" "$src" && log "saved $src to $dest"
}
backup_folder Documents docs.tar.gz  # …use it like a command

A function gets its own $1, $2… and return sets its exit code, just like exit does for a script.

Debugging and strict mode

Same on both
bash -x checkdisk.sh 10     # show each line (with variables filled in) before it runs
+ limit=10
++ df / | awk 'NR==2 {print $5}' | tr -d %
+ used=13
+ '[' 13 -ge 10 ']'

Many admins start every bash script with set -euo pipefail:

The classic scary bug

rm -rf "$BACKUP_DIR/"* with a misspelled or empty variable becomes rm -rf /*. set -u turns that into an error instead of a disaster. Quote your variables, use set -u, and check scripts with shellcheck (lesson 14).

Where do scripts live?

Personal helpers go in ~/bin, tools for everyone in /usr/local/bin (Linux Basics, lesson 7), and scheduled jobs are called by cron with their full path (lesson 5). Start them with #!/bin/bash if they use bash features, because on Ubuntu #!/bin/sh means dash.

Try it: write real scripts ✍️

The first missions are one-liners at the prompt. Then you'll write checkdisk.sh with a here-document (copy it from the top of the page, line by line) or with nano.

Quick check

1. [$n -gt 5] gives “command not found”. What's wrong?

2. Your script needs to install a package on both Rocky and Ubuntu. What's the cleanest way?

3. What does set -e do?

4. Why write "$@" with quotes when passing on all the arguments?

Finished the missions and the quiz? Mark it done to track your progress.