How to Create a Bash File Recovery Tool

How to Create a Bash File Recovery Tool

Every sysadmin has that one story — the accidental rm -rf, the overwritten config file, the deleted directory that nobody had a backup of. I have a few of those stories myself, and after the second time I lost something important, I decided to build a Bash-based file recovery tool that gives me a fighting chance to get files back before disaster strikes for real. In this article, I’ll walk you through exactly how I built it, why each piece matters, and how you can adapt it for your own systems.

I want to be upfront about something important: Bash alone cannot magically undelete a file that the filesystem has already overwritten. What we’re really building here is a combination of a safety net (so files are recoverable before they’re truly gone) and a recovery wrapper around existing low-level recovery utilities. That distinction matters, and I’ll explain it clearly as we go.

Understanding How File Deletion Actually Works

When you delete a file on Linux, the data usually isn’t wiped from disk immediately. The filesystem just marks the space as available for reuse. Until something else overwrites that space, recovery is often possible using tools like extundelete, testdisk, or photorec. Our Bash tool will do two things:

  1. Intercept deletions and move files to a “trash” location instead of deleting them outright (prevention).
  2. Wrap low-level recovery utilities in a friendly interface for genuine after-the-fact recovery.

Prerequisites

  • Bash 4.0+
  • rsync installed
  • Optionally testdisk/photorec for deep recovery (sudo apt install testdisk)
  • Root or sudo access for deep recovery operations

Part 1: Building a “Trash” Interceptor (Prevention Is Better Than Cure)

The single best recovery tool is the one that prevents permanent loss in the first place. Let’s build a safe-rm script that moves files to a trash folder instead of deleting them.

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

TRASH_DIR="$HOME/.local/share/bash-trash"
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")

mkdir -p "$TRASH_DIR"

if [[ $# -eq 0 ]]; then
    echo "Usage: saferm <file1> [file2] [file3] ..."
    exit 1
fi

for file in "$@"; do
    if [[ -e "$file" ]]; then
        base=$(basename -- "$file")
        dest="$TRASH_DIR/${base}.${TIMESTAMP}"
        mv -- "$file" "$dest"
        echo "Moved '$file' to trash as '$dest'"
    else
        echo "Warning: '$file' does not exist, skipping."
    fi
done

How This Works Internally

  • TRASH_DIR="$HOME/.local/share/bash-trash" follows the XDG-ish convention of storing app data under the user’s home directory rather than a system-wide path, which avoids permission issues.
  • TIMESTAMP=$(date +"%Y%m%d-%H%M%S") generates a sortable timestamp string, which we append to filenames so that deleting report.txt twice doesn’t overwrite the first deleted copy.
  • mkdir -p creates the trash directory if it doesn’t already exist, and does nothing (without error) if it does.
  • The for file in "$@" loop iterates over every argument passed to the script, so it supports deleting multiple files in one call, just like real rm.
  • basename -- "$file" strips the directory path, leaving just the filename. The -- tells basename that no further arguments should be treated as options, which protects against filenames that start with a dash.
  • mv -- "$file" "$dest" performs the actual “deletion” by relocating the file rather than removing it. Using mv here is what makes recovery trivial — the file’s data is untouched.

You can alias this so rm becomes safe by default:

echo "alias rm='saferm'" >> ~/.bashrc
source ~/.bashrc

Part 2: Building the Recovery Script for the Trash

Now let’s build the counterpart — a script to list and restore trashed files.

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

TRASH_DIR="$HOME/.local/share/bash-trash"

list_trash() {
    echo "Files currently in trash:"
    echo "-------------------------"
    ls -lt "$TRASH_DIR" 2>/dev/null || echo "Trash is empty."
}

restore_file() {
    local pattern="$1"
    local match
    match=$(find "$TRASH_DIR" -iname "*${pattern}*" | head -n 1)

    if [[ -z "$match" ]]; then
        echo "No trashed file matches '$pattern'."
        exit 1
    fi

    local original_name
    original_name=$(basename "$match" | sed -E 's/\.[0-9]{8}-[0-9]{6}$//')

    read -rp "Restore '$match' as './${original_name}'? [y/N] " confirm
    if [[ "$confirm" =~ ^[Yy]$ ]]; then
        mv -- "$match" "./$original_name"
        echo "Restored to ./$original_name"
    else
        echo "Cancelled."
    fi
}

case "${1:-}" in
    list)
        list_trash
        ;;
    restore)
        restore_file "${2:-}"
        ;;
    *)
        echo "Usage: $0 {list|restore <filename-pattern>}"
        exit 1
        ;;
esac

Explaining the Logic

  • list_trash() and restore_file() are Bash functions — reusable blocks of code. Defining them lets us keep the case statement at the bottom clean and readable.
  • find "$TRASH_DIR" -iname "*${pattern}*" | head -n 1 finds the most likely match and takes only the first result, since multiple deleted versions of the same file might exist.
  • sed -E 's/\.[0-9]{8}-[0-9]{6}$//' strips our timestamp suffix off the filename using a regular expression, so report.txt.20260115-093000 becomes report.txt again.
  • read -rp prompts for confirmation before restoring, which prevents accidental overwrites — this is a good habit for any destructive or file-moving operation.
  • The case statement at the bottom acts like a lightweight command router, letting the script behave differently based on the first argument (list or restore).

Usage looks like this:

./recover.sh list
./recover.sh restore report

Part 3: Wrapping Deep Recovery Tools for Already-Deleted Files

If a file was deleted before you had this safety net in place, you’ll need to reach for lower-level tools. Here’s a Bash wrapper around testdisk/photorec that simplifies the process for ext4 filesystems:

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

DEVICE="${1:-}"
OUTPUT_DIR="${2:-./recovered}"

if [[ -z "$DEVICE" ]]; then
    echo "Usage: $0 /dev/sdXN [output-directory]"
    echo "Run 'lsblk' to identify your device/partition."
    exit 1
fi

mkdir -p "$OUTPUT_DIR"

if ! command -v photorec &>/dev/null; then
    echo "photorec is not installed. Install it with: sudo apt install testdisk"
    exit 1
fi

echo "WARNING: Deep recovery scans can take a long time and should"
echo "be run against an unmounted device or partition when possible."
read -rp "Continue scanning $DEVICE? [y/N] " confirm

if [[ "$confirm" =~ ^[Yy]$ ]]; then
    sudo photorec /d "$OUTPUT_DIR" /cmd "$DEVICE" search
else
    echo "Cancelled."
fi

What’s Happening Here

  • command -v photorec &>/dev/null checks whether photorec is installed without printing anything to the screen — a standard Bash idiom for testing command availability.
  • We deliberately require explicit confirmation before running a deep scan, since these operations are I/O-intensive and can take hours on large drives.
  • We recommend running against an unmounted device because continuing to write to a disk (including just using it normally) increases the risk of overwriting the very data you’re trying to recover.

Real-World Use Cases

  • Developers who occasionally delete uncommitted work accidentally — the trash interceptor alone has saved me multiple times.
  • Sysadmins managing shared servers where multiple users might delete shared files by mistake.
  • Backup verification workflows — pairing this script with scheduled backups (see the Bash File Backup Tool article) gives you defense in depth.

Automation Ideas

You can schedule a cron job to purge trash older than 30 days so it doesn’t grow forever:

0 3 * * * find "$HOME/.local/share/bash-trash" -type f -mtime +30 -delete

This runs at 3 AM daily and removes any trashed file older than 30 days, giving you a rolling recovery window without unlimited disk growth.

Security Considerations

  • Trash directories still contain sensitive data. Treat ~/.local/share/bash-trash with the same care as your real files — don’t leave sensitive deleted files sitting around indefinitely.
  • Deep recovery tools require root, and running photorec/testdisk as root against the wrong device can cause data loss elsewhere. Always double-check device names with lsblk or fdisk -l before proceeding.
  • Never run recovery scans on a mounted, actively-used root filesystem if you can help it — boot from a live USB when recovering system-critical data for the best odds.

Optimization Tips

  • Recovery scans are I/O-bound, not CPU-bound — running them on an SSD is dramatically faster than on spinning disks.
  • Limit photorec file-type search to only the extensions you care about (it supports this interactively) to speed up scans significantly.
  • Keep the trash directory on the same filesystem/partition as the files being deleted — moving across filesystems with mv actually copies and deletes, which is slower and risks partial writes on interruption.

Troubleshooting

  • “mv: cannot move… Invalid cross-device link” — this happens if your trash directory is on a different filesystem than the source file. Either use cp + rm as a fallback, or place the trash directory on the same partition.
  • Restore script can’t find the file — check ./recover.sh list for the exact filename pattern; timestamps may differ from what you expect if the file was deleted at a different time than assumed.
  • photorec produces no results — this usually means the data has already been overwritten. The sooner you act after deletion, the better your odds.

Common Mistakes to Avoid

  • Continuing to write large amounts of data to a disk after discovering an accidental deletion — this increases the chance of overwriting the deleted file’s data blocks.
  • Relying solely on deep recovery tools instead of building simple prevention (like the trash interceptor) into your workflow.
  • Forgetting to test your recovery script before you actually need it — always do a dry run with a throwaway file.

Frequently Asked Questions

Is it possible to recover a file after rm -rf with no trash safety net? Sometimes, using tools like extundelete or photorec, but success isn’t guaranteed and depends heavily on filesystem type and how much the disk has been used since deletion.

Does this work on ext4, NTFS, and APFS? The trash interceptor works on any filesystem since it’s just a mv operation. Deep recovery tool support varies — testdisk/photorec support most common filesystems including ext4, NTFS, and FAT32.

Should I use this instead of real backups? No. This is a safety net, not a backup strategy. Pair it with the Bash File Backup Tool described elsewhere in this series.

Summary

We built a two-layer file recovery system in Bash: a prevention layer that intercepts deletions and moves files to a recoverable trash location, and a recovery layer that wraps professional-grade tools like photorec for cases where files were already deleted before the safety net existed. The key lesson here is that true “undelete” magic doesn’t really exist in Bash — what we can do is minimize risk and make recovery as smooth as possible when things go wrong.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Create a Bash File Decompression Tool

How to Create a Bash File Decompression Tool

Next Post
How to Create a Bash File Search Tool

How to Create a Bash File Search Tool

Related Posts