Bash script to automatically sort photos into folders based on EXIF data for Ubuntu Linux

If you — like me — take a lot of digital pictures, you probably have a hundred folders full of images on your hard drive or external drives, and not nearly as sorted as you would like them to be. you have probably gotten to the point that you don’t know what’s in them or can’t find an image when you’re looking. I had around 10,000 images in over a dozen folders spanning 5+ years, and I had no intention of even trying to sort them manually :) So I wrote this script.

The following script was written in bash on Ubuntu Linux and automatically sorts your images into directories based on the date and time the photo was taken. How does it do this? By making use of the EXIF data your digital camera stores inside the image. The date and time the photo was taken is stored in that EXIF data. When an image doesn’t have EXIF data (such as when it was downloaded from the Internet, or taken from a camera that doesn’t support adding EXIF data), it will use the files last-modified time.

First, this should be run in the top-most directory of wherever your pictures are stored. If you have pictures/foldername/somepics/ and pictures/anotherfolder/morepics, run it from your pictures/ directory.

There are quite a few opportunities to improve this script — and some cautionary notes as well — marked within the script with FIXME tags. I’m already finished sorting my images, but anyone is welcome to contribute suggestions and improvements, which I’ll look into incorporating the next time I’m using this. You are welcome to include any suggestions or code improvements in the comments below using <code> and/or <pre> tags.
Usage: Copy the script into a file, editing options where appropriate, and save it. Make it executable and run it from the command line or window, from the directory where your pictures are stored. No command-line arguments. Back up your stuff first :)

UPDATE: Because WordPress keeps mangling this code, it has been moved to github, here.

Questions, comments, or feedback can be left in the comments below, or please use the contact form. Thank you!

Advertisements

, , , ,

  1. #1 by J.W on January 13, 2012 - 11:54 am

    This script is exactly what I’ve been looking for, but when I tried to run the script I get the following error:

    line 127: syntax error: unexpected end of file

    Any idea what it’s about?

    • #2 by Mike on January 13, 2012 - 11:11 pm

      Not really. Unless you have unusual characters in your file name or you copied the script incorrectly, it shouldn’t give you much trouble.

  2. #3 by Michael on March 26, 2012 - 2:25 pm

    Thanks for the script it worked pretty nicely and i found it useful. I like to sort my files differently so I made the following modification:

    Replaced:
    DIRNAME=`echo $EDATE | sed y/:///`

    With:
    EYEAR=`echo $EDATE | cut -f 1 -d’:’`
    EMONTH=`echo $EDATE | cut -f 2 -d’:’`
    EDAY=`echo $EDATE | cut -f 3 -d’:’`
    DIRNAME=`echo “$EYEAR/$EYEAR-$EMONTH-$EDAY” | awk -F ‘{print $1}’`

    • #4 by Mike Beach on March 30, 2012 - 9:34 am

      Thanks Michael!

      One thing though — I’ve noticed the comment box tends to strip some special characters from comments, which can break code. Would you please check your code as posted and make sure it’s correct? If it’s not, shoot me a message via the contact form and I’d be happy to work with you to get it posted correctly.

      Thanks again!

      • #5 by Michael on March 30, 2012 - 2:55 pm

        It doesn’t seem to have objected on this instance.

  3. #6 by Michael on April 22, 2012 - 10:23 am

    hello. i made another change.

    to address “# FIXME: Collisions are not handled.” I added “–backup=numbered” on the mv commands.

    Something like fdupes could be used to get rid of true duplicates, though i would probably run it as a separate step rather than in the script.

    • #7 by Mike Beach on May 2, 2012 - 9:14 pm

      Thanks! There’s plenty of opportunity for improvement, I’m sure :)

      • #8 by Michael on May 7, 2012 - 1:34 pm

        i run it every time i download my photos and i tend to think about tweeks each time.

        Sounds to me like you had it as a once-off?

        • #9 by Mike on January 5, 2013 - 7:54 pm

          Yes, it was written just for use in one instance, but I thought to put it out there in case anyone else could make use of it.

  4. #10 by Bicko on May 15, 2012 - 5:32 pm

    Great job!
    My story – I accidentally formatted a CF card with 3GB of photos on it. I recovered 500+ images from the card (using dd and foremost) but I was only interested in the photos taken on one specific day, the rest had previously been copied from the card anyway. Your script is beavering away right now as I type and I’m happily watching one particular directory grow in size. I will simply delete everything else when the script terminates. Thanks.

  5. #11 by Nathan on June 7, 2012 - 6:25 pm

    Any way to run this on either Windows (Cygwin?) or Mac (in the Terminal)? The ‘identify’ command, which I assume grabs the exif info, does not seem to exist in Mac OS.

    Any help appreciated

  6. #12 by TimTom on July 3, 2012 - 1:53 am

    I’d suggest something like this:
    FILETYPES=(“*.jpg” “*.jpeg” “*.png” “*.tif” “*.tiff” “*.gif” “*.xcf” “*.dng” “*.pef”)
    with
    DATETIME=`exiftool “$2” | grep “Date/Time Original *: ” | awk -F ‘{print $4″ “$5}’`

    to support DNG, PEF and more file formats. I didn’t test this yet, but maybe you can make it work for real :)

    Using exiftool also solves MacOS problem from Nathan.

    P.S. Oh yeah, you should definitely use DateTimeOriginal instead of DateTime as it’s adjusted by several catalogue/album/imaging tools.

    • #13 by Mike Beach on July 23, 2012 - 1:31 pm

      Thanks for the suggestions! There are a bunch of file types which support EXIF data, so I made it as easy as possible to add new types based on your usage case.

      Thanks for the pointer on DateTimeOriginal too!

  7. #14 by Mark Nelson (@RocketRyda) on July 22, 2012 - 6:10 am

    I’m also trying to use this script on OS X Lion and I ended up changing

    DATETIME=`identify -verbose “$2” | grep “exif:DateTime:” | awk -F ‘{print $2″ “$3}’`
    to
    DATETIME=`exiftool “$2” |grep “Create Date” | awk ‘{print $4 ” ” $5}’`

    and that seems to work fine for me buy I’m not sure what to do with the following line

    # Check for the presence of imagemagick and the identify command.
    # Assuming its valid and working if found.
    I=`which identify`
    if [ “$I” == “” ]; then
    echo “The ‘identify’ command is missing or not available.”
    echo “Is imagemagick installed?”

    any suggestion would be greatly appreciated. Also please not any other known OS X changes (if possible).

    Thanks a lot.

    • #15 by Mike Beach on July 22, 2012 - 10:57 pm

      Since your fix eliminates relying on identify for the exif data, you could simply remove or comment out those lines.

  8. #16 by Szelev71 on October 7, 2012 - 2:22 am

    thanks, very useful and easy to customize script! Helped me a lot! :)

  9. #17 by Eric Berard on November 7, 2012 - 9:55 pm

    I love the idea of this script but when I run it I get the following error messages:

    ./photosort.sh: line 79: syntax error in conditional expression: unexpected token `&’
    ./photosort.sh: line 79: syntax error near `&a’
    ./photosort.sh: line 79: `if [[ “$1” == “doAction” && “$2” != “” ]]; then’

    Any idea what might be causing this?
    EB

    • #18 by Mike on November 7, 2012 - 10:26 pm

      Thanks for pointing that out. Somehow a few lines of code got mangled.

      Should be all fixed now.

  10. #19 by Sikevux on December 9, 2012 - 10:49 am

    at line 154:
    find . -iname “$x” -print0 -exec sh -c “$0 doAction ‘{}'” ;
    should be replaced with:
    find . -iname “$x” -print0 -exec sh -c “$0 doAction ‘{}'” ;

    (escape “;”)
    to avoid interpetation by the shell.

    • #20 by Mike on January 5, 2013 - 7:55 pm

      Thanks! All fixed in the above.

  11. #21 by Steve on December 10, 2012 - 11:38 pm

    Mike: thank you for the script. I was fortunate enough to use it after ext3 file system corrupted and used foremost to recover photos. I had to utilize Sikevux suggestion to fix the error I got with the script on Ubuntu Linux 12.04 LTS:
    find: missing argument to `-exec’
    which is fixed by escaping the semicolon at the end of find line with a backslash
    find . -iname "$x" -print0 -exec sh -c "$0 doAction '{}'" ;

  12. #22 by Mike on December 11, 2012 - 1:11 am

    @Sikevux and @Steve

    Thanks for the correction. I think it was one of the points where the editor mangled it and I wasn’t aware. I’ve fixed it above. Please feel free to let me know if you find any other issues.

  13. #23 by Steve on December 20, 2012 - 7:47 pm

    Had another error in the script:
    “sed: strings for y command are different lengths”
    Apparently[1], the y option doesn’t interpret the backslash consistently as the rest of SED options on all systems (mine system is Ubuntu 12.04).

    [1] http://www.linuxmisc.com/12-unix-shell/c99e560523f0e1ce.htm

    • #24 by Mike on December 22, 2012 - 2:55 pm

      Thank you for pointing that out!

      • #25 by Jon on January 5, 2013 - 5:20 pm

        What was the solution to this issue?

        • #26 by Mike on January 5, 2013 - 8:00 pm

          Hmm.. Although I don’t have a case for using it at the moment, I have some thoughts on rewriting the script and fixing some issues. This one I’m not sure of at the moment, and I’ll take feedback. My thought is that since the y option has the issue, but not the s option (according to the post linked to by Steve — Thanks!) and since there’s a fixed number of characters to replace, just doing a for or while loop and using s instead of y inside that loop. A bit of a pain IMHO.

          • #27 by thomas on May 17, 2013 - 12:06 pm

            did you find the solution to line 88 in the script.

            having following problems (ubuntu 12.10)

            Checking EXIF… found: 2010:07:13 19:23:39
            Will rename to 1279041819.error:

            error:
            error:
            sed: -e udtryk nr. 1, tegn 5: strings for `y’ command are different lengths
            Moving to /1279041819.error:
            error:
            error: … mkdir: kan ikke oprette katalog ”: Ingen sådan fil eller filkatalog
            done.

          • #28 by thomas on May 17, 2013 - 12:41 pm

            maybe its line 111 or 125 but doesnt understand it.

            here is a more detailed example

            thomas@thomas-desktop:~/Media/Foto/Digitalfoto/Usorteret/test$ ./sorter-foto.sh
            Scanning for *.jpg…
            ./IMG_9571.JPG: Checking EXIF… found: 2011:07:06 21:19:20
            Will rename to 1309979960.jpeg
            sed: -e udtryk nr. 1, tegn 5: strings for `y’ command are different lengths
            Moving to /1309979960.jpeg … mkdir: kan ikke oprette katalog ”: Ingen sådan fil eller filkatalog
            done.

            … end of *.jpg
            Scanning for *.jpeg…
            … end of *.jpeg
            Scanning for *.png…
            … end of *.png
            Scanning for *.tif…
            … end of *.tif
            Scanning for *.tiff…
            … end of *.tiff
            Scanning for *.gif…
            … end of *.gif
            Scanning for *.xcf…
            … end of *.xcf
            Removing Thumbs.db files … done.
            Cleaning up empty directories … done.

          • #29 by thomas on May 17, 2013 - 2:25 pm

            figured it out:

            changed line 126 from:

            DIRNAME=`echo $EDATE | sed y/:///`

            to:

            DIRNAME=`echo $EDATE | sed y-:-/-`

          • #30 by Mike on May 18, 2013 - 8:28 pm

            Thanks for that solution, Thomas! I’ll change to script to reflect that!

  14. #31 by Stephen on June 4, 2013 - 4:03 am

    Hmmm. This is awesome. I take it that it’s not on Github yet, given the date of the last comment…

    It’d be great to be able to see changes over time and allow others to contribute in a more formal/trackable way…

    Are you able to do this (or are you opposed?). If you don’t, can I? It would be preferable for you to do it, since your repository would be the master…

    Cheers

    • #32 by Mike on June 7, 2013 - 5:23 pm

      Stephen,

      I appreciate the positive feedback and suggestion to use github, but I’ll have to decline, and respectfully ask you not to post it elsewhere.

      I may at some point consider it, but not at this time.

  15. #33 by Stephen on June 4, 2013 - 4:30 am

    I’ve found jhead which could make this script a whole lot simpler and/or redundant.

    Apart from boiler checking directories etc, the script at http://www.linuxquestions.org/questions/programming-9/sorting-photos-into-folders-based-on-exif-date-722733/ ends up boiling down to:

    jhead -n%s *.jpg # millis since epoch

    or my preferred

    jhead –autorot -ft “n%Y/%m – %B/%Y%m%d/%f” *.jpg
    # rotate (according to exif)
    # set timestamp to exif date, and
    # put in 2013/06 – June/20130604/IMG_1234.jpg

    and it will append suffixes if the file is already there etc…

  16. #34 by Ghislain on June 16, 2013 - 6:12 pm

    Hi Mike,

    you can also use exiftool (from the package libimage-exiftool-perl) with the following command:

    exiftool -ext JPG -ext AVI “-Directory<DateTimeOriginal" -r -d "%Y-%m-%d" .

    You can add some other extensions (I tried with MOV or NEF which worked) by adding other -ext SOMETHING.
    -r option is for recursive search.
    -d is for the destination directories pattern (here the pictures will be in directories named with the pictures dates). It can of course be changed to look like something "%Y/%m/%d.

    There are a lot of examples in exiftool's man page, in the "Examples" sections.

    I guess it could be used with a couple of bash lines to handles more files (for example pictures with no exif as you suggested).

  17. #35 by Gabriel on July 16, 2013 - 5:40 pm

    I searched around and downloaded ten scripts to review for my needs. Yours was easily my favorite. I needed to modify it a little bit for the way I wanted it to run. I didn’t want it to search recursively and I did want it to handle folder and file collisions. Below is the code how I run it.


    #!/bin/bash
    #
    ###############################################################################
    # Photo sorting program by Mike Beach
    # For usage and instructions, and to leave feedback, see
    # https://mikebeach.org/?p=4729
    #
    # Last update: 20-May-2013
    ###############################################################################
    #
    # The following are the only settings you should need to change:
    #
    # TS_AS_FILENAME: This can help eliminate duplicate images during sorting.
    # WARNING: Any two files with the same filename are automatically overwritten when
    # this is on. FIXME: Handle filename collisions.
    # TRUE: File will be renamed to the Unix timestamp and its extension.
    # FALSE (any non-TRUE value): Filename is unchanged.

    TS_AS_FILENAME=FALSE

    #
    # USE_LMDATE: If this is TRUE, images without EXIF data will have their Last Modified file
    # timestamp used as a fallback. If FALSE, images without EXIF data are put in noexif/ for
    # manual sorting.
    # WARNING: Filename collisions are NOT handled when this is off. Files are automatically
    # overwritten.
    # FIXME: Handle collisions when this is off.
    # Valid options are "TRUE" or anything else (assumes FALSE). FIXME: Restrict to TRUE/FALSE
    #

    USE_LMDATE=TRUE

    #
    # USE_FILE_EXT: The following option is here as a compatibility option as well as a bugfix.
    # If this is set to TRUE, files are identified using FILE's magic, and the extension
    # is set accordingly. If FALSE (or any other value), file extension is left as-is.
    # CAUTION: If set to TRUE, extensions may be changed to values you do not expect.
    # See the manual page for file(1) to understand how this works.
    # NOTE: This option is only honored if TS_AS_FILENAME is TRUE.
    #

    USE_FILE_EXT=FALSE

    #
    # JPEG_TO_JPG: The following option is here for personal preference. If TRUE, this will
    # cause .jpg to be used instead of .jpeg as the file extension. If FALSE (or any other
    # value) .jpeg is used instead. This is only used if USE_FILE_EXT is TRUE and used.
    #

    JPEG_TO_JPG=FALSE

    #
    #
    # The following is an array of filetypes that we intend to locate using find.
    # Any imagemagick-supported filetype can be used, but EXIF data is only present in
    # jpeg and tiff. Script will optionally use the last-modified time for sorting (see above)
    # Extensions are matched case-insensitive. *.jpg is treated the same as *.JPG, etc.
    # Can handle any file type; not just EXIF-enabled file types. See USE_LMDATE above.
    #

    FILETYPES=("*.jpg" "*.jpeg" "*.png" "*.tif" "*.tiff" "*.gif" "*.xcf")

    #
    # Optional: Prefix of new top-level directory to move sorted photos to.
    # if you use MOVETO, it MUST have a trailing slash! Can be a relative pathspec, but an
    # absolute pathspec is recommended.
    # FIXME: Gracefully handle unavailable destinations, non-trailing slash, etc.
    #

    MOVETO=""

    #
    ###############################################################################
    # End of settings. If you feel the need to modify anything below here, please share
    # your edits at the URL above so that improvements can be made to the script. Thanks!
    #
    #
    # Assume find, grep, stat, awk, sed, tr, etc.. are already here, valid, and working.
    # This may be an issue for environments which use gawk instead of awk, etc.
    # Please report your environment and adjustments at the URL above.
    #
    ###############################################################################
    # Nested execution (action) call
    # This is invoked when the programs calls itself with
    # $1 = "doAction"
    # $2 =
    # This is NOT expected to be run by the user in this matter, but can be for single image
    # sorting. Minor output issue when run in this manner. Related to find -print0 below.
    #
    # Are we supposed to run an action? If not, skip this entire section.
    if [[ "$1" == "doAction" && "$2" != "" ]]; then

    # Check for EXIF and process it
    echo -n ": Checking EXIF... "

    DATETIME=`identify -verbose "$2" | grep "exif:DateTime:" | awk -F '{print $2" "$3}'`

    if [[ "$DATETIME" == "" ]]; then

    echo "not found."

    if [[ $USE_LMDATE == "TRUE" ]]; then

    # I am deliberately not using %Y here because of the desire to display the date/time
    # to the user, though I could avoid a lot of post-processing by using it.
    DATETIME=`stat --printf='%y' "$2" | awk -F. '{print $1}' | sed y/-/:/`

    echo " Using LMDATE: $DATETIME"

    else

    echo " Moving to ./noexif/"

    mkdir -p "${MOVETO}noexif" && mv -f "$2" "${MOVETO}noexif"

    exit

    fi;

    else

    echo "found: $DATETIME"

    fi;

    # The previous iteration of this script had a major bug which involved handling the
    # renaming of the file when using TS_AS_FILENAME. The following sections have been
    # rewritten to handle the action correctly as well as fix previously mangled filenames.
    # FIXME: Collisions are not handled.
    #
    EDATE=`echo $DATETIME | awk -F '{print $1}'`

    # Evaluate the file extension
    if [ "$USE_FILE_EXT" == "TRUE" ]; then

    # Get the FILE type and lowercase it for use as the extension
    EXT=`file -b $2 | awk -F '{print $1}' | tr '[:upper:]' '[:lower:]'`

    if [[ "${EXT}" == "jpeg" && "${JPEG_TO_JPG}" == "TRUE" ]]; then EXT="jpg"; fi;

    else

    # Lowercase and use the current extension as-is
    EXT=`echo $2 | awk -F. '{print $NF}' | tr '[:upper:]' '[:lower:]'`

    fi;

    # Evaluate the file name
    if [ "$TS_AS_FILENAME" == "TRUE" ]; then

    # Get date and times from EXIF stamp
    ETIME=`echo $DATETIME | awk -F '{print $2}'`

    # Unix Formatted DATE and TIME - For feeding to date()
    UFDATE=`echo $EDATE | sed y/:/-/`

    # Unix DateSTAMP
    UDSTAMP=`date -d "$UFDATE $ETIME" +%s`

    echo " Will rename to $UDSTAMP.$EXT"

    MVCMD="/$UDSTAMP.$EXT"

    fi;

    # DIRectory NAME for the file move
    # sed issue for y command fix provided by thomas
    # MOD: Added cut -c -8
    DIRNAME=`echo $EDATE | cut -c -8 | sed y-:-/-`

    # MOD: Added FILENAME variable
    FILENAME=`echo $2 | cut -c 3-`

    echo -n " Moving to ${MOVETO}${DIRNAME}${MVCMD} ... "

    # MOD: Checking if there is a duplicate file in the destination
    if [ -f "${MOVETO}${DIRNAME}${MVCMD}/$2" ]; then

    # MOD: Created EXT2 variable
    EXT2=`echo $2 | awk -F. '{print $NF}'`;

    # MOD: Add some random characters to the end of the file name but before the extension
    # before moving the file
    echo "Duplicate found!" && mv -f "$2" "${MOVETO}${DIRNAME}${MVCMD}${FILENAME%.*}${RANDOM}.${EXT2}";

    else

    # MOD: If there are no duplicates found but the destination directory already exists...
    if [ -e "${MOVETO}${DIRNAME}${MVCMD}" ]; then

    # MOD: ...move the files to the existing directory
    mv -n "$2" "${MOVETO}${DIRNAME}${MVCMD}";

    else

    # MOD: Otherwise, create a new directory and then move the files there
    mkdir -p "${MOVETO}${DIRNAME}" && mv -f "$2" "${MOVETO}${DIRNAME}${MVCMD}";

    fi;

    fi;

    echo "done."

    echo ""

    exit

    fi;

    #
    ###############################################################################
    # Scanning (find) loop
    # This is the normal loop that is run when the program is executed by the user.
    # This runs find for the recursive searching, then find invokes this program with the two
    # parameters required to trigger the above loop to do the heavy lifting of the sorting.
    # Could probably be optimized into a function instead, but I don't think there's an
    # advantage performance-wise. Suggestions are welcome at the URL at the top.
    for x in "${FILETYPES[@]}"; do

    # Check for the presence of imagemagick and the identify command.
    # Assuming its valid and working if found.
    I=`which identify`

    if [ "$I" == "" ]; then

    echo "The 'identify' command is missing or not available."

    echo "Is imagemagick installed?"

    exit 1

    fi;

    echo "Scanning for $x..."

    # FIXME: Eliminate problems with unusual characters in filenames.
    # Currently the exec call will fail and they will be skipped.
    # MOD: Added -maxdepth 1
    find . -maxdepth 1 -iname "$x" -print0 -exec sh -c "$0 doAction '{}'" ;

    echo "... end of $x"

    done;

    # clean up empty directories. Find can do this easily.
    # Remove Thumbs.db first because of thumbnail caching
    echo -n "Removing Thumbs.db files ... "

    find . -name Thumbs.db -delete

    echo "done."

    echo -n "Cleaning up empty directories ... "

    find . -empty -delete

    echo "done."

  18. #36 by merlin on July 17, 2013 - 2:05 pm

    Hi Mike,

    thanks for the great blog post.

    I compared the time it takes to extract the DateTimeOriginal with identify and to extraxt the CreateDate with exiftool. In my case both values where identicaly for the tested image. But exiftool was much faster than identify. Is there any reason you keep on using identify in your original posting?

    Greetings,
    merlin

    P.S. Here is my testing:

    user@machine:/media/123$ time identify -verbose IMG_0001.JPG | grep DateTimeOri | sed s%:%/%g
    exif/DateTimeOriginal/ 2013/07/02 19/22/50

    real 0m6.579s
    user 0m7.644s
    sys 0m0.156s

    user@machine:/media/123$ time exiftool -CreateDate IMG_0001.JPG | sed s%:%/%g
    Create Date / 2013/07/02 19/22/50

    real 0m0.127s
    user 0m0.104s
    sys 0m0.028s

  19. #37 by Alessandro Belloni on September 26, 2013 - 5:16 am

    ./sortpicture.sh
    Scanning for *.jpg…
    find: missing argument to `-exec’
    … end of *.jpg
    Scanning for *.jpeg…
    find: missing argument to `-exec’
    … end of *.jpeg
    Scanning for *.png…
    find: missing argument to `-exec’
    … end of *.png
    Scanning for *.tif…
    find: missing argument to `-exec’
    … end of *.tif
    Scanning for *.tiff…
    find: missing argument to `-exec’
    … end of *.tiff
    Scanning for *.gif…
    find: missing argument to `-exec’
    … end of *.gif
    Scanning for *.xcf…
    find: missing argument to `-exec’
    … end of *.xcf
    Removing Thumbs.db files … done.
    Cleaning up empty directories … done.

  20. #44 by Dayvo on September 28, 2013 - 7:33 pm

    The above error happens to me too on centos 6 64 bit

  21. #45 by Jim on October 13, 2013 - 4:50 pm

    Same issue…
    Scanning for *.jpg…
    find: -exec: no terminating “;” or “+”
    … end of *.jpg
    Scanning for *.jpeg…
    find: -exec: no terminating “;” or “+”
    … end of *.jpeg
    Scanning for *.png…
    find: -exec: no terminating “;” or “+”
    … end of *.png
    Scanning for *.tif…
    find: -exec: no terminating “;” or “+”
    … end of *.tif
    Scanning for *.tiff…
    find: -exec: no terminating “;” or “+”
    … end of *.tiff
    Scanning for *.gif…
    find: -exec: no terminating “;” or “+”
    … end of *.gif
    Scanning for *.xcf…
    find: -exec: no terminating “;” or “+”
    … end of *.xcf
    Removing Thumbs.db files … done.
    Cleaning up empty directories … done.

    Mac OS X 10.8.5 64-bit

    • #46 by Jim on October 13, 2013 - 5:38 pm

      On the mac, this is fixed by changing line 53 above from

      find . -iname “$x” -print0 -exec sh -c “$0 doAction ‘{}'” ;

      to

      find . -iname “$x” -print0 -exec sh -c “$0 doAction ‘{}'” ;

  22. #47 by Eric on October 22, 2013 - 9:53 am

    Thanks again for this script. I’ve made some changes to the script to include an array of month names to have a folder naming convention that suited my needs. Here is the modified script. I hope you find it useful:

    #!/bin/bash
    #
    ###############################################################################
    # Photo sorting program by Mike Beach
    # For usage and instructions, and to leave feedback, see
    # https://mikebeach.org/?p=4729
    #
    # Last update by Eric Berard: 22-Oct-2013
    ###############################################################################
    # Additional information by Eric B
    # Must have imagemagick package installed.
    # To install imagemagick on Debian based systems run:
    # sudo apt-get install imagemagick
    ###############################################################################
    # The following are the only settings you should need to change:
    #
    # TS_AS_FILENAME: This can help eliminate duplicate images during sorting.
    # WARNING: Any two files with the same filename are automatically overwritten when
    # this is on. FIXME: Handle filename collisions.
    # TRUE: File will be renamed to the Unix timestamp and its extension.
    # FALSE (any non-TRUE value): Filename is unchanged.
    TS_AS_FILENAME=TRUE
    #
    # USE_LMDATE: If this is TRUE, images without EXIF data will have their Last Modified file
    # timestamp used as a fallback. If FALSE, images without EXIF data are put in noexif/ for
    # manual sorting.
    # WARNING: Filename collisions are NOT handled when this is off. Files are automatically
    # overwritten.
    # FIXME: Handle collisions when this is off.
    # Valid options are “TRUE” or anything else (assumes FALSE). FIXME: Restrict to TRUE/FALSE
    #
    USE_LMDATE=TRUE
    #
    # USE_FILE_EXT: The following option is here as a compatibility option as well as a bugfix.
    # If this is set to TRUE, files are identified using FILE’s magic, and the extension
    # is set accordingly. If FALSE (or any other value), file extension is left as-is.
    # CAUTION: If set to TRUE, extensions may be changed to values you do not expect.
    # See the manual page for file(1) to understand how this works.
    # NOTE: This option is only honored if TS_AS_FILENAME is TRUE.
    #
    USE_FILE_EXT=FALSE
    #
    # JPEG_TO_JPG: The following option is here for personal preference. If TRUE, this will
    # cause .jpg to be used instead of .jpeg as the file extension. If FALSE (or any other
    # value) .jpeg is used instead. This is only used if USE_FILE_EXT is TRUE and used.
    #
    JPEG_TO_JPG=FALSE
    #
    #
    # The following is an array of filetypes that we intend to locate using find.
    # Any imagemagick-supported filetype can be used, but EXIF data is only present in
    # jpeg and tiff. Script will optionally use the last-modified time for sorting (see above)
    # Extensions are matched case-insensitive. *.jpg is treated the same as *.JPG, etc.
    # Can handle any file type; not just EXIF-enabled file types. See USE_LMDATE above.
    #
    FILETYPES=(“*.jpg” “*.jpeg” “*.png” “*.tif” “*.tiff” “*.gif” “*.xcf” “*.mts” “*.avi” “*.mov”)
    #
    # Optional: Prefix of new top-level directory to move sorted photos to.
    # if you use MOVETO, it MUST have a trailing slash! Can be a relative pathspec, but an
    # absolute pathspec is recommended.
    # FIXME: Gracefully handle unavailable destinations, non-trailing slash, etc.
    #
    MOVETO=”/home/ericb/Drive_O/Ours/Pictures/Caleb_Camera/”
    #
    # The following is an array of months used in destination directory naming.
    MONTHS=(NOMONTH 01-January 02-February 03-March 04-April 05-May 06-June 07-July 08-August 09-September 10-October 11-November 12-December)
    #
    ###############################################################################
    # End of settings. If you feel the need to modify anything below here, please share
    # your edits at the URL above so that improvements can be made to the script. Thanks!
    #
    #
    # Assume find, grep, stat, awk, sed, tr, etc.. are already here, valid, and working.
    # This may be an issue for environments which use gawk instead of awk, etc.
    # Please report your environment and adjustments at the URL above.
    #
    ###############################################################################
    # Nested execution (action) call
    # This is invoked when the programs calls itself with
    # $1 = “doAction”
    # $2 =
    # This is NOT expected to be run by the user in this matter, but can be for single image
    # sorting. Minor output issue when run in this manner. Related to find -print0 below.
    #
    # Are we supposed to run an action? If not, skip this entire section.
    if [[ “$1” == “doAction” && “$2” != “” ]]; then
    # Check for EXIF and process it
    echo -n “: Checking EXIF… ”
    DATETIME=`identify -verbose “$2” | grep “exif:DateTime:” | awk -F ‘{print $2″ “$3}’`
    if [[ “$DATETIME” == “” ]]; then
    echo “not found.”
    if [[ $USE_LMDATE == “TRUE” ]]; then
    # I am deliberately not using %Y here because of the desire to display the date/time
    # to the user, though I could avoid a lot of post-processing by using it.
    DATETIME=`stat –printf=’%y’ “$2″ | awk -F. ‘{print $1}’ | sed y/-/:/`
    echo ” Using LMDATE: $DATETIME”
    else
    echo ” Moving to ./noexif/”
    mkdir -p “${MOVETO}noexif” && mv -f “$2” “${MOVETO}noexif”
    exit
    fi;
    else
    echo “found: $DATETIME”
    fi;
    # The previous iteration of this script had a major bug which involved handling the
    # renaming of the file when using TS_AS_FILENAME. The following sections have been
    # rewritten to handle the action correctly as well as fix previously mangled filenames.
    # FIXME: Collisions are not handled.
    #
    EDATE=`echo $DATETIME | awk -F ‘{print $1}’`
    # Evaluate the file extension
    if [ “$USE_FILE_EXT” == “TRUE” ]; then
    # Get the FILE type and lowercase it for use as the extension
    EXT=`file -b $2 | awk -F ‘{print $1}’ | tr ‘[:upper:]’ ‘[:lower:]’`
    if [[ “${EXT}” == “jpeg” && “${JPEG_TO_JPG}” == “TRUE” ]]; then EXT=”jpg”; fi;
    else
    # Lowercase and use the current extension as-is
    EXT=`echo $2 | awk -F. ‘{print $NF}’ | tr ‘[:upper:]’ ‘[:lower:]’`
    fi;
    # Evaluate the file name
    if [ “$TS_AS_FILENAME” == “TRUE” ]; then
    # Get date and times from EXIF stamp
    ETIME=`echo $DATETIME | awk -F ‘{print $2}’`
    # Unix Formatted DATE and TIME – For feeding to date()
    UFDATE=`echo $EDATE | sed y/:/-/`
    # Unix DateSTAMP
    UDSTAMP=`date -d “$UFDATE $ETIME” +%s`
    echo ” Will rename to $UDSTAMP.$EXT”
    MVCMD=”/$UDSTAMP.$EXT”
    fi;
    # DIRectory NAME for the file move
    #DIRNAME=`echo $EDATE | sed y/:///`

    EYEAR=`echo $EDATE | cut -f 1 -d’:’`
    EMONTH=`echo $EDATE | cut -f 2 -d’:’ | sed s/^0//`
    EDAY=`echo $EDATE | cut -f 3 -d’:’`
    DIRNAME=`echo “$EYEAR/${MONTHS[$EMONTH]}-$EYEAR” | awk -F ‘{print $1}’`

    echo -n ” Moving to ${MOVETO}${DIRNAME}${MVCMD} … ”
    mkdir -p “${MOVETO}${DIRNAME}” && mv -f “$2” “${MOVETO}${DIRNAME}${MVCMD}”
    echo “done.”
    echo “”
    exit
    fi;
    #
    ###############################################################################
    # Scanning (find) loop
    # This is the normal loop that is run when the program is executed by the user.
    # This runs find for the recursive searching, then find invokes this program with the two
    # parameters required to trigger the above loop to do the heavy lifting of the sorting.
    # Could probably be optimized into a function instead, but I don’t think there’s an
    # advantage performance-wise. Suggestions are welcome at the URL at the top.
    for x in “${FILETYPES[@]}”; do
    # Check for the presence of imagemagick and the identify command.
    # Assuming its valid and working if found.
    I=`which identify`
    if [ “$I” == “” ]; then
    echo “The ‘identify’ command is missing or not available.”
    echo “Is imagemagick installed?”
    exit 1
    fi;
    echo “Scanning for $x…”
    # FIXME: Eliminate problems with unusual characters in filenames.
    # Currently the exec call will fail and they will be skipped.
    find . -iname “$x” -print0 -exec sh -c “$0 doAction ‘{}'” ;
    echo “… end of $x”
    done;
    # clean up empty directories. Find can do this easily.
    # Remove Thumbs.db first because of thumbnail caching
    echo -n “Removing Thumbs.db files … ”
    find . -name Thumbs.db -delete
    echo “done.”
    echo -n “Cleaning up empty directories … ”
    find . -empty -delete
    echo “done.”
    ###############script end##############

  23. #48 by Manfred on December 28, 2013 - 2:40 am

    For some reason the script does nothing… no errors but also no files found. The script is run in the directory where the pics are with ./sortexif.sh (that’s how I named it). The output is:

    “Scanning for ““*.jpg”…”
    “… end of ““*.jpg””
    “Scanning for “*.jpeg”…”
    “… end of “*.jpeg””
    “Scanning for “*.png”…”
    “… end of “*.png””
    “Scanning for “*.tif”…”
    “… end of “*.tif””
    “Scanning for “*.tiff”…”
    “… end of “*.tiff””
    “Scanning for “*.gif”…”
    “… end of “*.gif””
    “Scanning for “*.xcf”…”
    “… end of “*.xcf””
    “Scanning for “*.mts”…”
    “… end of “*.mts””
    “Scanning for “*.avi”…”
    “… end of “*.avi””
    “Scanning for “*.mov””…”
    “… end of “*.mov”””
    “Removing Thumbs.db files … ”“done.”
    “Cleaning up empty directories … ”“done.”

    Any guess why? IMHO it looks strange to me that each echo has two “” is this really intended, or did my edit mess this up?

    Thanks to all!

    • #49 by Mike on December 28, 2013 - 8:46 am

      What OS are you running this on?

  24. #50 by paresh on January 12, 2014 - 11:00 am

    hay thanks for the nice util to get some much needed work done tested it out on a small chunk and looking very useful indeed. I have 30,000+ photos that needs be taken control of and this should come in handy to get photobook site up for my family. I think all in it’s around 130 GB and I was stocked to see the EXIF tags were there form very first photo. A html5 gui to show progress and pause and restart would be nice addition, cant wait to get to it. thanks again.

  25. #51 by mdiin on March 12, 2014 - 3:46 am

    Thanks, this utility helped a lot. I had to change a couple of calls to awk though, when the delimiter is supposed to be a blank space. This may be my incomplete awk-knowledge speaking, but it seems to only work when `awk -F ` is replaced by `awk -F” ” `.

  26. #52 by FuNKeR on April 8, 2014 - 9:11 am

    Hi there,

    nice script. I would really like to use it, but I am not too deep into Linux and I also get the following error messages. Sadly I was not able to correct it with the help of all the other commenters.

    Could you please have a look how I could solve the problem?

    [secretuser@localhost somefolder]$ uname -a
    Linux localhost.localdomain 3.12.9-201.fc19.x86_64 #1 SMP Wed Jan 29 15:44:35 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
    
    [secretuser@localhost somefolder]$ bash photosort.sh
    Scanning for *.jpg...
    find: missing argument to `-exec'
    ... end of *.jpg
    Scanning for *.jpeg...
    find: missing argument to `-exec'
    ... end of *.jpeg
    Scanning for *.png...
    find: missing argument to `-exec'
    ... end of *.png
    Scanning for *.tif...
    find: missing argument to `-exec'
    ... end of *.tif
    Scanning for *.tiff...
    find: missing argument to `-exec'
    ... end of *.tiff
    Scanning for *.gif...
    find: missing argument to `-exec'
    ... end of *.gif
    Scanning for *.xcf...
    find: missing argument to `-exec'
    ... end of *.xcf
    Removing Thumbs.db files ... done.
    Cleaning up empty directories ... done.
    

    Thanks in advance
    Jan

  27. #53 by Marc Dupain on April 8, 2014 - 3:23 pm

    Great script. Was looking for this for a while. Made some adjustment for my own needs.
    Switched to exif instead of imagemagic. Imagemagic openen the entire file to read exif data. exif only extracts exif data. This speeds up the process significantly.

    Handled duplicate files too.

    See below in the script.
    Thanks.

    #!/bin/bash
    #
    ###############################################################################
    # Photo sorting program by Mike Beach
    # For usage and instructions, and to leave feedback, see
    # https://mikebeach.org/?p=4729
    #
    # Last update: 20-May-2013
    ###############################################################################
    #
    # The following are the only settings you should need to change:
    #
    # TS_AS_FILENAME: This can help eliminate duplicate images during sorting.
    # WARNING: Any two files with the same filename are automatically overwritten when
    # this is on. FIXME: Handle filename collisions.
    # TRUE: File will be renamed to the Unix timestamp and its extension.
    # FALSE (any non-TRUE value): Filename is unchanged.
    TS_AS_FILENAME=FALSE
    #
    # USE_LMDATE: If this is TRUE, images without EXIF data will have their Last Modified file
    # timestamp used as a fallback. If FALSE, images without EXIF data are put in noexif/ for
    # manual sorting.
    # WARNING: Filename collisions are NOT handled when this is off. Files are automatically
    # overwritten.
    # FIXME: Handle collisions when this is off.
    # Valid options are “TRUE” or anything else (assumes FALSE). FIXME: Restrict to TRUE/FALSE
    #
    USE_LMDATE=TRUE
    #
    # USE_FILE_EXT: The following option is here as a compatibility option as well as a bugfix.
    # If this is set to TRUE, files are identified using FILE’s magic, and the extension
    # is set accordingly. If FALSE (or any other value), file extension is left as-is.
    # CAUTION: If set to TRUE, extensions may be changed to values you do not expect.
    # See the manual page for file(1) to understand how this works.
    # NOTE: This option is only honored if TS_AS_FILENAME is TRUE.
    #
    USE_FILE_EXT=FALSE
    #
    # JPEG_TO_JPG: The following option is here for personal preference. If TRUE, this will
    # cause .jpg to be used instead of .jpeg as the file extension. If FALSE (or any other
    # value) .jpeg is used instead. This is only used if USE_FILE_EXT is TRUE and used.
    #
    JPEG_TO_JPG=FALSE
    #
    #
    # The following is an array of filetypes that we intend to locate using find.
    # Any imagemagick-supported filetype can be used, but EXIF data is only present in
    # jpeg and tiff. Script will optionally use the last-modified time for sorting (see above)
    # Extensions are matched case-insensitive. *.jpg is treated the same as *.JPG, etc.
    # Can handle any file type; not just EXIF-enabled file types. See USE_LMDATE above.
    #
    FILETYPES=(“*.jpg” “*.jpeg” “*.png” “*.tif” “*.tiff” “*.gif” “*.xcf” “*.avi” “*.mov”)
    #
    # Optional: Prefix of new top-level directory to move sorted photos to.
    # if you use MOVETO, it MUST have a trailing slash! Can be a relative pathspec, but an
    # absolute pathspec is recommended.
    # FIXME: Gracefully handle unavailable destinations, non-trailing slash, etc.
    #
    MOVETO=””
    #
    #
    # Optional: Source location for the inputfiles. This is the top-level directory of the
    # sourcefiles. Should start with a slash! This path is relative to the script root dir
    SOURCE=”/Nieuwe_fotos”
    #
    #
    # The following option removes empty directories after moving
    CLEANUP=FALSE
    #
    #
    # The following option is the destination directory for files that are existing in the
    # destination directory
    DUPLICATEDIRNAME=”Conflicts/”
    #
    #
    # Duplicated directory prefix (date). Note: There should be a trailing slash.
    CDATE=$(date ‘+%Y-%m-%d_%H:%M/’)
    #
    #
    ###############################################################################
    # End of settings. If you feel the need to modify anything below here, please share
    # your edits at the URL above so that improvements can be made to the script. Thanks!
    #
    #
    # Assume find, grep, stat, awk, sed, tr, etc.. are already here, valid, and working.
    # This may be an issue for environments which use gawk instead of awk, etc.
    # Please report your environment and adjustments at the URL above.
    #
    ###############################################################################
    # Nested execution (action) call
    # This is invoked when the programs calls itself with
    # $1 = “doAction”
    # $2 =
    # This is NOT expected to be run by the user in this matter, but can be for single image
    # sorting. Minor output issue when run in this manner. Related to find -print0 below.
    #
    # Are we supposed to run an action? If not, skip this entire section.
    if [[ “$1” == “doAction” && “$2” != “” ]]; then
    # Check for EXIF and process it
    echo -n “: Checking EXIF… ”
    # DATETIME=`identify -verbose “$2” | grep “exif:DateTime:” | awk -F ‘ ‘ ‘{print $2″ “$3}’`
    # Changed to exif instead of identify because exif is much faster
    DATETIME=`exif -m -t 0x9003 “$2″ | awk -F ‘ ‘ ‘{print $1” “$2}’`
    if [[ “$DATETIME” == “” ]]; then
    echo “not found.”
    if [[ $USE_LMDATE == “TRUE” ]]; then
    # I am deliberately not using %Y here because of the desire to display the date/time
    # to the user, though I could avoid a lot of post-processing by using it.
    DATETIME=`stat –printf=’%y’ “$2″ | awk -F. ‘{print $1}’ | sed y/-/:/`
    echo ” Using LMDATE: $DATETIME”
    else
    echo ” Moving to ./noexif/”
    mkdir -p “${MOVETO}noexif” && mv -f “$2” “${MOVETO}noexif”
    exit
    fi;
    else
    echo “found: $DATETIME”
    fi;
    # The previous iteration of this script had a major bug which involved handling the
    # renaming of the file when using TS_AS_FILENAME. The following sections have been
    # rewritten to handle the action correctly as well as fix previously mangled filenames.
    # FIXME: Collisions are not handled.
    #
    # EDATE=`echo $DATETIME | awk -F ‘ ‘ ‘{print $1}’`
    # Changed edate to yyyy/yyyy_mm_dd format to match my directory structure
    EDATE=`echo $DATETIME | awk -F ‘[ :]’ ‘{print $1 “/” $1 “_” $2 “_” $3}’`
    # Evaluate the file extension
    if [ “$USE_FILE_EXT” == “TRUE” ]; then
    # Get the FILE type and lowercase it for use as the extension
    EXT=`file -b $2 | awk -F ‘{print $1}’ | tr ‘[:upper:]’ ‘[:lower:]’`
    if [[ “${EXT}” == “jpeg” && “${JPEG_TO_JPG}” == “TRUE” ]]; then EXT=”jpg”; fi;
    else
    # Lowercase and use the current extension as-is
    EXT=`echo $2 | awk -F. ‘{print $NF}’ | tr ‘[:upper:]’ ‘[:lower:]’`
    fi;
    # Evaluate the file name
    if [ “$TS_AS_FILENAME” == “TRUE” ]; then
    # Get date and times from EXIF stamp
    ETIME=`echo $DATETIME | awk -F ‘ ‘ ‘{print $2}’`
    # Unix Formatted DATE and TIME – For feeding to date()
    UFDATE=`echo $EDATE | sed y/:/-/`
    # Unix DateSTAMP
    UDSTAMP=`date -d “$UFDATE $ETIME” +%s`
    echo ” Will rename to $UDSTAMP.$EXT”
    MVCMD=”/$UDSTAMP.$EXT”
    fi;
    # DIRectory NAME for the file move
    # sed issue for y command fix provided by thomas
    # DIRNAME=`echo $EDATE | sed y-:-/-`
    DIRNAME=$EDATE
    # Check whether file exists
    FILENAME=$(basename “$2”)
    if [ -f ${MOVETO}${DIRNAME}${MVCMD}”/”${FILENAME} ];
    then
    echo “File ${MOVETO}${DIRNAME}${MVCMD}”/”${FILENAME} exists”
    echo “Moved file to ${MOVETO}${DUPLICATEDIRNAME}${CDATE}${MVCMD}”
    mkdir -p “${MOVETO}${DUPLICATEDIRNAME}${CDATE}” && mv -f “$2” “${MOVETO}${DUPLICATEDIRNAME}${CDATE}${MVCMD}”
    else
    echo -n ” Moving to ${MOVETO}${DIRNAME}${MVCMD} … ”
    mkdir -p “${MOVETO}${DIRNAME}” && mv -f “$2” “${MOVETO}${DIRNAME}${MVCMD}”
    echo “done.”
    echo “”
    fi;
    exit

    fi;
    #
    ###############################################################################
    # Scanning (find) loop
    # This is the normal loop that is run when the program is executed by the user.
    # This runs find for the recursive searching, then find invokes this program with the two
    # parameters required to trigger the above loop to do the heavy lifting of the sorting.
    # Could probably be optimized into a function instead, but I don’t think there’s an
    # advantage performance-wise. Suggestions are welcome at the URL at the top.
    for x in “${FILETYPES[@]}”; do
    # Check for the presence of imagemagick and the identify command.
    # Assuming its valid and working if found.
    I=`which identify`
    if [ “$I” == “” ]; then
    echo “The ‘identify’ command is missing or not available.”
    echo “Is imagemagick installed?”
    exit 1
    fi;
    echo “Scanning for $x…”
    # FIXME: Eliminate problems with unusual characters in filenames.
    # Currently the exec call will fail and they will be skipped.
    find .$SOURCE -iname “$x” -print0 -exec sh -c “$0 doAction ‘{}'” \;
    echo “… end of $x”
    done;
    # clean up empty directories. Find can do this easily.
    # Remove Thumbs.db first because of thumbnail caching
    echo -n “Removing Thumbs.db files … ”
    find .$SOURCE -name Thumbs.db -delete
    echo “done.”
    if [ “$CLEANUP” == “TRUE” ]; then
    echo -n “Cleaning up empty directories … ”
    find .$SOURCE -empty -delete
    echo “done.”
    fi;