Use a robust BASH script that leverages find with -print0 and read -d '' to safely handle filenames containing spaces or special characters. The script extracts the creation date using exiftool, formats it into a safe directory structure, and copies images while renaming them to prevent collisions, ensuring your camera photos are organized without data loss.
Why Safe Filename Handling is Critical
When automating file operations in Linux, relying on standard word splitting is a common pitfall. Filenames from cameras often contain spaces, parentheses, or even unusual Unicode characters. A naive approach using a simplefor file in $(find ...) loop will break immediately if a filename contains a space, treating each word as a separate argument. This leads to errors where files are not copied or directories are created with malformed names.
To solve this, you must use null-delimited output. The find command supports the -print0 option, which separates entries with a null byte instead of a newline. When combined with while read -r -d '' line, the shell reads one complete filename at a time, regardless of internal whitespace. This technique is essential for any production-grade script dealing with user-generated content like photos. For more context on robust shell scripting practices, you might find Bash Shell - Shorter Way to Remove Last Characters from the File Name useful for understanding string manipulation safety.
Extracting Metadata with ExifTool
The core requirement is retrieving the creation date. While some filesystems store modification times, these can be unreliable after copying or syncing. The most accurate source is the EXIF metadata embedded in the image file itself.exiftool is the industry-standard tool for reading and writing this metadata.
You need to extract the "CreateDate" or "DateTimeOriginal" tag. These tags typically follow the format YYYY:MM:DD HH:MM:SS. However, this format contains colons, which are invalid characters for directory names on most Unix-like systems. Therefore, you cannot simply create a directory named 2023:10:25. You must transform this string into a valid path structure, such as 2023/10/25.
Before running the full script, test your extraction command manually. Run:
exiftool -s3 -CreateDate photo.jpg
This outputs just the raw date string without labels. Verify that your version of exiftool returns the expected format. If the image lacks EXIF data, the variable will be empty, so your script must handle this edge case by falling back to the file's modification time or skipping the file entirely.
Constructing the Directory Structure
Once you have the raw date string, you need to parse it into a year, month, and day. Bash parameter expansion allows you to slice strings efficiently. Assuming the date is stored in variable$date_str (e.g., 2023:10:25 14:30:00), you can extract the components:
- Year: Extract the first four characters.
- Month: Extract characters 6 through 7 (skipping the colon).
- Day: Extract characters 9 through 10.
${date_str//:/}. Then, split by space to get just the date part. Construct the target directory path as ./output/${year}/${month}/${day}. Use mkdir -p to create this nested structure silently. The -p flag ensures that intermediate directories are created if they do not exist, and it does nothing if they already exist, making the operation idempotent.
The Complete Robust Script
Below is the corrected implementation. It addresses the previous rejection by implementing null-byte delimiters and safe date formatting.#!/bin/bash
# Check if exiftool is installed
if ! command -v exiftool &> /dev/null; then
echo "Error: exiftool is required but not installed."
exit 1
fi
SOURCE_DIR="${1:-.}"
DEST_DIR="${2:-organized_photos}"
# Create destination base directory
mkdir -p "$DEST_DIR"
# Find all image files recursively
# Using -print0 for safe handling of special characters
find "$SOURCE_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.raw" \) -print0 | while IFS= read -r -d '' file; do
# Get the creation date
date_str=$(exiftool -s3 -CreateDate "$file" 2>/dev/null)
# If no EXIF date, skip or use file mtime (here we skip for simplicity)
if [[ -z "$date_str" ]]; then
continue
fi
# Clean up date string: remove time part and replace colons with slashes
# Example input: 2023:10:25 14:30:00
# We want: 2023/10/25
clean_date=$(echo "$date_str" | cut -d' ' -f1 | tr ':' '/')
# Validate that we got a reasonable date format (YYYY/MM/DD)
if [[ ! "$clean_date" =~ ^[0-9]{4}/[0-9]{2}/[0-9]{2}$ ]]; then
echo "Warning: Could not parse date from $file"
continue
fi
# Define target directory and filename
target_dir="$DEST_DIR/$clean_date"
mkdir -p "$target_dir"
# Generate unique filename to avoid overwrites
basename_file=$(basename "$file")
extension="${basename_file##*.}"
name="${basename_file%.*}"
# Simple collision avoidance: append timestamp if exists
target_file="$target_dir/${name}_${date_str//[: ]/_}.${extension}"
# Copy the file
cp -- "$file" "$target_file"
echo "Copied: $file -> $target_file"
done
echo "Processing complete."
This script uses cp -- to handle filenames starting with hyphens correctly. It also sanitizes the filename replacement characters, turning colons and spaces into underscores to ensure the resulting filename is valid across different operating systems.
Error Handling and Validation
Even with safe loops, scripts can fail due to permissions or disk space. Always check the exit status of critical commands. In the script above, we check for the existence ofexiftool before starting. Inside the loop, we verify that the parsed date matches a regex pattern. This prevents creating directories with garbage names if the EXIF data is corrupted.
Additionally, consider adding logging. Instead of just echoing to stdout, you might redirect output to a log file. This helps in auditing which files were processed and which were skipped. If you encounter issues where commands work individually but fail in the script, remember to check for environment variable differences or path issues, similar to discussions found in Linux RHEL 7 - Shell Script - Can execute command separately but cannot execute inside a script.
Frequently Asked Questions
What if my images don't have EXIF data?
If an image lacks EXIF data, the exiftool command returns empty, causing the script to skip the file. You can modify the script to fall back to the file's modification time using stat if the EXIF date is missing, ensuring no files are lost during the process.
How do I handle RAW files like .CR2 or .NEF?
The script currently includes common extensions like .raw. To support specific camera formats, add their extensions to the find command's -iname conditions, such as -o -iname "*.cr2" for Canon or -o -iname "*.nef" for Nikon.
Can I run this script on Windows?
This script relies on Bash and exiftool, which are native to Linux/macOS. On Windows, you can run it using WSL (Windows Subsystem for Linux) or Git Bash, provided you install exiftool in that environment. Native PowerShell would require a different syntax.
What happens if two files have the same name and date?
The script appends a sanitized version of the original creation timestamp to the filename to create a unique identifier. This prevents overwriting existing files. For example, IMG_1234_2023_10_25_14_30_00.jpg ensures uniqueness based on the exact second of creation.
Is it safe to delete the original files after running this script?
No, never delete originals until you have verified the integrity of the copied files. Always perform a spot-check by opening random files from the new directory. Once verified, you can manually delete the originals or add a rm command to the script, but only after careful testing.