Usuń wszystkie oprócz 12 plików

14

Mam kilka tysięcy plików w formacie nazwa_pliku.12345.end. Chcę zachować tylko co 12 plik, więc file.00012.end, file.00024.end ... file.99996.end i usuwam wszystko inne.

Pliki mogą także zawierać numery wcześniej w nazwie pliku i zwykle mają postać: file.00064.name.99999.end

Używam powłoki Bash i nie potrafię wymyślić, jak przesłonić pliki, a następnie uzyskać numer i sprawdzić, czy number%%12=0 usuwa plik, jeśli nie. Czy ktoś może mi pomóc?

Dziękuję, Dorina

Dorina
źródło
Czy numer pliku zależy tylko od nazwy pliku?
Arronical
Ponadto, czy pliki zawsze mają 5 cyfr, a sufiks i prefiks są zawsze takie same?
Arronical
Tak, to zawsze 5 cyfr. Nie jestem pewien, czy dobrze odpowiem na twoje pierwsze pytanie. Pliki o różnych nazwach plików są różne i potrzebuję tych konkretnych plików, które mają numery 00012, 00024 itp.
Dorina,
3
@Dorina, edytuj swoje pytanie i wyjaśnij to. Zmienia wszystko!
terdon
2
I wszystkie są w tym samym katalogu, prawda?
Sergiy Kolodyazhnyy

Odpowiedzi:

18

Oto rozwiązanie Perla. Powinno to być znacznie szybsze dla tysięcy plików:

perl -e '@bad=grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV; unlink @bad' *

Które można dalej skondensować w:

perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

Jeśli masz zbyt wiele plików i nie możesz użyć tego prostego *, możesz zrobić coś takiego:

perl -e 'opendir($d,"."); unlink grep{/(\d+)\.end/ && $1 % 12 != 0} readdir($dir)'

Jeśli chodzi o szybkość, oto porównanie tego podejścia i powłoki podanej w jednej z pozostałych odpowiedzi:

$ touch file.{01..64}.name.{00001..01000}.end
$ ls | wc
  64000   64000 1472000
$ time for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

real    2m44.258s
user    0m9.183s
sys     1m7.647s

$ touch file.{01..64}.name.{00001..01000}.end
$ time perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

real    0m0.610s
user    0m0.317s
sys     0m0.290s

Jak widać, różnica jest ogromna, zgodnie z oczekiwaniami .

Wyjaśnienie

  • -eSię po prostu powiedzieć perl, aby uruchomić skrypt podany w wierszu poleceń.
  • @ARGVto specjalna zmienna zawierająca wszystkie argumenty podane w skrypcie. Ponieważ dajemy go *, będzie on zawierał wszystkie pliki (i katalogi) w bieżącym katalogu.
  • grepBędzie przeszukiwać listę nazw plików i patrzeć na te, które pasują ciąg cyfr, kropka i end( /(\d+)\.end/).

  • Ponieważ liczby ( \d) znajdują się w grupie przechwytywania (nawiasy), są zapisywane jako $1. Więc grepwtedy sprawdzić, czy liczba jest podzielna przez 12, a jeśli nie, to zostanie zwrócona nazwa pliku. Innymi słowy, tablica @badzawiera listę plików do usunięcia.

  • Następnie przekazywana jest lista, do unlink()której usuwa pliki (ale nie katalogi).

terdon
źródło
12

Biorąc pod uwagę, że twoje nazwy plików mają format file.00064.name.99999.end, najpierw musimy skrócić wszystko oprócz naszego numeru. W tym celu użyjemy forpętli.

Musimy także powiedzieć powłoce Bash, aby użyła bazy 10, ponieważ arytmetyka Bash potraktuje ich liczby zaczynające się od 0 jako bazę 8, co zepsuje nam wszystko.

Jako skrypt uruchamiany w katalogu zawierającym pliki należy użyć:

#!/bin/bash

for f in ./*
do
  if [[ -f "$f" ]]; then
    file="${f%.*}"
    if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
      rm "$f"
    fi
  else
    echo "$f is not a file, skipping."
  fi
done

Lub możesz użyć tego bardzo długiego brzydkiego polecenia, aby zrobić to samo:

for f in ./* ; do if [[ -f "$f" ]]; then file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; else echo "$f is not a file, skipping."; fi; done

Aby wyjaśnić wszystkie części:

  • for f in ./* oznacza wszystko dla bieżącego katalogu, wykonaj .... Ustawia każdy znaleziony plik lub katalog jako zmienną $ f.
  • if [[ -f "$f" ]]sprawdza, czy znaleziony element jest plikiem, jeśli nie, przechodzimy do echo "$f is not...części, co oznacza, że ​​nie zaczynamy przypadkowo usuwać katalogów.
  • file="${f%.*}"ustawia zmienną $ file jako przycinanie nazw plików niezależnie od tego, co nastąpi po ostatnim ..
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]] jest miejscem, gdzie rozpoczyna się główna arytmetyka ${file##*.} Przycina wszystko przed ostatnim .w naszej nazwie pliku bez rozszerzenia. $(( $num % $num2 ))jest składnią arytmetyki Bash używającej operacji modulo, 10#na początku mówi Bashowi, aby używał podstawy 10, aby radzić sobie z tymi irytującymi wiodącymi zerami. $((10#${file##*.} % 12))następnie pozostawia nam resztę liczby nazw plików podzieloną przez 12. -ne 0sprawdza, czy reszta nie jest „równa” zero.
  • Jeśli reszta nie jest równa 0, plik zostanie usunięty z rmpoleceniem, może chcesz zamienić rmze echopodczas pierwszego uruchomienia to, aby sprawdzić, czy można uzyskać oczekiwane pliki do usunięcia.

To rozwiązanie nie jest rekurencyjne, co oznacza, że ​​będzie przetwarzać tylko pliki w bieżącym katalogu, nie będzie przechodzić do żadnych podkatalogów.

ifSprawozdanie z echopoleceniem, aby ostrzec o katalogach nie jest naprawdę koniecznerm na swój własny będzie narzekać katalogów, a nie je usunąć, więc:

#!/bin/bash

for f in ./*
do
  file="${f%.*}"
  if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
    rm "$f"
  fi
done

Lub

for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

Będzie również działać poprawnie.

Arroniczny
źródło
5
Dzwonienie rmkilka tysięcy razy może być dość wolne. Proponuję echonazwę pliku zamiast rury i wyjście do pętli xargs rm(opcje Dodaj jako potrzebne): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
David Foerster,
Zredagowałem, aby uwzględnić sugerowaną poprawę prędkości.
Arronical
Właściwie po przetestowaniu katalogu z plikami 55999, oryginalna wersja zajęła 2 minuty 48 sekund, xargswersja zajęła 5 minut 1 sekundę. Czy może to być spowodowane narzutem na echo@DavidFoerster?
Arronical
Dziwny. Za 60 000 plików dostaję 0m0,659s / 0m0,545s / 0m0,380s (rzeczywisty / użytkownik / sys) w time { for f in *; do echo "$f"; done | xargs rm; }porównaniu z 1m11.450s / 0m10.695s / 0m16.800s z time { for f in *; do rm "$f"; done; }na tmpfs. Bash to v4.3.11, jądro to v4.4.19.
David Foerster,
6

Możesz użyć rozszerzenia nawiasów Bash do generowania nazw zawierających co 12 cyfry. Utwórzmy dane testowe

$ touch file.{0..9}{0..9}{0..9}{0..9}{0..9}.end # create test data
$ mv file.00024.end file.00024.end.name.99999.end # testing this form of filenames

Następnie możemy użyć następujących

$ ls 'file.'{00012..100..12}* # print these with numbers less than 100
file.00012.end                 file.00036.end  file.00060.end  file.00084.end
file.00024.end.name.99999.end  file.00048.end  file.00072.end  file.00096.end
$ rm 'file.'{00012..100000..12}* # do the job

Działa jednak beznadziejnie wolno w przypadku dużej liczby plików - generowanie tysięcy nazw zajmuje dużo czasu i pamięci - więc bardziej efektywne jest rozwiązanie.

Nykakin
źródło
Lubię grę w golfa na tym.
David Foerster,
1

Trochę długo, ale to właśnie przyszło mi do głowy.

 for num in $(seq 1 1 11) ; do
     for sequence in $(seq -f %05g $num 12 99999) ; do
         rm file.$sequence.end.99999;
     done
 done

Objaśnienie: Usuń co 12 plików jedenaście razy.

Terrik
źródło
0

Z całą pokorą uważam, że to rozwiązanie jest o wiele ładniejsze niż inna odpowiedź:

find . -name '*.end' -depth 1 | awk 'NR%12 != 0 {print}' | xargs -n100 rm

Małe wyjaśnienie: Najpierw generujemy listę plików find. Otrzymujemy wszystkie pliki, których nazwa kończy się na.end i których głębokość wynosi 1 (to znaczy są one bezpośrednio w katalogu roboczym, a nie w żadnych podfolderach. Możesz to pominąć, jeśli nie ma podfolderów). Lista wyników zostanie posortowana alfabetycznie.

Następnie potokujemy tę listę do awk, w której używamy specjalnej zmiennej, NRktóra jest numerem linii. Pomijamy każdy 12 plik, drukując pliki gdzie NR%12 != 0. awkKomenda może zostać skrócony do awk 'NR%12', ponieważ wynik operatora modulo zostanie zinterpretowane jako wartość logiczną i {print}jest niejawnie zrobić tak.

Mamy teraz listę plików, które należy usunąć, co możemy zrobić za pomocą xargs i rm. xargsuruchamia podaną komendę ( rm) ze standardowym wejściem jako argumentami.

Jeśli masz wiele plików, pojawi się błąd, mówiąc coś w rodzaju „zbyt długiej listy argumentów” (na moim komputerze limit ten wynosi 256 kB, a minimalny wymagany przez POSIX to 4096 bajtów). Można tego uniknąć za pomocą -n 100flagi, która dzieli argumenty co 100 słów (nie wiersze, na co należy uważać, jeśli w nazwach plików są spacje) i wykonuje osobne rmpolecenie, każde z tylko 100 argumentami.

użytkownik593851
źródło
3
Istnieje kilka problemów z twoim podejściem: -depthmusi być wcześniej -name; ii) to się nie powiedzie, jeśli którakolwiek z nazw plików zawiera spacje; iii) zakładasz, że pliki zostaną wyświetlone w porządku rosnącym numerycznie (właśnie awkto testujesz), ale prawie na pewno tak nie będzie. Dlatego spowoduje to usunięcie losowego zestawu plików.
terdon
och! Masz rację, mój zły (edytowany komentarz). Wystąpił błąd z powodu niewłaściwego umiejscowienia i nie pamiętam -depth. Mimo to był to najmniejszy problem, najważniejszy z nich to to, że usuwasz losowy zestaw plików, a nie te, których chce OP.
terdon
Och, i nie, -depthnie bierze wartości i robi coś przeciwnego do tego, co myślisz. Patrz man find: „-depth Przetwarzaj zawartość każdego katalogu przed samym katalogiem.”. Więc to faktycznie spadnie do podkatalogów i spowoduje spustoszenie w całym miejscu.
terdon
I) Oba -depth ni -maxdepth nistnieją. Pierwsza wymaga głębokości dokładnie n, a druga może wynosić <= n. II). Tak, to źle, ale w tym konkretnym przykładzie nie ma to znaczenia. Możesz to naprawić za pomocą find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rm, który używa bajtu zerowego jako separatora rekordów (co nie jest dozwolone w nazwach plików). III) Ponownie, w tym przypadku założenie jest uzasadnione. W przeciwnym razie możesz wstawić sort -npomiędzy findi awklub przekierować finddo pliku i posortować go w dowolny sposób.
user593851
3
Ach, prawdopodobnie wtedy używasz OSX. To zupełnie inna implementacja find. Ponownie jednak głównym problemem jest to, że zakładasz, że findzwraca posortowaną listę. Tak nie jest.
terdon
0

Aby użyć tylko bash, moim pierwszym podejściem byłoby: 1. przenieść wszystkie pliki, które chcesz zachować, do innego katalogu (tj. Wszystkie, których liczba w nazwie pliku jest wielokrotnością 12), a następnie 2. usunąć wszystkie pozostałe pliki w katalogu, następnie 3. umieść wiele z 12 plików, które zachowałeś tam, gdzie były. Więc coś takiego może działać:

cd dir_containing_files
mkdir keep_these_files
n=0
while [ "${n}" -lt 99999 ]; do
  padded_n="`echo -n "00000${n}" | tail -c 5`"
  mv "filename${padded_n}.end" keep_these_files/
  n=$[n+12]
done
rm filename*.end
mv keep_these_files/* .
rmdir keep_these_files
delt
źródło
Podoba mi się to podejście, ale jak wygenerować filenameczęść, jeśli nie jest spójna?
Arronical