Skip to content

Instantly share code, notes, and snippets.

@melalj
Created June 30, 2025 08:26
Show Gist options
  • Save melalj/e97b8ddc53e939094e32e0a5d6cc58c4 to your computer and use it in GitHub Desktop.
Save melalj/e97b8ddc53e939094e32e0a5d6cc58c4 to your computer and use it in GitHub Desktop.
πŸ“Έ Bulk Photo Renamer: EXIF datetime β†’ YYYY-MM-DD_HH-MM-SS with dry-run mode
#!/bin/bash
# Photo Rename Script - Renames photos based on EXIF datetime
# Usage: ./rename_photos.sh [directory] [--dry-run]
#
# Requirements: exiftool (install with: sudo apt install libimage-exiftool-perl)
set -uo pipefail
# Default values
TARGET_DIR="${1:-.}"
DRY_RUN=false
COUNTER=1
# Check for dry-run flag
if [[ "${2:-}" == "--dry-run" ]] || [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
echo "πŸ” DRY RUN MODE - No files will be renamed"
echo
fi
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Check if exiftool is installed
if ! command -v exiftool &> /dev/null; then
print_status "$RED" "❌ Error: exiftool is not installed."
echo "Install it with:"
echo " Ubuntu/Debian: sudo apt install libimage-exiftool-perl"
echo " macOS: brew install exiftool"
echo " Arch: sudo pacman -S perl-image-exiftool"
exit 1
fi
# Check if target directory exists
if [[ ! -d "$TARGET_DIR" ]]; then
print_status "$RED" "❌ Error: Directory '$TARGET_DIR' does not exist."
exit 1
fi
print_status "$BLUE" "πŸ“Έ Photo Renaming Script"
print_status "$BLUE" "Directory: $TARGET_DIR"
echo
# Supported image extensions
EXTENSIONS=("jpg" "jpeg" "png" "tiff" "tif" "raw" "cr2" "nef" "arw" "dng" "heic" "webp")
# Build find command with all extensions
FIND_PATTERN=""
for ext in "${EXTENSIONS[@]}"; do
if [[ -n "$FIND_PATTERN" ]]; then
FIND_PATTERN="$FIND_PATTERN -o"
fi
FIND_PATTERN="$FIND_PATTERN -iname \"*.$ext\""
done
# Count total files first
TOTAL_FILES=$(eval "find \"$TARGET_DIR\" -type f \\( $FIND_PATTERN \\)" | wc -l)
print_status "$BLUE" "Found $TOTAL_FILES image files to process"
echo
# Initialize counters
RENAMED=0
SKIPPED=0
ERRORS=0
# Function to generate safe filename
generate_safe_filename() {
local datetime="$1"
local extension="$2"
local base_name="$3"
local dir="$4"
# Convert datetime to filename format: YYYY-MM-DD_HH-MM-SS
local formatted_date=$(echo "$datetime" | sed 's/[: ]/_/g' | sed 's/__/_/g')
local new_name="${formatted_date}.${extension}"
local full_path="${dir}/${new_name}"
# If file exists, add counter
local counter=1
while [[ -e "$full_path" ]]; do
new_name="${formatted_date}_${counter}.${extension}"
full_path="${dir}/${new_name}"
((counter++))
done
echo "$new_name"
}
# Function to process a single file
process_file() {
local filepath="$1"
local filename=$(basename "$filepath")
local dirname=$(dirname "$filepath")
local extension="${filename##*.}"
printf "[$COUNTER/$TOTAL_FILES] Processing: %s... " "$filename"
# Extract datetime from EXIF data
local datetime=$(exiftool -s3 -DateTimeOriginal -CreateDate -d "%Y-%m-%d %H:%M:%S" "$filepath" 2>/dev/null | head -n1)
if [[ -z "$datetime" || "$datetime" == "-" ]]; then
print_status "$YELLOW" "⚠️ No datetime found, skipping"
SKIPPED=$((SKIPPED + 1))
return 0
fi
# Generate new filename
local new_name=$(generate_safe_filename "$datetime" "$extension" "$filename" "$dirname")
local new_path="${dirname}/${new_name}"
# Check if rename is needed
if [[ "$filename" == "$new_name" ]]; then
print_status "$BLUE" "βœ“ Already correctly named"
SKIPPED=$((SKIPPED + 1))
return 0
fi
# Perform rename
if [[ "$DRY_RUN" == true ]]; then
print_status "$GREEN" "πŸ“ Would rename to: $new_name"
RENAMED=$((RENAMED + 1))
else
if mv "$filepath" "$new_path" 2>/dev/null; then
print_status "$GREEN" "βœ… Renamed to: $new_name"
RENAMED=$((RENAMED + 1))
else
print_status "$RED" "❌ Failed to rename"
ERRORS=$((ERRORS + 1))
return 0
fi
fi
return 0
}
# Process all image files
while IFS= read -r -d '' file; do
process_file "$file"
COUNTER=$((COUNTER + 1))
done < <(eval "find \"$TARGET_DIR\" -type f \\( $FIND_PATTERN \\) -print0")
# Print summary
echo
print_status "$BLUE" "πŸ“Š Summary:"
if [[ "$DRY_RUN" == true ]]; then
echo " Files that would be renamed: $RENAMED"
else
echo " Files renamed: $RENAMED"
fi
echo " Files skipped: $SKIPPED"
echo " Errors: $ERRORS"
echo " Total processed: $TOTAL_FILES"
if [[ "$DRY_RUN" == true ]]; then
echo
print_status "$YELLOW" "πŸ’‘ Run without --dry-run to actually rename files"
fi
if [[ $ERRORS -gt 0 ]]; then
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment