**I would strongly recommend a total backup of all your data before proceeding. After that, create a new account and play-around from the new account.** "Advanced Bash-Scripting Guide" covers everything with numerous examples. http://www.tldp.org/guides.html It the fist manual listed. Bash is powerful but exacting. Should be able to find what you need in the existing code. The code eliminates all duplicate files by moving only one copy of each unique file to a new directory. ( It's been 9 years since I worked on this code. It deletes things too. ) The existing code creates hashes of all the files in a folder. It compares the hashes to find duplicates. You should be able to substitute a rename at this point. It has code to create a unique name for some reason. The code contains extensive debugging code to create a log of it's doing. Turn on the log and follow what it is doing. #!/bin/bash # Purpose of this script: # # Accurately eliminate duplicate files within a given folder. Optionally, consolidate all files # to a common folder. Duplicate names within the consolidate folder will be made unique. # # Methodology: # Find folders within a specified top folder which are not bundles. Find files # within each found folder [and within subfolders ]. because each folder is # handled individually.) # Find potential bundles and checksum their entire content as one item. # Get MD5 checksums for each item and save an index of all checksums and # filepaths. Sort the index by checksum. Compare each # item's checksum and if a duplicate is found, move the duplicate to the trash. # Compare each item's name and if a duplicate is found, append incrementing #X # to the name prior to the last period (as in "Filename #2.jpg") # Ignore iTunes and iPhoto folders # # Optionally move files and potential bundles to the top-level folder # Delete .DS_Store, .FBCIndex*, and .FBCSemaphoreFile files and subsequently # delete empty subfolders. Optionally regroup map files into folders based on # the name of .frq file if found, or .shp file # # This script is a partial rewrite of the code from JulieJulieJulie's Consolidate17 script. # See: # http://discussions.apple.com/message.jspa?messageID=11577992#11577992 # # additional discussion: # https://discussions.apple.com/thread/2434165?start=0&tstart=0 # https://discussions.apple.com/thread/8276669 # # Copyright 2018 rccharles # # GNU General Public License # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 3 # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # For a copy of the GNU General Public License see # <http://www.gnu.org/licenses/>. # copyRight="Copyright (c) 2010 & 2018 rccharles. GNU General Public License." # # # debug info export PS4='+(${BASH_SOURCE}:${LINENO}):' ## not in the tiger version of bash ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' ########## ### Functions (subroutines which can be called by name) function helpHelpIsAllINeed () { echo echo "${savedCommandName##*/} [ options ] [ directory-to-compress ]" echo echo "options:" echo "-h Display quick help." echo echo "-m Moving files is disabled." echo "-M Move files to top level." echo echo "-r Regrouping map files is disabled." echo "-R Regroup map files." echo echo "-V Display very detailed messages." echo echo "example:" echo " ./${savedCommandName##*/} -mrV /Users/mac/cons/see" echo } function Show_Time () { let currentRunTime="$( date +%s ) - ${Seconds_since_the_epoch_start}" let stepRunTime=currentRunTime-previousRunTime echo " Elapsed time in seconds since start of processing: ${currentRunTime}" \ " Time of this step: ${stepRunTime}" echo let previousRunTime=currentRunTime } function echoTrashInfo () { echo " -${trashFileName}" echo "${fileName} > ${trashFileName}" \ >>"${saveFolder}/Consolidate_Trashed_List_${runDateTime}.txt" case "${fileType}" in f ) let trashFileCount+=1 ;; d ) let trashDirectoryCount+=1 ;; * ) Echo Echo "Error --- Error in echoTrashInfo. fileName=${fileName};" Echo "Error --- fileType=${fileType};" Echo ;; esac } declare folder \ MainFolder \ MoveFiles="Question" \ RegroupMapFiles="Question" \ runDateTime=$(date '+%Y-%m-%d_%H.%M') \ Seconds_since_the_epoch_start declare -x tab=$'\t' \ trashFolder \ verbose="Yes" \ veryVerbose="Yes" \ whereIsTrash declare -i count=0 \ offsetMainFolder \ currentRunTime \ previousRunTime=0 \ stepRunTime \ trashDirectoryCount=0 \ trashFileCount=0 declare -a filePath \ fileName \ fileSum \ folders savedCommandName="$0" echo echo "${savedCommandName} script revised $(GetFileInfo -m $0)" echo bigVersion="Lizard 4" echo "==============================================================" echo echo " ${bigVersion}" echo " ${bigVersion}" echo " ${bigVersion}" echo echo "==============================================================" echo echo "${copyRight}" echo # If the utility md5 is not present then just quit, it's needed for the checksums. [ ! -x /sbin/md5 ] && exit 2 # Check for command-line options used when calling the script if [ $# -gt 0 ] ; then while getopts "hmMrRV" Option ; do case "${Option}" in h ) helpHelpIsAllINeed echo "Bye for now." exit 7 ;; m ) echo "-m argument used on command line, moving files is disabled." echo MoveFiles="No" ;; M ) echo "Moving files. -M argument used on command line." echo MoveFiles="Yes" ;; r ) echo "-r argument used on command line, regrouping map files is disabled." echo RegroupMapFiles="No" ;; R ) echo "Regrouping map files. -R argument used on command line." echo RegroupMapFiles="Yes" ;; V ) echo "Display very detailed messages. -V argument used on command line." echo veryVerbose="Yes" ;; * ) echo "Unknown argument among arguments $* on command line." helpHelpIsAllINeed exit 6 ;; esac done fi # We're done with switches / options from the command line shift $(($OPTIND - 1)) # Main folder can be declared on the command line or interactively MainFolder="${1}" # If the main folder doesn't exist, prompt once, then just quit if it's still not there. if [ ! -d "${MainFolder}" ] ; then echo "Folder not specified. Type the path to the top-level folder or drag" echo "it into this Terminal window, then click back in this window and press return:" read MainFolder echo fi [ ! -d "${MainFolder}" ] \ && echo "Folder not found. Was ${MainFolder}" \ && exit 4 [ "${MainFolder}" = "/" ] \ && echo "You must be crazy." \ && exit 5 echo "Holding down Control-c on the keyboard will stop this script in an emergency." echo if [ "${MoveFiles}" = "Question" ] ; then echo "WARNING: allowing the script to move files could cause problems!" echo echo "Do you want to move all files and bundles to the top level at the end of the run?" read -n1 -p "Press 'n' to NOT move files and keep the folder structure intact; otherwise press any key. " if [ "${REPLY}" = "n" -o "${REPLY}" = "N" ] ; then MoveFiles="No" else MoveFiles="Yes" fi echo fi if [ "${RegroupMapFiles}" = "Question" ] ; then echo "Do you want to disable regrouping of map files into folders based on the .shp filename?" read -n1 -p "Press 'n' to NOT regroup map files; otherwise press any key. " if [ "${REPLY}" = "n" -o "${REPLY}" = "N" ] ; then RegroupMapFiles="No" else RegroupMapFiles="Yes" fi echo fi [ "${veryVerbose}" = "Yes" ] \ && ulimit -a \ && df \ && echo [ "${veryVerbose}" = "Yes" ] \ && echo "MainFolder=${MainFolder};" # get physical path if [ -d "$MainFolder" ]; then pushd $PWD cd "$MainFolder" MainFolder=$( /bin/pwd -P ) popd else # just to be sure. echo "You need to supply a directory name for ${MainFolder}" exit 8 fi let offsetMainFolder="${#MainFolder}"-1 # Ensure that the path is in a consistant format. i.e. no // # drop trailing directory separator if needed [ "${MainFolder:${offsetMainFolder}}" = "/" ] \ && MainFolder="${MainFolder:0:${offsetMainFolder}}" # Place all created files in one directory. saveFolder="${MainFolder}/saved--Consolidated--Data" # Figure out the trash folder name. Each user has his or her own trash folder. # Each volume has a different trash folder. On the startup drive, # the trash folders are in the users home folder. On the other volumes, the # trash folder contains a folder with the user id for each user. # # This isn't perfect. if [ $( echo -n "${MainFolder:0:7}" | tr '[:upper:]' '[:lower:]' ) \ = "/users/" ] ; then whereIsTrash="u" trashFolder="${HOME}/.Trash" elif [ $( echo -n "${MainFolder:0:9}" | tr '[:upper:]' '[:lower:]' ) \ = "/volumes/" ] ; then whereIsTrash="v" diskName="${MainFolder:9}" diskName="${diskName%%/*}" trashFolder="/Volumes/${diskName}/.Trashes" if [ ! -d ${trashFolder} ] ; then # Needs work. Should be ok, since folder is created when formated. Echo "Creating trash folder. ${trashFolder};" saveUmask=$( umask ) mkdir ${trashFolder} umask ${saveUmask} fi currentId=$( id -ru ) trashFolder="${trashFolder}/${currentId}" if [ ! -d ${trashFolder} ] ; then Echo "Creating user's individual trash folder. ${trashFolder};" saveUmask=$( umask ) umask 077 mkdir ${trashFolder} umask ${saveUmask} fi else echo "Not sure where main folder resides. defulat to ~/.Trash" whereIsTrash="u" trashFolder="${HOME}/.Trash" fi [ "${veryVerbose}" = "Yes" ] \ && echo "MainFolder=${MainFolder};" \ && echo "saveFolder=${saveFolder};" \ && echo "trashFolder=${trashFolder};" [ ! -d "${saveFolder}" ] \ && mkdir "${saveFolder}" #Housekeeping for Index files. # If the index archive file does not exist, # create it so that it will be there for sorting and merging later [ ! -e "${saveFolder}/Index_Archive.txt" ] \ && touch "${saveFolder}/Index_Archive.txt" # If the index file exists, archive it and be sure to delete the index. if [ -e "${saveFolder}/Index.txt" ] ; then echo "----------------------------" >> "${saveFolder}/Index_Archive.txt" cat "${saveFolder}/Index.txt" >> "${saveFolder}/Index_Archive.txt" rm "${saveFolder}/Index.txt" fi touch "${saveFolder}/Index.txt" # In case of a crash, clean up from previous run. [ -e "${saveFolder}/names.txt" ] \ && rm "${saveFolder}/names.txt" touch "${saveFolder}/names.txt" # Ensure that the trashed list file will exist even if no files were trashed [ ! -e "${saveFolder}/Consolidate_Trashed_List_${runDateTime}.txt" ] \ && touch "${saveFolder}/Consolidate_Trashed_List_${runDateTime}.txt" [ "${veryVerbose}" = "Yes" ] \ && ls -l "${saveFolder}/Index.txt" \ && ls -l "${saveFolder}/names.txt" \ && ls -l "${saveFolder}/Consolidate_Trashed_List_${runDateTime}.txt" \ && ls -ld "${trashFolder}" \ && ls -ld "${trashFolder}/.." \ && echo echo "--> Deleting .DS_Store and .FBC files from subfolders." find -d "${MainFolder}" -type f \( -name ".DS_Store" -o -name ".FBCIndex*" \ -o -name ".FBCSemaphoreFile" \) -delete if [ "${veryVerbose}" = "Yes" ] ; then echo "Gathering debug information. May take a moment. Counting files." fileCountBefore=$( find ${MainFolder} -type f \ \! \( -path "${saveFolder}" -o -path "${saveFolder}/*" \) \ | wc -l ) trashCountBefore=$( find ${trashFolder} -type f | wc -l ) fi # Set the internal field separator to newline to preserve spaces in file paths IFS=$'\n' # Set the start time now that the user interaction is done Seconds_since_the_epoch_start=$( date +%s ) ########## ### Find items echo "--> Finding folders and ignoring any folder whose name contains a period and it's content (potential packages)." echo "--> then finding files & generating list of MD5 checksums for file content." find "${MainFolder}" -type d \ \! \( -name "*.*" -o -name "iPhoto Library" -o -name "iTunes" \) \ \! \( -path "*/*.*/*" -o -path "*/iPhoto Library/*" -o -path "*/iTunes/*" \ -o -path "${saveFolder}" -o -path "${saveFolder}/*" \) \ | ( while read folder do [ "${veryVerbose}" = "Yes" ] \ && echo "folder=${folder};" # Perform a 'find' within each of the folders for files in that folder # (and not in subdirectories) Skip hidden files. find "${folder}" -maxdepth 1 -type f \! -name ".*" \ | ( while read theFilePath do # Get an MD5 checksum for each file's combined content of both # data and resource forks fileCheckSum=$( cat "${theFilePath}" "${theFilePath}/rsrc" | md5 ) echo " ${fileCheckSum} ${theFilePath}" echo "${fileCheckSum}${tab}f${tab}${theFilePath}" \ >> "${saveFolder}/Index.txt" done ) done ) Show_Time echo "--> Finding and processing potential packages/bundles." echo "--> Note; we look for a period in the base filename which" \ "may not be acurate in all cases." # Find folders within $MainFolder which are possible packages and bundles find "${MainFolder}" -type d \ \( -iname "*.*" -o -iname "iPhoto Library" -o -iname "iTunes" \) \ \! \( -name ".*" -o -ipath "*/*.*/*" -o -ipath "*/iPhoto Library/*" \ -o -ipath "*/iTunes/*" \) \ | ( while read folder do fileCheckSum=$( find "${folder}" -type f \! -name ".DS_Store" \ -exec cat '{}' '{}/rsrc' \; | md5 ) echo "${fileCheckSum} ${folder}" echo "${fileCheckSum}${tab}d${tab}${folder}" >> "${saveFolder}/Index.txt" done ) ######### ;;;;;;;; echo Show_Time echo "--> Sorting list of files and potential bundles by checksum then" echo " Compare checksums." sort -t "${tab}" "${saveFolder}/Index.txt" | ( declare previousCheckSum="" declare -i trashCount=0 # Process both files and application bundles. while read theData do checkSum="${theData:0:32}" fileType="${theData:33:1}" fileName="${theData:35}" [ "${veryVerbose}" = "Yes" ] \ && echo "${checkSum} ${fileName};" if [ "${checkSum}" = "${previousCheckSum}" ] ; then # Duplicate file. Move current file to trash. [ "${verbose}" = "Yes" ] \ && echo -n " =" # Make up trash name. # Base name on path name so we can remember # where file came from trashFileName="${fileName}" case "${whereIsTrash}" in u ) trashFileName="${trashFileName:7}" trashFileName="${trashFileName#*/}" # chop user id ;; v) trashFileName="${trashFileName:9}" ;; * ) echo "<><><><> Serious error. whereIsTrash=${whereIsTrash};" esac trashFileName=$(echo -n "${trashFileName}" | tr "/" "~" ) trashFileName="${trashFolder}/${trashFileName}" # don't let a directory be copyied into another directory. # Happens with applications. # Might as well avoid files too. [ ! -e "${trashFileName}" ] \ && mv -n "${fileName}" "${trashFileName}" if [ -e "${fileName}" ] ; then # Move didn't work. myRandom=$RANDOM trashFileName="${trashFileName}~$(date '+%Y-%m-%d_%H.%M')~${myRandom}" mv -n "${fileName}" "${trashFileName}" if [ -e "${fileName}" ] ; then # Odd , second move didn't work. echo echo "big time error " echo "<><> fileName=${fileName};" \ "myRandom=${myRandom}" \ "trashFileName=${trashFileName};" echo else # Ok, moved file to trash echoTrashInfo fi else # Ok, moved file to trash echoTrashInfo fi else # Output data for next step. # Mac OS X hfs+ defaults to case independent names, so # sort on lower case. # ie A folder may not contain the names abc & Abc. lowerBaseName=$( echo -n "${fileName##*/}" | \ tr '[:upper:]' '[:lower:]' ) echo "${lowerBaseName}${tab}${fileName}" \ >>"${saveFolder}/names.txt" fi previousCheckSum="${checkSum}" done outLine="Total trashed files: ${trashFileCount}" outLine="${outLine} Total trashed applications/bundles: ${trashDirectoryCount}" echo echo "${outLine}" echo { echo echo "${outLine}" echo }>> "${saveFolder}/Consolidate_Trashed_List_${runDateTime}.txt" ) Show_Time ########## ### Compare names echo "--> Sorting list of names." # Sort (by basename) sort -t "${tab}" "${saveFolder}/names.txt" | ( declare previousBaseName="" \ baseName declare -i alterCount let alterCount=0 while read theData do echo "..${theData}" lowerBaseName="${theData%${tab}*}" fileName="${theData#*${tab}}" baseName="${fileName##*/}" [ "${veryVerbose}" = "Yes" ] \ && echo "${lowerBaseName}; ${baseName}; ${fileName};" if [ "${previousBaseName}" = "${lowerBaseName}" ] ; then [ "${verbose}" = "Yes" ] \ && echo -n " =" # Rename files with duplicate names by appending #X prior to # the filename extension (before the last period.) directoryName="${fileName%/*}" extension="${baseName##*.}" # Was an extension found? if [ "${extension}" != "${baseName}" ] ; then # extension found, since a period was in the data extension=".${extension}" name="${baseName%.*}" else extension="" name="${baseName}" fi while [ "1" = "1" ] ; do # do forever... simulate do until let alterCount+=1 actualAlteration="${alterCount}" if [ ${alterCount} -gt 9999 ] ; then echo echo "================== odd: alterCount too big!!! " echo "previousBaseName=${previousBaseName}; baseName=${baseName}; fileName=${fileName}; " echo "================== odd: alterCount too big!!! " echo actualAlteration="${alterCount}${RANDOM}" break fi # Manually implement do until. # If file doesn't exist, break. Unused name found. [ ! -e "${directoryName}/${name} #${actualAlteration}${extension}" ] \ && break done # rename newName="${directoryName}/${name} #${actualAlteration}${extension}" mv -n "${fileName}" "${newName}" if [ -e "${fileName}" ] ; then # Odd , rename didn't work. echo echo "another big time error " echo "<><><> fileName=${fileName};" \ "newName=${newName}" echo else # all went well with rename echo " +${newName}" fi else let alterCount=0 fi previousBaseName="${lowerBaseName}" done ) Show_Time ########## ### Moving items to top-level if [ "${MoveFiles}" = "Yes" ] ; then echo "--> Finding possible packages/bundles and moving them to the top-level." find "${MainFolder}" -mindepth 1 -type d \ \( -iname "*.*" -o -iname "iPhoto Library" -o -iname "iTunes" \) \ \! \( -name ".*" -o -ipath "*/*.*/*" -o -ipath "*/iPhoto Library/*" \ -o -ipath "*/iTunes/*" \) \ -exec mv -n '{}' "${MainFolder}" \; echo "--> Finding files and moving them to the top-level." folders=( $(find "${MainFolder}" -mindepth 1 -type d \ \! \( -name "*.*" -o -name "iPhoto Library" -o -name "iTunes" \) \ \! \( -path "*/*.*/*" -o -path "*/iPhoto Library/*" -o -path "*/iTunes/*" \ -o -path "${saveFolder}" -o -path "${saveFolder}/*" \) ) ) # Perform a 'find' within each of the folders for files in that folder # (and not in subdirectories) for folder in ${folders[*]} ; do find "${folder}" -maxdepth 1 -type f \! -name ".*" -exec mv -n '{}' "${MainFolder}" \; done fi echo "--> Deleting empty subfolders. (This only deletes folders which are completely empty.)" find -d "${MainFolder}" -type d -empty -delete ########## ### Regroup map files into their own folders if [ "${RegroupMapFiles}" = "Yes" ] ; then echo "--> Finding map file groups in top-level and regrouping them to their own folders in the top-level." # deal with .frq first, .frq indicates group with differing filename end prior to .shp Maps=( $( find "${MainFolder}" -maxdepth 1 -iname "*.frq" ) ) for Map in ${Maps[*]} ; do [ ! -e "${Map}" ] && continue [ "${Map}" = "" ] && continue fullfilename=$( basename "${Map}" ) filename="${fullfilename%.*}" if [ ! -e "${saveFolder}/${filename}.map" ] ; then mkdir "${saveFolder}/${filename}.map" find "${MainFolder}" -maxdepth 1 -iname "${filename}*.*" -type f \ -exec mv -n '{}' "${saveFolder}/${filename}.map" \; fi done Maps=( $( find "${MainFolder}" -maxdepth 1 -iname "*.shp" ) ) for Map in ${Maps[*]} ; do [ ! -e "${Map}" ] && continue [ "${Map}" = "" ] && continue fullfilename=$( basename "${Map}" ) filename="${fullfilename%.*}" if [ ! -e "${saveFolder}/${filename}.map" ] ; then mkdir "${saveFolder}/${filename}.map" find "${MainFolder}" -maxdepth 1 -iname "${filename}.*" -type f \ -exec -n mv '{}' "${saveFolder}/${filename}.map" \; fi done fi Show_Time echo if [ "${veryVerbose}" = "Yes" ] ; then echo "Gathering post run debug information. May take a moment." \ "Counting files." fileCountAfter=$( find ${MainFolder} -type f \ \! \( -path "${saveFolder}" -o -path "${saveFolder}/*" \) \ | wc -l ) trashCountAfter=$( find ${trashFolder} -type f | wc -l ) let trashDiff=trashCountAfter-trashCountBefore let calculateFileCountAfter=fileCountAfter+trashDiff echo echo "Information on files and directories in folder/directory ${MainFolder}" echo "fileCountBefore=${fileCountBefore};" \ "fileCountAfter=${fileCountAfter};" echo echo "trashDiff=${trashDiff}; " \ "trashCountBefore=${trashCountBefore};" \ "trashCountAfter=${trashCountAfter};" echo echo "===========" echo "calculateFileCountAfter=${calculateFileCountAfter};" echo if [ "${fileCountBefore}" -ne "${calculateFileCountAfter}" ] ; then echo "- - - - - - - -" echo "- - - - - - - - Error: Some files have gone missing." echo "- - - - - - - -" \ "fileCountBefore should be equal to calculateFileCountAfter" echo "- - - - - - - - Where files deleted from the trash?" echo "- - - - - - - -" fi fi echo "DONE." echo "NOTE: Invisible files and files with unresolved name conflicts may be left within any remaining subfolders!" echo echo "${savedCommandName} script revised $(GetFileInfo -m $0)" echo echo "Bye from ${bigVersion}" echo exit 0 Here is an example of the output: [https://pastebin.com/zVWRDA9E][2] additional references. This script is a partial rewrite of the code from JulieJulieJulie's Consolidate17 script. See: [http://discussions.apple.com/message.jspa?messageID=11577992#11577992][3] additional discussion: [https://discussions.apple.com/thread/2434165?start=0&tstart=0][3] [https://discussions.apple.com/thread/8276669][3] [1]: https://pastebin.com/raw/RQTXZVt6 [2]: https://pastebin.com/zVWRDA9E [3]: https://discussions.apple.com/thread/8276669