Skip to content

Instantly share code, notes, and snippets.

@jvhaarst
Last active April 9, 2025 13:05
Show Gist options
  • Save jvhaarst/2343281 to your computer and use it in GitHub Desktop.
Save jvhaarst/2343281 to your computer and use it in GitHub Desktop.
Bash script to move images, based on exif data and file timestamp
#!/bin/bash
# Reads EXIF creation date from all .JPG files in the
# current directory and moves them carefully under
#
# $BASEDIR/YYYY/YYYY-MM/YYYY-MM-DD/
#
# ...where 'carefully' means that it does not overwrite
# differing files if they already exist and will not delete
# the original file if copying fails for some reason.
#
# It DOES overwrite identical files in the destination directory
# with the ones in current, however.
#
# This script was originally written and put into
# Public Domain by Jarno Elonen <[email protected]> in June 2003.
# Feel free to do whatever you like with it.
# Defaults
TOOLS=(exiftool jq) # Also change settings below if changing this, the output should be in the format YYYY:MM:DD
DEFAULTDIR='/Users/jvhaarst/Pictures/van camera/van camera'
MAXDEPTH=-maxdepth 1
#MAXDEPTH=''
# activate debugging from here
#set -o xtrace
#set -o verbose
# Improve error handling
set -o errexit
set -o pipefail
# Check whether needed programs are installed
for TOOL in ${TOOLS[*]}
do
hash $TOOL 2>/dev/null || { echo >&2 "I require $TOOL but it's not installed. Aborting."; exit 1; }
done
# Enable handling of filenames with spaces:
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
# Use BASEDIR from commandline, or default if none given
BASEDIR=${1:-$DEFAULTDIR}
for FILE in $(find $(pwd -P) $MAXDEPTH -not -wholename "*._*" -iname "*.JPG" -or -iname "*.JPEG" -or -iname "*.CRW" -or -iname "*.THM" -or -iname "*.RW2" -or -iname '*.ARW' -or -iname "*AVI" -or -iname "*MOV" -or -iname "*MP4" -or -iname "*MTS" -or -iname "*PNG")
do
INPUT=${FILE}
DATE=$(exiftool -quiet -tab -dateformat "%Y:%m:%d" -json -DateTimeOriginal "${INPUT}" | jq --raw-output '.[].DateTimeOriginal')
if [ "$DATE" == "null" ] # If exif extraction with DateTimeOriginal failed
then
DATE=$(exiftool -quiet -tab -dateformat "%Y:%m:%d" -json -MediaCreateDate "${INPUT}" | jq --raw-output '.[].MediaCreateDate')
fi
if [ -z "$DATE" ] || [ "$DATE" == "null" ] # If exif extraction failed
then
DATE=$(stat -f "%Sm" -t %F "${INPUT}" | awk '{print $1}'| sed 's/-/:/g')
fi
if [ ! -z "$DATE" ]; # Doublecheck
then
YEAR=$(echo $DATE | sed -E "s/([0-9]*):([0-9]*):([0-9]*)/\\1/")
MONTH=$(echo $DATE | sed -E "s/([0-9]*):([0-9]*):([0-9]*)/\\2/")
DAY=$(echo $DATE | sed -E "s/([0-9]*):([0-9]*):([0-9]*)/\\3/")
if [ "$YEAR" -gt 0 ] & [ "$MONTH" -gt 0 ] & [ "$DAY" -gt 0 ]
then
OUTPUT_DIRECTORY=${BASEDIR}/${YEAR}_${MONTH}_${DAY}
mkdir -pv ${OUTPUT_DIRECTORY}
OUTPUT=${OUTPUT_DIRECTORY}/$(basename ${INPUT})
if [ -e "$OUTPUT" ] && ! cmp -s "$INPUT" "$OUTPUT"
then
echo "WARNING: '$OUTPUT' exists already and is different from '$INPUT'."
else
echo "Moving '$INPUT' to $OUTPUT"
rsync -ah --progress "$INPUT" "$OUTPUT"
if ! cmp -s "$INPUT" "$OUTPUT"
then
echo "WARNING: copying failed somehow, will not delete original '$INPUT'"
else
rm -f "$INPUT"
fi
fi
else
echo "WARNING: '$INPUT' doesn't contain date."
fi
else
echo "WARNING: '$INPUT' doesn't contain date."
fi
done
# restore $IFS
IFS=$SAVEIFS
@ldong
Copy link

ldong commented Dec 27, 2015

Nice, thank you. This script uses exiftool as dependency, so brew install exiftool for mac people.

@sko-lv
Copy link

sko-lv commented Jun 29, 2016

Is 69 line correct or should be:
mkdir -pv ${OUTPUT_DIRECTORY} ?

@jvhaarst
Copy link
Author

Yeah, fixed now.

@dforrer
Copy link

dforrer commented Mar 5, 2017

If $INPUT-Path equals $OUTPUT-Path you're just deleting the files.
Meaning if you run this script twice you delete all files which are already sorted.

@rampol
Copy link

rampol commented Jun 25, 2017

Thanks for this script! I needed to modify lines 48 and 51 adding .val to make it work. Not sure why, probably my exiftool version (brew in osx el capitan)
DATE=$(exiftool -quiet -tab -dateformat "%Y:%m:%d" -json -DateTimeOriginal "${INPUT}" | jq --raw-output '.[].DateTimeOriginal.val')

@ncherro
Copy link

ncherro commented Dec 6, 2017

Thanks for this 👍

@atescula
Copy link

Thanks for script ! How to also rename files based on some EXIF parameters like Nikon ShutterCount, Canon filenumber, etc. Thanks a lot

@freefd
Copy link

freefd commented Apr 9, 2025

Thank you so much for such an amazing script, exactly what I've been looking for for so long.

Besides of fixing the jq execution, I've also improved it a bit to handle the local timezone in dates from video files that are always in UTC and added CLI arguments: https://github.com/freefd/utils/blob/master/media_files_sort.sh.

The short documentation can be found at https://github.com/freefd/utils#media_files_sortsh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment