Suche und entferne Duplikate in einem Verzeichnis

12

Ich habe ein Verzeichnis mit mehreren IMG-Dateien und einige von ihnen sind identisch, aber sie haben alle unterschiedliche Namen. Ich muss Duplikate entfernen, aber ohne externe Tools nur mit einem bashSkript. Ich bin ein Anfänger in Linux. Ich habe versucht, verschachtelte for-Schleifen zu vergleichen md5und je nach Ergebnis zu entfernen, aber etwas stimmt mit der Syntax nicht und es funktioniert nicht. irgendeine Hilfe?

was ich versucht habe ist ...

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

Ich bekomme: test: too many arguments

linuxbegin
quelle
Bitte geben Sie auch alle Fehlermeldungen an, die Sie in Ihrer Frage erhalten.
terdon
Warum können Sie keine externen Tools wie fdupes verwenden? Die Antwort von @terdon ist erstaunlich, aber es zeigt wirklich, warum die Verwendung eines guten Tools der richtige Weg ist, wenn möglich. Wenn es sich um eine dedizierte Hardware oder einen dedizierten Server handelt, können Sie möglicherweise weiterhin über ein Netzwerk usw. auf einem Computer zugreifen, auf dem Tools wie fdupes verfügbar sind.
Joe

Antworten:

28

Es gibt einige Probleme in Ihrem Skript.

  • Um das Ergebnis eines Befehls einer Variablen zuzuweisen , müssen Sie ihn zunächst entweder in backtics ( `command`) oder vorzugsweise in. Einschließen $(command). Sie haben es in einfachen Anführungszeichen ( 'command'), die den Befehl selbst als Zeichenfolge zuweisen, anstatt das Ergebnis Ihres Befehls Ihrer Variablen zuzuweisen. Daher ist Ihr testeigentlich:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
  • Das nächste Problem ist, dass der Befehl md5summehr als nur den Hash zurückgibt:

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

    Sie möchten nur das erste Feld vergleichen, daher sollten Sie die md5sumAusgabe analysieren, indem Sie einen Befehl ausführen, der nur das erste Feld druckt:

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

    oder

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | awk '{print $1}' 
  • Außerdem gibt der findBefehl viele Übereinstimmungen zurück, nicht nur eine, und jede dieser Übereinstimmungen wird von der zweiten dupliziert find. Das bedeutet, dass Sie irgendwann dieselbe Datei mit sich selbst vergleichen, die MD5-Summe identisch ist und am Ende alle Ihre Dateien gelöscht werden (ich habe dies in einem Testverzeichnis ausgeführt, das a.jpgund enthält 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
  • Sie möchten nicht ausgeführt werden, es for i in directory_pathsei denn, Sie übergeben ein Array von Verzeichnissen. Wenn sich alle diese Dateien in demselben Verzeichnis befinden, möchten Sie ausführen for i in $(find directory_path -iname "*.jpg", um alle Dateien zu durchsuchen.

  • Es ist eine schlechte Idee , forSchleifen mit der Ausgabe von find zu verwenden. Sie sollten whileSchleifen oder Globbing verwenden :

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

    oder, wenn sich alle Ihre Dateien in demselben Verzeichnis befinden:

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

    Abhängig von Ihrer Shell und den von Ihnen festgelegten Optionen können Sie Globbing auch für Dateien in Unterverzeichnissen verwenden, aber darauf wollen wir hier nicht näher eingehen.

  • Schließlich sollten Sie auch Ihre Variablen in Anführungszeichen setzen, da sonst Verzeichnispfade mit Leerzeichen Ihr Skript beschädigen.

Dateinamen können Leerzeichen, neue Zeilen, umgekehrte Schrägstriche und andere seltsame Zeichen enthalten. Um diese in einer whileSchleife korrekt zu behandeln, müssen Sie einige weitere Optionen hinzufügen. Was Sie schreiben möchten, ist so etwas wie:

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

Ein noch einfacherer Weg wäre:

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

Eine bessere Version, die mit Leerzeichen in Dateinamen umgehen kann:

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

Dieses kleine Perl-Skript durchläuft die Ergebnisse des find Befehls (dh die MD5-Summe und den Dateinamen). Die -aOption zum perlTeilen von Eingabezeilen mit Leerzeichen und zum Speichern im FArray enthält $F[0]die MD5-Summe und $F[1]den Dateinamen. Die md5sum wird im Hash gespeichert kund das Skript prüft, ob der Hash bereits gesehen wurde ( if $k{$F[0]}>1) und löscht die Datei, falls vorhanden ( system("rm $F[1]")).


Dies funktioniert zwar, ist jedoch für große Bildersammlungen sehr langsam und Sie können nicht auswählen, welche Dateien aufbewahrt werden sollen. Es gibt viele Programme, die auf elegantere Weise damit umgehen, darunter:

terdon
quelle
+1 für das Perl-Snippet. Wirklich elegant! Sie können auch Perls eigene verwenden, unlinkanstatt einen systemAnruf zu tätigen.
Joseph R.
@ JosephR. Vielen Dank :). Wäre ein Fehler aufgetreten, würde dies bei Dateinamen mit Leerzeichen fehlschlagen, da nur die ersten Zeichen eines Namens bis zum ersten Leerzeichen enthalten wären $F[1]. Problem mit Array-Slices behoben. Ich kenne unlink (), wollte aber die Perlismen auf ein Minimum beschränken, und der Systemaufruf ist einfacher zu verstehen, wenn Sie Perl nicht kennen.
Terdon
13

Es gibt ein raffiniertes Programm, fdupesdas den gesamten Vorgang vereinfacht und den Benutzer zum Löschen von Duplikaten auffordert. Ich denke, es lohnt sich zu überprüfen:

$ 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

Im Grunde wurde ich aufgefordert, die zu speichernde Datei auszuwählen . Ich gab 1 ein und entfernte die zweite.

Andere interessante Optionen sind:

-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

In Ihrem Beispiel möchten Sie es wahrscheinlich ausführen als:

fdupes --recurse --delete --noprompt DIRECTORY_WITH_DUPLICATES

Siehe man fdupesfür alle Optionen zur Verfügung.

Teresa und Junior
quelle