Bash Scripting Series — Level 3

 

Bash Scripting Series — Level 3: Creating Automated Directory Backups

Introduction

In Level 2, we learned how to validate user input and check whether a directory exists. Now, we will combine those concepts to create a practical backup script.

Backups are an essential part of system administration. They help preserve files before system changes, support recovery after accidental deletion, and provide copies of important data.

In this lesson, we will create a Bash script that asks the user for a directory, validates the path, creates a backup directory, generates a timestamp, and compresses the selected directory into a .tar.gz archive.

We will also identify a few issues in the original script and improve its error handling.

1. The Original Script

#!/bin/bash

# TODO: Prompt the user to enter a directory path
read -p "Enter a directory path to continue: " dir

# TODO: Check if the user provided input
if [ -z "$dir" ]; then
    echo "No directory path provided."
    exit 1
fi

# TODO: Check if the directory exists
if [[ ! -d "$dir" ]]; then
    echo "Directory path does not exist."
    read -p "Enter a directory path to continue: " dir
else
    echo "Directory path exists"
fi

BACKUP_DIR="./backups"

# TODO: Create a directory at the above path
mkdir -p "$BACKUP_DIR"

backup="backup"

# Generate a timestamp and backup filename
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/$backup_${TIMESTAMP}.tar.gz"

# TODO: Create the compressed backup
tar -czf "$BACKUP_FILE" "$dir"

# TODO: Confirm success
if [ $? -eq 0 ]; then
    echo "Backup created successfully: $BACKUP_FILE"
else
    echo "Backup creation failed."
fi

What Does the Script Do?

The script follows this process:

  1. Requests a directory path.

  2. Checks whether the user entered anything.

  3. Checks whether the directory exists.

  4. Creates a directory named backups.

  5. Generates a timestamp for the backup filename.

  6. Compresses the selected directory.

  7. Reports whether the backup command succeeded.

There is one important problem: if the directory does not exist, the script asks for another path but never validates the new input again. It may therefore continue to the backup operation with an invalid path.

We will examine that problem and fix it later.

2. Collecting and Validating User Input

The script begins with:

read -p "Enter a directory path to continue: " dir

The read command collects user input, and -p displays the prompt. The supplied path is stored in the variable dir.

For example, the user might enter:

/home/kali/Documents

The next condition checks whether the user entered anything:

if [ -z "$dir" ]; then
    echo "No directory path provided."
    exit 1
fi

The -z test checks whether a string is empty. If the user presses Enter without typing a path, the condition succeeds, an error message appears, and exit 1 terminates the script.

The next condition checks the directory:

if [[ ! -d "$dir" ]]; then

Here:

  • -d checks whether the path identifies an existing directory.

  • ! reverses the condition.

  • "$dir" supplies the path stored in the variable.

Therefore, the condition means: if the supplied path is not an existing directory, execute the following commands.

The original script asks the user to enter a new path inside this condition. However, because it does not validate that second response, the input-handling logic is incomplete.

3. Creating the Backup Directory

Next, the script defines a destination for backup files:

BACKUP_DIR="./backups"

BACKUP_DIR is a variable containing the relative path ./backups.

The ./ means the current working directory. For example, if you run the script from /home/kali, the backup directory will be /home/kali/backups.

The following command creates the directory:

mkdir -p "$BACKUP_DIR"

mkdir creates directories, while -p also creates missing parent directories and avoids an error simply because the target directory already exists as a directory.

This allows the script to reuse the same backup destination each time it runs.

One improvement is needed here: the original script does not check whether mkdir succeeds. If directory creation fails because of permissions or a filesystem problem, the backup operation will probably fail too.

A more reliable script should check the result and stop if the destination cannot be prepared.

4. Generating a Timestamp

A backup should have a useful filename so that you can distinguish it from previous backups.

The script defines a filename prefix:

backup="backup"

It then generates a timestamp:

TIMESTAMP=$(date +"%Y%m%d_%H%M%S")

The date command retrieves the current date and time. The formatting string specifies how that information should be displayed.

FormatMeaning
%YFour-digit year
%mTwo-digit month
%dTwo-digit day
%HHour in 24-hour format
%MMinutes
%SSeconds

For example, a timestamp might look like this:

20261011_143025

This represents October 11, 2026, at 14:30:25.

The expression $(...) is called command substitution. It runs the command inside the parentheses and substitutes its output into the surrounding expression. Here, the resulting timestamp is stored in TIMESTAMP.

5. Building the Backup Filename

The script combines the destination, prefix, timestamp, and archive extension:

BACKUP_FILE="${BACKUP_DIR}/$backup_${TIMESTAMP}.tar.gz"

There is a subtle variable-expansion issue in the original line.

Bash variable names can contain letters, numbers, and underscores, so $backup_ is interpreted as a variable named backup_, not the variable backup followed by an underscore.

The original line may therefore generate an unexpected filename.

The corrected version uses braces to make the variable boundary clear:

BACKUP_FILE="${BACKUP_DIR}/${backup}_${TIMESTAMP}.tar.gz"

For example, the result might be:

./backups/backup_20261011_143025.tar.gz

The .tar.gz extension indicates a TAR archive compressed using gzip.

Because the timestamp only includes seconds, two runs using the same destination within the same second can generate the same filename. For basic practice, this is unlikely to be a problem, but production backup scripts should account for possible filename collisions.

6. Understanding TAR and GZIP

The original script creates the archive using:

tar -czf "$BACKUP_FILE" "$dir"

The tar command groups files and directories into an archive. With the options used here, it also compresses the archive using gzip.

OptionPurpose
-cCreate a new archive
-zCompress the archive using gzip
-fSpecify the archive filename

The variable "$BACKUP_FILE" supplies the destination filename, and "$dir" identifies the directory to archive.

For example, if the directory is /home/kali/Documents, the command attempts to package its contents and directory structure into the timestamped archive.

The quotes preserve paths containing spaces.

Important: TAR archives files and directory structures, while gzip compresses the resulting archive. A .tar.gz file combines both operations.

Depending on the path supplied, TAR may display a warning about removing a leading slash from an absolute path. This is normal behavior designed to avoid creating absolute filesystem paths when extracting the archive.

Another consideration is the backup destination. If you select a directory that contains the backups folder, previous backups or files being written to that location may be included in the source. For this exercise, keep the backup destination outside the directory being backed up.

7. Checking Whether the Backup Succeeded

The original script finishes with:

if [ $? -eq 0 ]; then
    echo "Backup created successfully: $BACKUP_FILE"
else
    echo "Backup creation failed."
fi

The special variable $? contains the exit status of the most recently executed command.

The -eq operator compares integer values. Therefore, [ $? -eq 0 ] checks whether the previous command returned status 0.

If TAR succeeds, the script prints the archive's filename. Otherwise, it displays a failure message.

This works in the original script because the condition immediately follows the TAR command. However, placing the command directly in the if statement is clearer:

if tar -czf "$BACKUP_FILE" "$dir"; then
    echo "Backup created successfully: $BACKUP_FILE"
else
    echo "Backup creation failed."
fi

Bash checks the command's exit status automatically. This avoids having to inspect $? separately.

8. An Improved Version of the Script

The following version addresses the input-validation problem, corrects the filename construction, checks whether the backup directory can be created, and handles TAR failures.

#!/bin/bash

# Ask for a directory until a valid path is provided
while true; do
    read -r -p "Enter a directory path to back up: " dir

    if [[ -z "$dir" ]]; then
        echo "Error: No directory path provided."
        continue
    fi

    if [[ -d "$dir" ]]; then
        echo "Directory path exists: $dir"
        break
    fi

    echo "Error: Directory does not exist. Please try again."
done

# Define the backup destination
BACKUP_DIR="./backups"

# Create the backup directory
if ! mkdir -p -- "$BACKUP_DIR"; then
    echo "Error: Could not create the backup directory."
    exit 1
fi

# Generate a timestamp and backup filename
backup="backup"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/${backup}_${TIMESTAMP}.tar.gz"

# Create the compressed backup
if tar -czf "$BACKUP_FILE" -C "$(dirname "$dir")" "$(basename "$dir")"; then
    echo "Backup created successfully: $BACKUP_FILE"
else
    echo "Error: Backup creation failed."
    exit 2
fi

What Changed?

1. The input check now uses a loop.

while true repeats the input process. The continue statement restarts the loop when the input is empty, while break exits the loop after a valid directory has been supplied.

This prevents the script from attempting to back up a path that failed validation.

2. The backup directory is checked.

The if ! mkdir ... condition handles failure and prevents the script from continuing when the destination cannot be created.

3. The filename uses explicit variable boundaries.

${backup} makes it clear where the variable name ends before the underscore.

4. TAR runs inside a conditional.

The script reports success or failure based on TAR's exit status.

5. The archive uses a cleaner internal path.

The -C option makes TAR change to the source directory's parent before archiving its folder name. This avoids storing an absolute source path in the archive.

This example assumes the input is a conventional directory path rather than the filesystem root. It is intended as a beginner exercise, not a complete production backup system.

9. How to Run and Test the Script

Step 1: Create the File

Open your Linux terminal and run:

nano backup_script.sh

Paste the improved script into the editor.

Save the file using Ctrl + O, press Enter, and exit with Ctrl + X.

Step 2: Make It Executable

chmod +x backup_script.sh

Step 3: Run the Script

./backup_script.sh

Enter an existing directory, such as a test folder you created specifically for this lesson.

Step 4: Verify the Backup

List the backup files:

ls -lh ./backups

You should see a file similar to:

backup_20261011_143025.tar.gz

To inspect the archive's contents without extracting it, run:

tar -tzf ./backups/backup_20261011_143025.tar.gz

Replace the example filename with the actual archive created by your script.

To extract an archive into a separate test directory, you can use:

mkdir -p ./restore_test
tar -xzf ./backups/backup_20261011_143025.tar.gz -C ./restore_test

Again, use the filename that actually exists on your system.

For this exercise, verify that the restored files match the originals. A successful TAR command confirms that the archive operation completed without reporting a failure; it does not, by itself, guarantee that you have a complete backup strategy or that every restored file will be usable.

10. Practical Exercises

Exercise 1 — Input validation: Enter an empty path, then a missing directory, followed by a valid directory. Observe how the loop behaves.

Exercise 2 — Filename customization: Change the filename prefix from backup to documents and verify the output filename.

Exercise 3 — Backup verification: Use tar -tzf to list the archive's contents and compare them with the source directory.

Exercise 4 — Error handling: Test what happens when you choose a destination where you do not have permission to create files.

Exercise 5 — Compression and archiving: Research the differences between .tar, .tar.gz, and .zip files.

Exercise 6 — Backup improvements: Add a feature that displays the archive's size after creation. You can use du -h "$BACKUP_FILE".

Conclusion

In Level 3, we combined Bash input validation, loops, conditional statements, directory creation, command substitution, timestamps, TAR archives, and exit-status handling.

The result is a practical backup script that can validate a source directory and create a compressed, timestamped archive.

These concepts are useful in system administration, log collection, project backups, and authorized security lab workflows.

Next step: Expand the script with options for choosing the backup destination, recording backup activity in a log file, and handling older archives. As you build these features, continue checking the exit status of important commands and verifying that your backups can actually be restored.

Comments

Popular posts from this blog

Information disclosure in version control history

Linux AAA

Source code disclosure via backup files