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:
- What backups do I have available?
- Which specific files or folders do I want back?
- 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+
tarinstalled- 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_MAPcreates 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 -rlists 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 awhileloop creates a subshell that loses variable changes after the loop ends. Using< <(...)keeps the loop in the main shell, soBACKUP_MAPandipersist 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 tellstarto 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-ownerif you need to preserve original ownership and have appropriate privileges. - Restore directory already has conflicting files —
tarwill 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.