buscar y eliminar duplicados en un directorio

12

Tengo un directorio con varios archivos img y algunos de ellos son idénticos, pero todos tienen nombres diferentes. Necesito eliminar duplicados pero sin herramientas externas solo con un bashscript. Soy un principiante en Linux. Intenté anidar para el bucle para comparar md5sumas y, dependiendo del resultado, eliminar, pero algo está mal con la sintaxis y no funciona. ¿alguna ayuda?

lo que he intentado es ...

for i in directory_path; do
    sum1='find $i -type f -iname "*.jpg" -exec md5sum '{}' \;'
    for j in directory_path; do
        sum2='find $j -type f -iname "*.jpg" -exec md5sum '{}' \;'
        if test $sum1=$sum2 ; then rm $j ; fi
    done
done

Yo obtengo: test: too many arguments

linuxbegin
fuente
Incluya también cualquier mensaje de error que reciba en su pregunta.
terdon
¿Por qué no puedes usar herramientas externas como fdupes? La respuesta de @terdon es sorprendente, pero realmente resalta por qué usar una buena herramienta es el camino a seguir si es posible. Si se trata de algún tipo de hardware o servidor dedicado, es posible que aún pueda acceder a él a través de una red, etc., desde una máquina que tenga herramientas como fdupes disponibles.
Joe

Respuestas:

28

Hay bastantes problemas en su script.

  • En primer lugar, con el fin de asignar el resultado de un comando a una variable que necesita para encerrarlo ya sea en backtics ( `command`) o, preferiblemente, $(command). Lo tiene entre comillas simples ( 'command') que, en lugar de asignar el resultado de su comando a su variable, asigna el comando en sí como una cadena. Por lo tanto, tu testes en realidad:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
  • El siguiente problema es que el comando md5sumdevuelve más que solo el hash:

    $ md5sum /etc/fstab
    46f065563c9e88143fa6fb4d3e42a252  /etc/fstab

    Solo desea comparar el primer campo, por lo que debe analizar el md5sumresultado pasándolo a través de un comando que solo imprime el primer campo:

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | cut -f 1 -d ' '

    o

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | awk '{print $1}' 
  • Además, el findcomando devolverá muchas coincidencias, no solo una y cada una de esas coincidencias se duplicará por la segunda find. Esto significa que en algún momento comparará el mismo archivo consigo mismo, el md5sum será idéntico y terminará eliminando todos sus archivos (ejecuté esto en un directorio de prueba que contiene a.jpgy b.jpg):

    for i in $(find . -iname "*.jpg"); do
      for j in $(find . -iname "*.jpg"); do
         echo "i is: $i and j is: $j"
      done
    done   
    i is: ./a.jpg and j is: ./a.jpg   ## BAD, will delete a.jpg
    i is: ./a.jpg and j is: ./b.jpg
    i is: ./b.jpg and j is: ./a.jpg
    i is: ./b.jpg and j is: ./b.jpg   ## BAD will delete b.jpg
  • No desea ejecutar a for i in directory_pathmenos que esté pasando una serie de directorios. Si todos estos archivos están en el mismo directorio, desea ejecutar for i in $(find directory_path -iname "*.jpg") para revisar todos los archivos.

  • Es una mala idea usar forbucles con la salida de find. Debe usar whilebucles o globbing :

    find . -iname "*.jpg" | while read i; do [...] ; done

    o, si todos sus archivos están en el mismo directorio:

    for i in *jpg; do [...]; done

    Dependiendo de su shell y las opciones que haya configurado, puede usar globbing incluso para archivos en subdirectorios, pero no entremos en eso aquí.

  • Finalmente, también debe citar sus variables; de lo contrario, las rutas de directorio con espacios romperán su script.

Los nombres de archivo pueden contener espacios, nuevas líneas, barras invertidas y otros caracteres extraños, para tratarlos correctamente en un whilebucle necesitará agregar algunas opciones más. Lo que quieres escribir es algo como:

find dir_path -type f -iname "*.jpg" -print0 | while IFS= read -r -d '' i; do
  find dir_path -type f -iname "*.jpg" -print0 | while IFS= read -r -d '' j; do
    if [ "$i" != "$j" ]
    then
      sum1=$(md5sum "$i" | cut -f 1 -d ' ' )
      sum2=$(md5sum "$j" | cut -f 1 -d ' ' )
      [ "$sum1" = "$sum2" ] && rm "$j"
    fi
  done
done

Una forma aún más simple sería:

find directory_path -name "*.jpg" -exec md5sum '{}' + | 
 perl -ane '$k{$F[0]}++; system("rm $F[1]") if $k{$F[0]}>1'

Una mejor versión que puede manejar espacios en los nombres de archivo:

find directory_path -name "*.jpg" -exec md5sum '{}' + | 
 perl -ane '$k{$F[0]}++; system("rm \"@F[1 .. $#F]\"") if $k{$F[0]}>1'

Este pequeño script de Perl se ejecutará a través de los resultados del findcomando (es decir, el md5sum y el nombre del archivo). La -aopción para perldividir líneas de entrada en espacios en blanco y guardarlas en la Fmatriz, $F[0]será md5sum y $F[1]el nombre del archivo. El md5sum se guarda en el hash ky el script comprueba si el hash ya se ha visto ( if $k{$F[0]}>1) y elimina el archivo si lo tiene ( system("rm $F[1]")).


Si bien eso funcionará, será muy lento para grandes colecciones de imágenes y no puede elegir qué archivos guardar. Hay muchos programas que manejan esto de una manera más elegante, incluyendo:

terdon
fuente
+1 para el fragmento de Perl. Muy elegante! También puede usar el propio Perl en unlinklugar de hacer una systemllamada.
Joseph R.
@JosephR. Gracias :). Sin embargo, tenía un error, fallaría para los nombres de archivo con espacios ya que solo estarían los primeros caracteres de un nombre hasta el primer espacio $F[1]. Se solucionó utilizando rebanadas de matriz. En cuanto a unlink (), lo sé, pero quería mantener los perlismos al mínimo y la llamada al sistema es más fácil de entender si no conoce a Perl.
terdon
13

Hay un programa ingenioso llamado fdupesque simplifica todo el proceso y solicita al usuario que elimine los duplicados. Creo que vale la pena verificarlo:

$ fdupes --delete DIRECTORY_WITH_DUPLICATES
[1] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz        
[2] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz.1

Set 1 of 1, preserve files [1 - 2, all]: 1

   [+] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz
   [-] DIRECTORY_WITH_DUPLICATES/package-0.1-linux.tar.gz.1

Básicamente, me solicitó qué archivo guardar , escribí 1 y eliminó el segundo.

Otras opciones interesantes son:

-r --recurse
    for every directory given follow subdirectories encountered within

-N --noprompt
    when used together with --delete, preserve the first file in each set of duplicates and delete the others without prompting the user

A partir de su ejemplo, probablemente quiera ejecutarlo como:

fdupes --recurse --delete --noprompt DIRECTORY_WITH_DUPLICATES

Ver man fdupespara todas las opciones disponibles.

Teresa e Junior
fuente