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 withif,&&/||andcase - Loops:
for,while, reading a file line by line,breakandcontinue - Functions,
local, and debugging withbash -xandset -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"
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 script | Means |
|---|---|
$0 | the 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 |
shift | drop $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.
| Files | Text | Numbers |
|---|---|---|
-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!) |
[ -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
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"
ficase compares one value against patterns. It's perfect for making one script work on 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 ;;
esacEvery if ends with fi, every case with esac (the words backwards), and each case branch ends with ;;.
Loops
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
breakleaves the loop, andcontinueskips to the next round.- Bash also has
for i in {1..5}andfor ((i=0; i<5; i++)). In plainsh, usefor i in $(seq 5)or awhileloop. - That
while readloop takes its input withdone < file. If you writecat file | while read …instead, the loop runs in a subshell, and any variables it sets vanish afterwards (lesson 12). IFS=:tellsreadto split the line at colons, like the fields of/etc/passwd.
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
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 commandA function gets its own $1, $2… and return sets its exit code, just like exit does for a script.
Debugging and strict mode
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:
-e: stop at the first command that fails, instead of carrying on and doing damage.-u: using a variable that was never set is an error, which catches typos like$UESR.-o pipefail: a pipe counts as failed if any part failed, not just the last command.
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?
✓ Bash looked for a command literally named [5.
2. Your script needs to install a package on both Rocky and Ubuntu. What's the cleanest way?
✓ The same trick real installers use.
3. What does set -e do?
✓ -x is the one that prints commands.
4. Why write "$@" with quotes when passing on all the arguments?
✓ Unquoted $@ splits at every space.