Public beta. We're in our soft launch: the lessons are ready, but things may still change, and your feedback shapes what comes next.

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.

Send feedback

We're in a public beta, so every message helps. What's on your mind?

Please don't include your name, email or other personal details. We can't reply, but we read every one. We'll note which page you sent it from.