Backup Script V1 First Try
This chapter starts a four-part project: one backup script, evolved across four chapters, the same way a real script actually matures in production — not written perfect the first time, but hardened deliberately, one concern at a time. This chapter builds v1: a genuinely working script with straightforward logic, and intentionally minimal safeguards. The next three chapters add error handling and logging, then safety (locking, validation, cleanup), then production polish (configuration, proper argument parsing, dry-run, notifications) — each one building directly on the exact script the previous chapter left off with.
What This Script Needs To Do
Back up a source directory’s contents into a timestamped destination directory under a backup root, so each run produces a new, distinctly-named backup rather than overwriting the last one.
Building v1
#!/usr/bin/env bash
set -euo pipefail
usage() {
printf '%s\n' \
"Usage: $0 <source>" \
"<source> source directory to backup"
}
if [[ $# -ne 1 ]]; then
usage
exit 1
fi
# ---------------------------
# Constants & Readonly Vars
# ---------------------------
SOURCE="$1"
BACKUP_ROOT="$HOME/backup"
SECTION_END="************************************************************************************************"
# ---------------------------
# User Defined Functions
# ---------------------------
# check if dir exists, takes 1st arg as dir name
dir_exists() {
local dir_name="$1"
[[ -d "$dir_name" ]] && return 0
}
# create dir (including missing parent), 1st arg is dir name
make_dir() {
local dir_name="$1"
mkdir -p "$dir_name" || return 1
}
# print check msg for dir, 1st arg is dir name, 2nd arg is identifier of dir
print_dir_check_msg_for () {
local dir_name="$1"
local dir_identifier="$2"
printf "Checking $dir_identifier directory: '$dir_name'\n"
}
# create dir only if it doesn't exists, takes 1st arg is dir name, 2nd arg is dir identifier to print
create_dir_if_not_exists() {
local dir_name="$1"
local dir_identifier="$2"
print_dir_check_msg_for "$dir_name" "$dir_identifier"
if ! dir_exists "$dir_name"; then
printf "Directory doesn't exists: '$dir_name', Creating...\n"
if ! make_dir "$dir_name"; then
printf "Creating directory failed: '$dir_name'\n"
return 1
else
printf "Directory created successfully: '$dir_name'\n"
fi
fi
}
# create backup, takes source and dest
create_backup() {
local source="$1"
local destination="$2"
printf "Backup process start: '$source' to '$destination'\n"
if ! cp -r "$source" "$destination"; then
printf "Either all or some files failed to backup, could be permission issues.\n"
return 1
fi
printf "Backup success: '$source' to '$destination'\n"
}
# just print section end, no args required
print_section_end() {
printf "\n$SECTION_END\n$SECTION_END\n\n"
}
# ---------------------------
# Driver Code
# ---------------------------
# exit if source doesn't exists
if ! dir_exists "$SOURCE"; then
printf "Source directory doesn't exists: '$SOURCE'\n"
exit
fi
# create if backup root does not exists, exit if creation failed
create_dir_if_not_exists "$BACKUP_ROOT" "backup root" || exit 1
print_section_end
# create destination, exit if failed
timestamp=$(date +%Y%m%d_%H%M%S)
destination="$BACKUP_ROOT/$SOURCE/$timestamp"
create_dir_if_not_exists "$destination" "destination" || exit 1
print_section_end
# create backup
create_backup "$SOURCE" "$destination"Walking Through The Script
timestamp=$(date +%Y%m%d_%H%M%S) uses command substitution to generate a sortable, collision-resistant timestamp — 20260823_140211, for instance. destination is built by simple string interpolation, combining $BACKUP_ROOT/$SOURCE with that timestamp so every run’s backup lands in its own uniquely-named directory rather than colliding with a previous run. mkdir creates that destination, and cp -r copies everything from source_dir into it.
The use of functions makes the code more cleaner and easy to read and understand. This is still a good working version 1 which performs intended task and handle most of the edge cases carefully. But evolution is a part of the process which is eventually needed now or then.
What v1 Gets Right
Every variable reference here is quoted, — this isn’t a beginner’s script ignoring fundamentals, it’s a working script that simply hasn’t yet been given any defensive architecture beyond that baseline. The timestamp-based naming is a genuinely sound choice: it avoids the single biggest naive-backup-script mistake (overwriting yesterday’s backup with today’s) without needing anything more sophisticated than date and string interpolation.
What v1 Deliberately Leaves Out
This is the roadmap for the rest of this project:
- No logging at all. If something fails for any reason, the script doesn’t even distinguish by exit codes. The next chapter builds proper error handling and a real logging function on top of this exact script.
- No protection against being run twice concurrently, no input validation, no safe temporary-resource handling, no guarantee of idempotency, and no cleanup if something fails partway through. The chapter after that adds all of it — locking, validation,
mktemp, and cleanup. - No configuration file, no real command-line argument parsing, no
--dry-run, and no notifications. The final chapter in this arc turns the hardcoded paths at the top of this script into proper, configurable options.
Testing And Summary
We’ve built a genuinely working backup script which denies if source doesn’t exist:
./backup.sh non_existent_dirSource directory doesn't exists: 'non_existent_dir'And works if existing directory is provided to backup:
./backup.sh database_projectChecking backup root directory: '/home/alice/backup'
Directory doesn't exists: '/home/alice/backup', Creating...
Directory created successfully: '/home/alice/backup'
************************************************************************************************
************************************************************************************************
Checking destination directory: '/home/alice/backup/database_project/20260830_212753'
Directory doesn't exists: '/home/alice/backup/database_project/20260830_212753', Creating...
Directory created successfully: '/home/alice/backup/database_project/20260830_212753'
************************************************************************************************
************************************************************************************************
Backup process start: 'database_project' to '/home/alice/backup/database_project/20260830_212753'The output structure doesn’t look that bad and you can see what’s actually happening. The next chapter takes this exact script and adds meaningful exit handling, a real logging function, and command-failure detection, etc.