Skip to main content
Back to module
Linux Administration · Automation and Access

Bash scripting

Reading1h 20mLesson 2 of 3 in this module

What you'll learn

  • Write a backup script with checks
  • Start scripts with a shebang and set -euo pipefail
  • Check arguments and paths with [[ ]] tests and exit non-zero with a message on failure
  • Verify a script's result and test a restore

Write a bash backup script that checks its inputs, stops on errors and verifies its own result.

Why it matters

Anything you type more than twice is a candidate for a script. Administrators script backups, user setup, log clean-up, health checks and deployments. A good script does the same thing the same way every time, which is more reliable than a person following notes at 2 a.m. A bad script, one that keeps going after an error, can do real damage quickly. This lesson focuses on writing scripts that check before they act.

Anatomy of a script

#!/usr/bin/env bash
# backup.sh: archive a folder into a dated tar.gz
set -euo pipefail

echo "Hello from $0"
  • The first line, the shebang, tells the system which interpreter runs the file.
  • set -e stops the script when a command fails. set -u treats unset variables as errors (so a typo cannot expand to an empty string). set -o pipefail makes a pipeline fail if any command in it fails.
  • Make it executable and run it: chmod u+x backup.sh, then ./backup.sh.

The building blocks

Variables and quoting

name="web logs"
echo "Backing up $name"          # double quotes expand variables
echo 'Literal $name'             # single quotes do not
today=$(date +%F)                # command substitution: 2026-10-23

No spaces around =. Always quote variables ("$file"), or a value containing a space becomes two arguments.

Arguments

$1, $2... are the script's arguments, $# is how many there are, and "$@" is all of them.

src="${1:-}"                     # first argument, or empty if missing
dest="${2:-$HOME/backups}"       # default value when not given

Tests and if

if [[ -d "$src" ]]; then echo "folder exists"; fi
if [[ ! -r "$src" ]]; then echo "cannot read $src" >&2; exit 1; fi

Useful tests: -d directory, -f regular file, -e exists, -r readable, -w writable, -z empty string, -n non-empty, and -eq/-lt/-gt for numbers. >&2 sends a message to standard error, where error messages belong.

Exit codes

Every command returns an exit code: 0 means success, anything else means failure. $? holds the last one. Your scripts should exit 0 on success and a non-zero code on failure, so other tools (and cron, next lesson) can tell what happened.

Loops and functions

for f in "$dest"/*.tar.gz; do
  echo "found $f"
done

log() { echo "$(date '+%F %T') $*"; }
log "starting"

Worked example: a backup script with checks

#!/usr/bin/env bash
# backup.sh SRC [DEST] - archive SRC into DEST/name-YYYY-mm-dd-HHMMSS.tar.gz
set -euo pipefail

log() { echo "$(date '+%F %T') $*"; }
die() { echo "ERROR: $*" >&2; exit 1; }

src="${1:-}"
dest="${2:-$HOME/backups}"
keep=7

[[ -n "$src" ]]  || die "usage: $0 SRC [DEST]"
[[ -d "$src" ]]  || die "source folder not found: $src"
[[ -r "$src" ]]  || die "cannot read: $src"
mkdir -p "$dest"
[[ -w "$dest" ]] || die "cannot write to: $dest"

name="$(basename "$src")-$(date +%F-%H%M%S).tar.gz"
archive="$dest/$name"

log "archiving $src to $archive"
tar -czf "$archive" -C "$(dirname "$src")" "$(basename "$src")"

# verify: the archive must exist, be non-empty and be readable by tar
[[ -s "$archive" ]] || die "archive is empty: $archive"
tar -tzf "$archive" > /dev/null || die "archive is corrupt: $archive"

# keep only the newest $keep archives for this source
ls -1t "$dest"/"$(basename "$src")"-*.tar.gz | tail -n +$((keep + 1)) | xargs -r rm --
log "done: $(du -h "$archive" | cut -f1)"

What makes it trustworthy:

  1. Inputs are checked before anything happens, with clear messages and a non-zero exit.
  2. set -euo pipefail stops on the first unexpected failure.
  3. The result is verified: the archive exists, is not empty and can be listed by tar. A backup you have not tested restoring is only a hope.
  4. Deletion is narrow: it only removes old archives matching this source's name pattern, in the destination folder.

Run it:

./backup.sh ~/projects ~/backups
./backup.sh /does/not/exist ; echo "exit code: $?"    # ERROR ... exit code: 1

Testing a restore

mkdir -p /tmp/restore-test
tar -xzf ~/backups/projects-2026-10-23-104500.tar.gz -C /tmp/restore-test
diff -r ~/projects /tmp/restore-test/projects && echo "restore matches"

Check your script with ShellCheck

shellcheck (sudo apt install shellcheck) reads a script and points out unquoted variables, common traps and portability problems. Run shellcheck backup.sh before you trust any script.

Common mistakes

  • Unquoted variables, which break on spaces and can turn rm -r $dir/ into rm -r / if $dir is empty. Quote everything and use set -u.
  • No checks before destructive commands such as rm, mv or overwriting with >.
  • Relative paths in scripts that later run from a different folder. Use absolute paths or paths built from arguments.
  • Ignoring exit codes, so a failed backup still prints "done".
  • Backups stored on the same disk as the data in real life. For practice it is fine; at work, copy them elsewhere.

Recap

  • Start scripts with a shebang and set -euo pipefail.
  • Quote variables, check arguments with [[ ... ]] tests, and send errors to stderr with a non-zero exit.
  • Verify what the script produced, and test restores.
  • Run shellcheck on everything you write.

Try it

The Do tab has you write, break and verify your own backup script on your practice system.

Key terms (6)

shebang
The first line of a script, such as #!/usr/bin/env bash, that tells the system which interpreter runs it.
set -e
A bash option that stops the script as soon as a command fails.
pipefail
A bash option that makes a pipeline fail if any command in it fails, not just the last one.
exit code
The number a command returns: 0 for success, anything else for failure; $? holds the last one.
command substitution
Running a command inside $(...) and using its output as a value, such as $(date +%F).
ShellCheck
A tool that reads shell scripts and points out unquoted variables, common traps and portability problems.

Recap

  • Start scripts with a shebang and set -euo pipefail.
  • Always quote variables, check arguments with [[ ... ]] tests, and send errors to stderr with a non-zero exit.
  • Check inputs before destructive commands and keep deletions narrow.
  • Verify what the script produced and test restores.
  • Run shellcheck on every script you write.
Also in this lesson: