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


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).


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.


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.


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.


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.


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

Podoba mi się to podejście, ale jak wygenerować filenameczęść, jeśli nie jest spójna?
Arronical
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.