How to Create a Bash File Restore Tool

How to Create a Bash File Restore Tool

Backups are only half the story. I learned that the hard way once — I had a solid backup routine running for months, but when I actually needed to restore a file, I realized I’d never actually tested how painful the restore process would be. It took me nearly an hour of digging through nested archive folders just to pull back a single config file. That experience is exactly why I built a dedicated Bash restore tool, separate from my backup script, so that pulling files back is just as easy as backing them up in the first place.

This article walks through building that restore tool, and it pairs naturally with the Bash File Backup Tool article in this series — I’d recommend reading both together.

What “Restore” Actually Means in This Context

A restore tool needs to answer three questions quickly:

  1. What backups do I have available?
  2. Which specific files or folders do I want back?
  3. Where should they go, and should anything be overwritten?

Let’s build a tool around exactly those three questions, assuming a backup structure of timestamped tar.gz archives (the kind our Backup Tool article produces).

Prerequisites

  • Bash 4.0+
  • tar installed
  • An existing set of timestamped backup archives, e.g. backup-20260115-020000.tar.gz

Step 1: Listing Available Backups

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="${1:-$HOME/backups}"

if [[ ! -d "$BACKUP_DIR" ]]; then
    echo "Error: backup directory '$BACKUP_DIR' not found."
    exit 1
fi

echo "Available backups in $BACKUP_DIR:"
echo "----------------------------------"

i=1
declare -A BACKUP_MAP
while IFS= read -r file; do
    BACKUP_MAP[$i]="$file"
    size=$(du -h "$file" | cut -f1)
    echo "$i) $(basename "$file")  ($size)"
    ((i++))
done < <(find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | sort -r)

Explaining the Internals

  • declare -A BACKUP_MAP creates an associative array — a Bash data structure that maps arbitrary keys (in our case, simple numbers) to values (file paths). This lets us present a numbered menu and later look up the actual file path by number.
  • find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | sort -r lists backup archives matching our naming pattern and sorts them in reverse order, so the most recent backup (assuming a sortable timestamp in the filename) appears first.
  • while IFS= read -r file; do ... done < <(...) is process substitution combined with a safe read loop — this pattern avoids a common Bash gotcha where piping into a while loop creates a subshell that loses variable changes after the loop ends. Using < <(...) keeps the loop in the main shell, so BACKUP_MAP and i persist correctly afterward.
  • ((i++)) increments our counter using Bash’s arithmetic evaluation syntax.

Step 2: Letting the User Choose a Backup

read -rp "Select a backup to restore from (number): " CHOICE

SELECTED="${BACKUP_MAP[$CHOICE]:-}"

if [[ -z "$SELECTED" ]]; then
    echo "Invalid selection."
    exit 1
fi

echo "Selected: $(basename "$SELECTED")"

${BACKUP_MAP[$CHOICE]:-} looks up the chosen number in our associative array, defaulting to an empty string if the key doesn’t exist — this lets us cleanly detect invalid input without the script crashing on an unset variable (which set -u would otherwise catch as an error).

Step 3: Previewing Archive Contents Before Restoring

Never restore blind. Let’s list what’s inside before extracting anything:

echo "Contents of this backup:"
echo "-------------------------"
tar -tzf "$SELECTED" | head -n 20

TOTAL_FILES=$(tar -tzf "$SELECTED" | wc -l)
echo "... ($TOTAL_FILES total entries)"

tar -tzf lists (-t) the contents of a gzip-compressed (-z) archive (-f) without extracting anything, which is exactly what we want for a safe preview. head -n 20 keeps the initial listing manageable for large archives.

Step 4: Restoring Specific Files or the Whole Archive

read -rp "Restore (a)ll files or (s)pecific file? [a/s] " MODE
RESTORE_DIR="${2:-./restored}"
mkdir -p "$RESTORE_DIR"

if [[ "$MODE" == "s" ]]; then
    read -rp "Enter the exact path/filename to restore (from the list above): " TARGET_FILE
    tar -xzf "$SELECTED" -C "$RESTORE_DIR" "$TARGET_FILE"
    echo "Restored '$TARGET_FILE' to '$RESTORE_DIR'"
else
    tar -xzf "$SELECTED" -C "$RESTORE_DIR"
    echo "Restored all files to '$RESTORE_DIR'"
fi

What’s Happening

  • Restoring to a separate restored/ directory rather than directly overwriting the original location is a deliberate safety choice — it lets you inspect restored files before deciding to move them into place, avoiding accidental overwrites of newer data.
  • tar -xzf "$SELECTED" -C "$RESTORE_DIR" "$TARGET_FILE" — passing a specific path after the archive name tells tar to extract only that one entry instead of everything, which is exactly what we want for a targeted single-file restore.

Step 5: Adding a Confirmation Step Before Overwriting

If the user does want to restore directly into the original location, we should double-check first:

read -rp "Restore directly to original location? This may overwrite existing files. [y/N] " CONFIRM

if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
    ORIGINAL_LOCATION="${3:-/}"
    tar -xzf "$SELECTED" -C "$ORIGINAL_LOCATION"
    echo "Restored directly to $ORIGINAL_LOCATION"
else
    echo "Skipped direct restore. Files remain in '$RESTORE_DIR' for manual review."
fi

This explicit y/N confirmation, defaulting to “no” via the regex check ^[Yy]$, is a small but important safeguard against destructive mistakes — a theme you’ll notice throughout every tool in this series.

Full Combined Script

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="${1:-$HOME/backups}"
RESTORE_DIR="${2:-./restored}"

if [[ ! -d "$BACKUP_DIR" ]]; then
    echo "Error: backup directory '$BACKUP_DIR' not found."
    exit 1
fi

echo "Available backups:"
i=1
declare -A BACKUP_MAP
while IFS= read -r file; do
    BACKUP_MAP[$i]="$file"
    size=$(du -h "$file" | cut -f1)
    echo "$i) $(basename "$file")  ($size)"
    ((i++))
done < <(find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | sort -r)

read -rp "Select a backup (number): " CHOICE
SELECTED="${BACKUP_MAP[$CHOICE]:-}"

if [[ -z "$SELECTED" ]]; then
    echo "Invalid selection."
    exit 1
fi

echo "Preview of contents:"
tar -tzf "$SELECTED" | head -n 20
echo "... ($(tar -tzf "$SELECTED" | wc -l) total entries)"

mkdir -p "$RESTORE_DIR"
read -rp "Restore (a)ll or (s)pecific file? [a/s] " MODE

if [[ "$MODE" == "s" ]]; then
    read -rp "Enter exact filename/path to restore: " TARGET_FILE
    tar -xzf "$SELECTED" -C "$RESTORE_DIR" "$TARGET_FILE"
else
    tar -xzf "$SELECTED" -C "$RESTORE_DIR"
fi

echo "Restore complete. Files are in '$RESTORE_DIR'."

Real-World Use Cases

  • Recovering a single accidentally-deleted config file without restoring an entire system backup.
  • Testing disaster recovery procedures on a schedule, to make sure your backups are actually restorable (a step people skip far too often).
  • Rolling back a bad deployment by restoring the previous night’s backup of an application directory.
  • Auditing historical file states by extracting an old backup to compare against the current version of a file.

Automating Restore Testing

It’s genuinely valuable to periodically verify that your latest backup can actually be restored, without manual intervention:

#!/usr/bin/env bash
LATEST=$(find "$HOME/backups" -iname "backup-*.tar.gz" | sort -r | head -n 1)
TEST_DIR=$(mktemp -d)

tar -xzf "$LATEST" -C "$TEST_DIR"

if [[ -n "$(ls -A "$TEST_DIR")" ]]; then
    echo "OK: Latest backup restores successfully."
else
    echo "ALERT: Latest backup appears empty or failed to restore!" | mail -s "Backup Restore Test FAILED" you@example.com
fi

rm -rf "$TEST_DIR"

mktemp -d creates a temporary directory with a unique, collision-free name, which is the correct way to create scratch space in scripts rather than hardcoding a path like /tmp/test.

Security Considerations

  • Restored files inherit the permissions stored in the archive, which might not match what you want on the destination system — review permissions after restoring, especially for executable scripts or sensitive config files.
  • Never restore untrusted archives without inspecting contents first using tar -tzf, since a malicious archive could contain path traversal entries designed to write outside the intended directory.
  • Be careful restoring directly to / or other system paths — always restore to an isolated directory first, verify contents, then move files manually if a direct restore genuinely isn’t needed.
  • Protect the backup directory itself with appropriate permissions (chmod 700), since backups often contain the same sensitive data as the original files.

Optimization Tips

  • For huge archives, extracting a single file (as shown in Step 4) is vastly faster than extracting everything and then deleting what you don’t need.
  • If you restore frequently from the same backup, consider extracting once to a temp location and reusing it rather than re-extracting from the compressed archive each time.
  • Sorting backups by filename timestamp (as we do) is much faster than sorting by modification time via stat, since it avoids extra system calls per file.

Troubleshooting

  • “tar: file_name: Not found in archive” — the path you specified for a single-file restore must match exactly, including any leading directory structure that was present when the archive was created; check the preview listing carefully.
  • Restored files have wrong ownership — this happens when restoring as a different user than the one who created the backup; use sudo tar -xzf ... --same-owner if you need to preserve original ownership and have appropriate privileges.
  • Restore directory already has conflicting filestar will overwrite by default; if you want to avoid this, extract to a fresh empty directory first (as our script does) and merge manually.

Common Mistakes to Avoid

  • Never testing your restore process until an actual emergency, only to discover it doesn’t work as expected.
  • Restoring directly over live/production files without a preview or backup-of-the-backup first.
  • Assuming the most recent backup file (by filename) is definitely the most recent by content — always double check if backups were generated by multiple overlapping processes.
  • Forgetting to preserve file permissions when restoring sensitive files like SSH keys or credential files.

Frequently Asked Questions

How do I restore just one file without extracting the whole archive? Use tar -xzf archive.tar.gz -C destination/ path/inside/archive, exactly as shown in Step 4 — tar supports selective extraction natively.

What if my backups aren’t named with a consistent timestamp pattern? Adjust the find -iname pattern in Step 1 to match your actual naming convention, or switch the sort to use modification time via find ... -printf '%T@ %p\n' | sort -rn.

Can I restore from a backup stored on a remote server? Yes — first rsync or scp the archive locally (or mount the remote location), then run this restore tool against the local copy for simplicity and speed.

Summary

We built a Bash restore tool that lists available backups in a friendly numbered menu, lets you preview contents before extracting anything, supports both full and single-file restores, and includes a confirmation step before any potentially destructive direct restore. The key takeaway is that restore tooling deserves just as much care and testing as backup tooling — a backup you’ve never successfully restored from isn’t a backup you can actually trust.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Create a Bash File Backup Tool

How to Create a Bash File Backup Tool

Next Post
How to Create a Bash File Sync Tool

How to Create a Bash File Sync Tool

Related Posts