Skip to content

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

backup.sh
#!/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_dir
Source directory doesn't exists: 'non_existent_dir'

And works if existing directory is provided to backup:

./backup.sh database_project
Checking 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.

Last updated on