
Never Accidentally `rm -rf` Again: A Safer `rm` for Your Terminal
We've all felt that jolt of icy fear 🥶. The one that hits you a millisecond after you press Enter on an `rm -rf` command, realizing you were in the wrong directory. Your project, your photos, your data—gone in an instant.
The standard rm command is unforgiving. It's a powerful tool, but it lacks an undo button. Today, we'll fix that. We're going to create a smart, self-backing-up replacement for rm that acts as a safety net for your most valuable files.
The Strategy: A Smarter rm
Our goal is to replace the default rm command with a function that does the following:
- Always Backs Up: Before deleting anything, it creates a compressed backup in a temporary folder.
- Prevents "Up-Arrow" Mistakes: If you run the exact same delete command twice in a row (a common mistake from using shell history), it will stop and ask for confirmation.
- Keeps Metadata: Each backup includes a small file with details like when and where the files were deleted from.
- Is Easy to Bypass: For scripts or situations where you need the original, ruthless
rm, you can still access it easily.
The Magic Script ✨
Here is the complete solution. Just paste this entire block of code into your shell configuration file, like ~/.bashrc for Bash or ~/.zshrc for Zsh.
#!/bin/bash
# A safer rm function that moves files to a temporary backup location
# instead of deleting them permanently.
rm() {
# --- Configuration ---
# Set the directory where deleted files will be stored.
local backup_root="$HOME/.local/share/trash"
mkdir -p "$backup_root"
# File to store the hash of the last rm command.
local last_command_log="$HOME/.cache/safe_rm_last_command"
mkdir -p "$(dirname "$last_command_log")"
# --- Argument Parsing ---
local force_delete=false
local recursive_delete=false
local files_to_delete=()
# Loop through all arguments to identify flags and files.
for arg in "$@"; do
case "$arg" in
-f|--force)
force_delete=true
;;
-r|-R|--recursive)
recursive_delete=true
;;
-rf|-fr)
force_delete=true
recursive_delete=true
;;
-*)
# Silently ignore other flags to maintain compatibility
;;
*)
# If it's not a flag, it's a file or directory.
files_to_delete+=("$arg")
;;
esac
done
# If no files are provided, show the standard rm error.
if [[ ${#files_to_delete[@]} -eq 0 ]]; then
/bin/rm -- "$@"
return $?
fi
# --- Safety Check: Prevent Double-Run ---
# Create a unique signature for the current command.
local command_signature
command_signature=$(printf "%s;" "${files_to_delete[@]}" && pwd)
local command_hash
command_hash=$(echo "$command_signature" | md5sum | awk '{print $1}')
# Check if the last command was identical.
if [[ -f "$last_command_log" ]] && [[ "$command_hash" == "$(cat "$last_command_log")" ]] && [[ "$force_delete" == false ]]; then
echo "⚠️ You just ran the exact same 'rm' command twice."
echo "This is a safety check to prevent accidental deletion from shell history."
read -p "Are you sure you want to proceed? [y/N] " -n 1 -r
echo
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
echo "Deletion cancelled."
# Clear the log so the next attempt isn't flagged.
rm "$last_command_log" 2>/dev/null
return 1
fi
fi
# --- Backup Process ---
# Create a unique directory for this specific deletion operation.
local backup_uuid
backup_uuid=$(date +%s)-$(uuidgen 2>/dev/null || od -x /dev/urandom | head -1 | awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}')
local backup_dir="$backup_root/$backup_uuid"
mkdir -p "$backup_dir"
# Create a metadata file.
cat > "$backup_dir/metadata.log" << EOF
Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
User: $USER@$HOSTNAME
Directory: $(pwd)
Command: rm $@
---
EOF
local backed_up_count=0
# Process each file/directory targeted for deletion.
for target in "${files_to_delete[@]}"; do
if [[ -e "$target" || -L "$target" ]]; then
# Append info to metadata and create a compressed archive.
echo "Original Path: $(realpath "$target")" >> "$backup_dir/metadata.log"
tar -czf "$backup_dir/$(basename "$target").tar.gz" --remove-files "$target" &>/dev/null
((backed_up_count++))
fi
done
# --- Finalization ---
if [[ $backed_up_count -gt 0 ]]; then
echo "✅ Moved $backed_up_count item(s) to trash. Restore with 'restore-files'."
echo " UUID: $backup_uuid"
# Log the command hash for the next run's safety check.
echo "$command_hash" > "$last_command_log"
else
echo "rm: no such file or directory"
# Clean up the empty backup dir if nothing was deleted.
/bin/rm -rf "$backup_dir"
return 1
fi
}
# --- Utility Functions ---
# List recently deleted items.
list-deleted() {
local backup_root="$HOME/.local/share/trash"
if [[ ! -d "$backup_root" ]] || [[ -z "$(ls -A "$backup_root")" ]]; then
echo "Trash is empty."
return
fi
# Find metadata files, sort by time, and display them.
find "$backup_root" -name "metadata.log" -printf '%T@ %p\n' | sort -nr | cut -d' ' -f2- | while read -r meta; do
echo "----------------------------------------"
grep -E "UUID|Timestamp|Directory|Command" "$meta" | sed "s/Timestamp/Deleted/" | sed "s/Directory/From/"
echo "UUID: $(basename "$(dirname "$meta")")"
done
}
# Restore files from a specific deletion UUID.
restore-files() {
local uuid="$1"
local backup_root="$HOME/.local/share/trash"
if [[ -z "$uuid" ]]; then
echo "Usage: restore-files <UUID>"
echo "Run 'list-deleted' to find the UUID of the files you want to restore."
return 1
fi
local backup_dir="$backup_root/$uuid"
if [[ ! -d "$backup_dir" ]]; then
echo "Error: No backup found with UUID '$uuid'."
return 1
fi
echo "Restoring files from $backup_dir..."
# Extract all .tar.gz files from the backup dir to the current directory.
tar -xzf "$backup_dir"/*.tar.gz -C .
# Optional: remove the backup after restoring.
read -p "Restore successful. Remove this backup from trash? [y/N] " -n 1 -r
echo
if [[ "$REPLY" =~ ^[Yy]$ ]]; then
/bin/rm -rf "$backup_dir"
echo "Backup removed."
fi
}
# Permanently delete old backups (e.g., older than 30 days).
clean-trash() {
local days=${1:-30}
local backup_root="$HOME/.local/share/trash"
echo "Deleting backups older than $days days..."
find "$backup_root" -maxdepth 1 -type d -mtime +"$days" -exec /bin/rm -rf {} \;
echo "Cleanup complete."
}<br><br>
<p><a href="https://dancingdragons.cc/m/consult-appt" style="display: inline-block; padding: 12px 24px; background-color: rgb(123, 63, 0); color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px;">Book a Discovery Call</a></p>


