znajdź i usuń duplikaty w katalogu

12

Mam katalog z wieloma plikami img, a niektóre z nich są identyczne, ale wszystkie mają różne nazwy. Muszę usunąć duplikaty, ale bez narzędzi zewnętrznych tylko ze bashskryptem. Jestem początkującym w Linuksie. Próbowałem zagnieździć pętlę for, aby porównać md5sumy i w zależności od wyniku usunąć, ale coś jest nie tak ze składnią i to nie działa. jakaś pomoc?

próbowałem ...

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

Dostaję: test: too many arguments

linuxbegin
źródło
Dołącz także wszelkie komunikaty o błędach pojawiające się w pytaniu.
terdon
Dlaczego nie możesz używać zewnętrznych narzędzi, takich jak FDUPES? Odpowiedź @terdona jest niesamowita, ale naprawdę podkreśla, dlaczego warto skorzystać z dobrego narzędzia, jeśli to możliwe. Jeśli jest to jakiś dedykowany sprzęt lub serwer, nadal możesz mieć do niego dostęp przez sieć itp. Z komputera, na którym dostępne są narzędzia takie jak fdupes.
Joe

Odpowiedzi:

28

W twoim skrypcie jest sporo problemów.

  • Po pierwsze, aby przypisać wynik polecenia do zmiennej, musisz ująć ją w backtics ( `command`) lub, najlepiej, w $(command). Masz go w pojedynczych cudzysłowach ( 'command'), które zamiast przypisywać wynik polecenia do zmiennej, przypisują samo polecenie jako ciąg znaków. Dlatego twoje testjest w rzeczywistości:

    $ echo "test $sum1=$sum2"
    test find $i -type f -iname "*.jpg" -exec md5sum {} \;=find $j -type f -iname "*.jpg" -exec md5sum {} \;
  • Następnym problemem jest to, że polecenie md5sumzwraca więcej niż tylko skrót:

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

    Chcesz tylko porównać pierwsze pole, więc powinieneś przeanalizować dane md5sumwyjściowe, przekazując je za pomocą polecenia, które wypisuje tylko pierwsze pole:

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

    lub

    find $i -type f -iname "*.png" -exec md5sum '{}' \; | awk '{print $1}' 
  • Ponadto findpolecenie zwróci wiele dopasowań, a nie tylko jedno, a każde z nich zostanie powtórzone przez drugi find. Oznacza to, że w pewnym momencie będziesz porównywał ten sam plik z samym sobą, suma md5 będzie identyczna i skończysz na usuwaniu wszystkich plików (uruchomiłem to w katalogu testowym zawierającym a.jpgi 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
  • Nie chcesz uruchamiać, for i in directory_pathchyba że przekazujesz tablicę katalogów. Jeśli wszystkie te pliki znajdują się w tym samym katalogu, chcesz uruchomić for i in $(find directory_path -iname "*.jpg"), aby przejrzeć wszystkie pliki.

  • Nie jest dobrym pomysłem stosowanie forpętli z wyjściem find. Powinieneś użyć whilepętli lub globowania :

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

    lub jeśli wszystkie twoje pliki znajdują się w tym samym katalogu:

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

    W zależności od powłoki i ustawionych opcji możesz używać globowania nawet dla plików w podkatalogach, ale nie wchodźmy w to tutaj.

  • Na koniec powinieneś również podać swoje zmienne, inaczej ścieżki katalogu ze spacjami spowodują uszkodzenie skryptu.

Nazwy plików mogą zawierać spacje, nowe linie, ukośniki odwrotne i inne dziwne znaki, aby poprawnie radzić sobie z nimi w whilepętli, musisz dodać więcej opcji. To, co chcesz napisać, to:

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

Jeszcze prostszym sposobem byłoby:

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

Lepsza wersja, która radzi sobie ze spacjami w nazwach plików:

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

Ten mały skrypt Perla przejdzie przez wyniki findpolecenia (tj. Md5sum i nazwa pliku). -aOpcja dla perllinii wejściowych dzieli na białych znaków i zapisuje je na Ftablicy, więc $F[0]będzie md5sum i $F[1]nazwa pliku. Wartość md5sum jest zapisywana w haszu, ka skrypt sprawdza, czy hash został już wyświetlony ( if $k{$F[0]}>1), i usuwa plik, jeśli ma ( system("rm $F[1]")).


Chociaż to zadziała, będzie bardzo powolne w przypadku dużych kolekcji obrazów i nie można wybrać, które pliki zachować. Istnieje wiele programów, które obsługują to w bardziej elegancki sposób, w tym:

terdon
źródło
+1 za fragment Perla. Naprawdę elegancki! Możesz także użyć własnego Perla unlinkzamiast nawiązywać systempołączenia.
Joseph R.
@JosephR. dzięki :). Gdyby wystąpił błąd, zawiodłby dla nazw plików ze spacjami, ponieważ byłyby w nim tylko pierwsze znaki nazwy aż do pierwszej spacji $F[1]. Naprawiono to za pomocą wycinków tablicy. Co do unlink (), wiem, ale chciałem ograniczyć perlizmy do minimum, a wywołanie systemowe jest łatwiejsze do zrozumienia, jeśli nie znasz Perla.
terdon
13

Istnieje sprytny program o nazwie, fdupesktóry upraszcza cały proces i monituje użytkownika o usunięcie duplikatów. Myślę, że warto sprawdzić:

$ 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

Zasadniczo podpowiedział mi, który plik zachować , wpisałem 1 i usunąłem drugi.

Inne interesujące opcje to:

-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

Z twojego przykładu prawdopodobnie chcesz uruchomić go jako:

fdupes --recurse --delete --noprompt DIRECTORY_WITH_DUPLICATES

Zobacz man fdupeswszystkie dostępne opcje.

Teresa e Junior
źródło