rechercher et supprimer les doublons dans un répertoire

12

J'ai un répertoire avec plusieurs fichiers img et certains d'entre eux sont identiques mais ils ont tous des noms différents. J'ai besoin de supprimer les doublons mais sans outils externes uniquement avec un bashscript. Je suis débutant sous Linux. J'ai essayé la boucle imbriquée pour comparer les md5sommes et selon le résultat, mais quelque chose ne va pas avec la syntaxe et cela ne fonctionne pas. de l'aide?

ce que j'ai essayé c'est ...

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

Je reçois: test: too many arguments

linuxbegin
la source
Veuillez également inclure tout message d'erreur que vous obtenez dans votre question.
terdon
Pourquoi ne pouvez-vous pas utiliser des outils externes comme fdupes? La réponse de @terdon est étonnante, mais elle souligne vraiment pourquoi l'utilisation d'un bon outil est la voie à suivre si possible. S'il s'agit d'une sorte de matériel ou de serveur dédié, vous pouvez toujours y accéder via un réseau, etc. à partir d'une machine qui dispose d'outils tels que fdupes.
Joe

Réponses:

28

Il y a pas mal de problèmes dans votre script.

  • Premièrement, afin d'assigner le résultat d'une commande à une variable, vous devez l'enfermer soit dans backtics ( `command`), soit de préférence $(command). Vous l'avez entre guillemets simples ( 'command') qui, au lieu d'affecter le résultat de votre commande à votre variable, affecte la commande elle-même sous forme de chaîne. Par conséquent, vous testêtes en fait:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
  • Le problème suivant est que la commande md5sumrenvoie plus que le hachage:

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

    Vous souhaitez uniquement comparer le premier champ, vous devez donc analyser la md5sumsortie en la passant par une commande qui imprime uniquement le premier champ:

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

    ou

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | awk '{print $1}' 
  • De plus, la findcommande renverra de nombreuses correspondances, pas une seule et chacune de ces correspondances sera dupliquée par la seconde find. Cela signifie qu'à un moment donné, vous comparerez le même fichier à lui-même, la somme md5 sera identique et vous finirez par supprimer tous vos fichiers (j'ai exécuté cela sur un répertoire de test contenant a.jpget 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
  • Vous ne voulez pas exécuter for i in directory_pathsauf si vous passez un tableau de répertoires. Si tous ces fichiers sont dans le même répertoire, vous souhaitez exécuter for i in $(find directory_path -iname "*.jpg") pour parcourir tous les fichiers.

  • C'est une mauvaise idée d'utiliser des forboucles avec la sortie de find. Vous devez utiliser des whileboucles ou des globes :

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

    ou, si tous vos fichiers se trouvent dans le même répertoire:

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

    Selon votre shell et les options que vous avez définies, vous pouvez utiliser la globalisation même pour les fichiers dans les sous-répertoires, mais n'abordons pas cela ici.

  • Enfin, vous devez également citer vos variables, sinon les chemins de répertoire avec des espaces briseront votre script.

Les noms de fichiers peuvent contenir des espaces, de nouvelles lignes, des barres obliques inverses et d'autres caractères étranges, pour les traiter correctement dans une whileboucle, vous devrez ajouter quelques options supplémentaires. Ce que vous voulez écrire, c'est quelque chose comme:

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

Une manière encore plus simple serait:

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

Une meilleure version qui peut gérer les espaces dans les noms de fichiers:

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

Ce petit script Perl passera par les résultats de la findcommande (ie le md5sum et le nom du fichier). L' -aoption de perlfractionnement des lignes d'entrée à l'espace blanc et les enregistre dans le Ftableau, $F[0]sera donc le md5sum et $F[1]le nom de fichier. Le md5sum est enregistré dans le hachage ket le script vérifie si le hachage a déjà été vu ( if $k{$F[0]}>1) et supprime le fichier s'il a ( system("rm $F[1]")).


Bien que cela fonctionne, cela sera très lent pour les grandes collections d'images et vous ne pouvez pas choisir les fichiers à conserver. Il existe de nombreux programmes qui gèrent cela de manière plus élégante, notamment:

terdon
la source
+1 pour l'extrait de code Perl. Vraiment élégant! Vous pouvez également utiliser Perl's unlinkau lieu de passer un systemappel.
Joseph R.
@JosephR. Merci :). Cependant, s'il y avait un bogue, il échouerait pour les noms de fichiers avec des espaces car seuls les premiers caractères d'un nom jusqu'au premier espace figureraient $F[1]. Corrigé en utilisant des tranches de tableau. Quant à unlink () je sais, mais je voulais garder les perlismes au minimum et l'appel système est plus facile à comprendre si vous ne connaissez pas Perl.
terdon
13

Il existe un programme astucieux appelé fdupesqui simplifie l'ensemble du processus et invite l'utilisateur à supprimer les doublons. Je pense que cela vaut la peine de vérifier:

$ 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

Fondamentalement, il m'a demandé quel fichier conserver , j'ai tapé 1 et il a supprimé le second.

D'autres options intéressantes sont:

-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

D'après votre exemple, vous voudrez probablement l'exécuter en tant que:

fdupes --recurse --delete --noprompt DIRECTORY_WITH_DUPLICATES

Voir man fdupespour toutes les options disponibles.

Teresa e Junior
la source