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:
- Intercept deletions and move files to a “trash” location instead of deleting them outright (prevention).
- Wrap low-level recovery utilities in a friendly interface for genuine after-the-fact recovery.
Prerequisites
- Bash 4.0+
rsyncinstalled- Optionally
testdisk/photorecfor 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 deletingreport.txttwice doesn’t overwrite the first deleted copy.mkdir -pcreates 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 realrm. basename -- "$file"strips the directory path, leaving just the filename. The--tellsbasenamethat 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. Usingmvhere 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()andrestore_file()are Bash functions — reusable blocks of code. Defining them lets us keep thecasestatement at the bottom clean and readable.find "$TRASH_DIR" -iname "*${pattern}*" | head -n 1finds 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, soreport.txt.20260115-093000becomesreport.txtagain.read -rpprompts for confirmation before restoring, which prevents accidental overwrites — this is a good habit for any destructive or file-moving operation.- The
casestatement at the bottom acts like a lightweight command router, letting the script behave differently based on the first argument (listorrestore).
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/nullchecks whetherphotorecis 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-trashwith the same care as your real files — don’t leave sensitive deleted files sitting around indefinitely. - Deep recovery tools require root, and running
photorec/testdiskas root against the wrong device can cause data loss elsewhere. Always double-check device names withlsblkorfdisk -lbefore 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
photorecfile-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
mvactually 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+rmas a fallback, or place the trash directory on the same partition. - Restore script can’t find the file — check
./recover.sh listfor 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.