How to Write a Bash Script (Variables, Ifs, Loops and the Mistakes Everyone Makes)

Write your first bash script: Typed it twice? Make it a file.

Any sequence of commands you've typed twice should be a script. A bash script is just those commands in a file, with a few extras — variables, conditions, loops — that let it handle more than one situation. This covers everything you need for the scripts a home server runs: backups, cleanups, checks and alerts.

Quick reference

ThingSyntax
First line#!/bin/bash
Stop on first errorset -e
Variablename="value" then "$name"
Command output into a variabletoday=$(date +%F)
Ifif [ -f "$file" ]; then ... fi
Loop over filesfor f in /var/log/*.log; do ... done
Arguments$1, $2, all: "$@"
Run itchmod +x script.sh then ./script.sh

1. The smallest script

nano hello.sh

#!/bin/bash
echo "Hello from $(hostname) on $(date)"

chmod +x hello.sh
./hello.sh

The first line tells Linux which program runs the file. $(...) runs a command and drops its output in place.

2. Variables

backup_dir="/mnt/backup"
today=$(date +%F)
echo "Backing up to $backup_dir/$today"

No spaces around =. Always put "$variable" in double quotes — otherwise a value with a space in it becomes two arguments and things break in confusing ways.

3. Stop when something fails

#!/bin/bash
set -euo pipefail

Put this second line in every script. -e exits on the first failing command (so a failed cd doesn't lead to rm -r in the wrong folder), -u errors on undefined variables (a typo in a name), -o pipefail makes a pipeline fail if any part fails.

4. If statements

if [ -f "/var/run/reboot-required" ]; then
echo "Reboot needed"
fi

if [ "$used" -gt 85 ]; then
echo "Disk is ${used}% full" | mail -s "Disk warning" you@example.com
else
echo "Disk fine"
fi

Common tests: -f file exists, -d folder exists, -z "$s" string is empty, "$a" = "$b" strings equal, "$n" -gt 5 number greater than. Spaces inside the brackets are required.

Test a command instead: if ping -c1 -W1 192.168.1.1 >/dev/null; then echo "router up"; fi.

5. Loops

for f in /home/john/photos/*.jpg; do
echo "Processing $f"
done

for host in 192.168.1.1 192.168.1.50 192.168.1.60; do
ping -c1 -W1 "$host" >/dev/null && echo "$host up" || echo "$host DOWN"
done

while read -r line; do
echo "Line: $line"
done < list.txt

6. Arguments

#!/bin/bash
set -euo pipefail
target="${1:-/mnt/backup}" # first argument, or a default
echo "Target is $target"

./backup.sh /mnt/usb uses the argument; ./backup.sh uses the default. $# is the number of arguments; "$@" is all of them.

7. A real one

#!/bin/bash
# Warn if any Docker container isn't running
set -euo pipefail
down=$(docker ps -a --filter "status=exited" --format "{{.Names}}")
if [ -n "$down" ]; then
echo -e "Stopped containers on $(hostname):\n$down" | mail -s "Containers down" you@example.com
fi

Save it in /usr/local/bin/, chmod +x, add to cron.

The mistakes everyone makes

Checking a script before running it

Install shellcheck (sudo apt install shellcheck) and run shellcheck script.sh. It finds unquoted variables and dozens of other classic bugs with clear explanations.

Troubleshooting

"Permission denied". chmod +x. "command not found" for a variable. Spaces around =. "unary operator expected". An empty, unquoted variable in [ ]. Quote it. Runs by hand, not in cron. Full paths (tutorial 20).

Wrapping up

Shebang, set -euo pipefail, quoted variables, if and for, shellcheck before cron. That's enough to automate everything a home server needs, and enough to read other people's scripts before running them.

Call us