Entfernen Sie alle bis auf jede 12. Datei

14

Ich habe ein paar tausend Dateien im Format Dateiname.12345.end. Ich möchte nur jede 12. Datei behalten, also file.00012.end, file.00024.end ... file.99996.end und alles andere löschen.

Die Dateien können auch Nummern in ihrem Dateinamen haben und haben normalerweise die folgende Form: file.00064.name.99999.end

Ich verwende die Bash-Shell und kann nicht herausfinden, wie man die Dateien durchläuft, und dann die Nummer herausholt und prüfe, ob number%%12=0 die Datei gelöscht wird, wenn nicht. Kann mir jemand helfen?

Vielen Dank, Dorina

Dorina
quelle
Ist die Nummer der Datei nur vom Dateinamen abhängig?
Arronical
Haben die Dateien auch immer 5 Ziffern und sind Suffix und Präfix immer gleich?
Arronical
Ja, es sind immer 5 Ziffern. Ich bin mir nicht sicher, ob ich deine erste Frage richtig gestellt habe. Dateien mit unterschiedlichen Dateinamen sind unterschiedlich, und ich benötige diese spezifischen Dateien, die zufällig die Nummern 00012, 00024 usw. haben.
Dorina
3
@ Dorina bitte bearbeiten Sie Ihre Frage und machen Sie das klar. Es verändert alles!
Terdon
2
Und sie befinden sich alle im selben Verzeichnis, oder?
Sergiy Kolodyazhnyy

Antworten:

18

Hier ist eine Perl-Lösung. Dies sollte für Tausende von Dateien viel schneller sein:

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

Was kann weiter kondensiert werden in:

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

Wenn Sie zu viele Dateien haben und das einfache nicht verwenden können *, können Sie Folgendes tun:

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

Im Folgenden finden Sie einen Vergleich dieses Ansatzes mit dem in einer der anderen Antworten angegebenen Ansatz:

$ 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

Wie Sie sehen, ist der Unterschied erwartungsgemäß enorm .

Erläuterung

  • Es -ewird lediglich empfohlen perl, das in der Befehlszeile angegebene Skript auszuführen.
  • @ARGVist eine spezielle Variable, die alle Argumente für das Skript enthält. Da wir es geben *, enthält es alle Dateien (und Verzeichnisse) im aktuellen Verzeichnis.
  • Das grepdurchsucht die Liste der Dateinamen und sucht nach allen, die mit einer Folge von Zahlen, einem Punkt und end( übereinstimmen /(\d+)\.end/).

  • Da sich die Nummern ( \d) in einer Erfassungsgruppe (Klammern) befinden, werden sie als gespeichert $1. Also das grepwird dann prüfen , ob diese Zahl ein Vielfaches von 12 ist und, wenn dies nicht der Fall, wird der Dateiname zurückgegeben werden. Mit anderen Worten, das Array @badenthält die Liste der zu löschenden Dateien.

  • Die Liste wird dann übergeben, um unlink()Dateien (aber nicht Verzeichnisse) zu entfernen.

terdon
quelle
12

Da Ihre Dateinamen im Format vorliegen file.00064.name.99999.end, müssen wir zuerst alles außer unserer Nummer entfernen. Wir werden eine forSchleife verwenden, um dies zu tun.

Wir müssen der Bash-Shell auch mitteilen, dass sie die Basis 10 verwenden soll, da die Bash-Arithmetik sie mit einer 0 beginnende Zahlen als Basis 8 behandelt, was die Sache für uns durcheinander bringt.

Als Skript, das im Verzeichnis mit den Dateien gestartet werden soll, verwenden Sie:

#!/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

Oder Sie können diesen sehr langen, hässlichen Befehl verwenden, um dasselbe zu tun:

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

So erklären Sie alle Teile:

  • for f in ./* bedeutet für alles im aktuellen Verzeichnis, mache .... Dies setzt jede gefundene Datei oder jedes Verzeichnis als Variable $ f.
  • if [[ -f "$f" ]]prüft, ob es sich bei dem gefundenen Element um eine Datei handelt. Wenn dies nicht echo "$f is not...der Fall ist, springen wir zum Teil, was bedeutet, dass wir nicht versehentlich mit dem Löschen von Verzeichnissen beginnen.
  • file="${f%.*}"Legt die Variable $ file als Dateinamen fest und schneidet ab, was nach dem letzten kommt ..
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]]Hier setzt die Hauptarithmetik an. Das ${file##*.}schneidet alles vor dem letzten .in unserem Dateinamen ohne Erweiterung. $(( $num % $num2 ))ist die Syntax für die Bash-Arithmetik zur Verwendung der Modulo-Operation, die 10#Bash am Anfang anweist, die Basis 10 zu verwenden, um mit diesen nervigen führenden Nullen umzugehen. $((10#${file##*.} % 12))Dann lässt uns der Rest unserer Dateinamen durch 12 geteilt. -ne 0Überprüft, ob der Rest "ungleich" zu Null ist.
  • Wenn der Rest nicht gleich 0 ist, wird die Datei mit dem gelöschten rmBefehl, können Sie ersetzen rmmit , echowenn zuerst die ausgeführt wird , um zu überprüfen , dass Sie die erwarteten Dateien erhalten zu löschen.

Diese Lösung ist nicht rekursiv, dh, es werden nur Dateien im aktuellen Verzeichnis verarbeitet und keine Unterverzeichnisse angelegt.

Die ifAnweisung mit dem echoBefehl zum Warnen vor Verzeichnissen ist nicht unbedingt erforderlich, da rmVerzeichnisse von sich aus beanstandet und nicht gelöscht werden.

#!/bin/bash

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

Oder

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

Funktioniert auch einwandfrei.

Arronisch
quelle
5
rmEin paar tausend Anrufe können sehr langsam sein. Ich schlage vor , um echodie Dateinamen statt und Rohr den Ausgang der Schleife xargs rm(Add - Optionen je nach Bedarf): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
David Foerster
Ich habe sie bearbeitet, um Ihre vorgeschlagene Geschwindigkeitsverbesserung einzuschließen.
Arronical
Nach dem Testen in einem Verzeichnis mit 55999 Dateien dauerte die ursprüngliche Version 2 Minuten, 48 Sekunden und die xargsVersion 5 Minuten und 1 Sekunde. Könnte dies am Overhead von echo@DavidFoerster liegen?
Arronical
Seltsam. Für 60.000 Dateien erhalte ich 0m0.659s / 0m0.545s / 0m0.380s (real / user / sys) mit time { for f in *; do echo "$f"; done | xargs rm; }vs. 1m11.450s / 0m10.695s / 0m16.800s mit time { for f in *; do rm "$f"; done; }einem tmpfs. Bash ist v4.3.11, Kernel ist v4.4.19.
David Foerster
6

Sie können die Bash-Klammer-Erweiterung verwenden, um Namen zu generieren, die jede 12. Zahl enthalten. Lassen Sie uns einige Testdaten erstellen

$ 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

Dann können wir folgendes verwenden

$ 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

Funktioniert hoffnungslos langsam bei einer großen Anzahl von Dateien - es braucht Zeit und Speicher, um Tausende von Namen zu generieren - es ist also eher ein Trick als eine effektive Lösung.

Nykakin
quelle
Ich mag das Code-Golfen auf diesem.
David Foerster
1

Ein bisschen lang, aber es ist mir eingefallen.

 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

Erläuterung: Löschen Sie jede 12. Datei elf Mal.

Terrik
quelle
0

Trotz aller Bescheidenheit finde ich diese Lösung viel netter als die andere Antwort:

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

Eine kleine Erklärung: Zuerst generieren wir eine Liste von Dateien mit find. Wir erhalten alle Dateien, deren Name mit endet.end 1 und die eine Tiefe von 1 haben (das heißt, sie befinden sich direkt im Arbeitsverzeichnis und nicht in einem Unterordner. Sie können dies weglassen, wenn es keine Unterordner gibt). Die Ausgabeliste wird alphabetisch sortiert.

Dann leiten wir diese Liste in awk, wo wir die spezielle Variable verwenden, NRdie die Zeilennummer ist. Wir lassen jede 12. Datei aus, indem wir die Dateien dort ausdrucken, wo sie sind NR%12 != 0. Der awkBefehl kann auf abgekürzt werden awk 'NR%12', da das Ergebnis des Modulo-Operators als boolescher Wert interpretiert wird und der{print} implizit trotzdem erfolgt.

Jetzt haben wir eine Liste der Dateien, die gelöscht werden müssen, was wir mit xargs und rm tun können. xargsFührt den angegebenen Befehl aus (rm ) mit der Standardeingabe als Argument aus.

Wenn Sie über viele Dateien verfügen, erhalten Sie eine Fehlermeldung mit der Aufschrift "Argumentliste zu lang" (auf meinem Computer ist das Limit 256 kB, und POSIX benötigt mindestens 4096 Byte). Dies kann durch das -n 100Flag vermieden werden , das die Argumente alle 100 Wörter aufteilt (keine Zeilen, was zu beachten ist, wenn Ihre Dateinamen Leerzeichen enthalten) und einen separaten rmBefehl mit jeweils nur 100 Argumenten ausführt .

user593851
quelle
3
Es gibt ein paar Probleme mit Ihrem Ansatz: -depthmuss vorher sein -name; ii) dies schlägt fehl, wenn einer der Dateinamen Leerzeichen enthält; iii) Sie gehen davon aus, dass die Dateien in aufsteigender numerischer Reihenfolge aufgelistet werden (das ist es, worauf Sie awktesten), aber dies wird mit ziemlicher Sicherheit nicht der Fall sein. Dies löscht daher einen zufälligen Satz von Dateien.
Terdon
d'oh! Du hast ganz recht, meine schlechte (Kommentar bearbeitet). Ich habe den Fehler wegen der falschen Platzierung bekommen und konnte mich nicht erinnern -depth. Dies war jedoch das geringste Problem. Das wichtigste ist, dass Sie eine zufällige Gruppe von Dateien löschen und nicht die, die das OP haben möchte.
Terdon
Oh, und nein, -depthnimmt keinen Wert an und tut das Gegenteil von dem, was Sie denken, dass es tut. Siehe man find: "-depth Verarbeitet den Inhalt jedes Verzeichnisses vor dem Verzeichnis selbst." Dies wird also tatsächlich in Unterverzeichnisse absteigen und überall Chaos anrichten.
Terdon
I) Beides -depth nund -maxdepth nexistiert. Ersteres setzt voraus, dass die Tiefe genau n ist, und letzteres kann <= n sein. II). Ja, das ist schlecht, aber für dieses Beispiel ist es kein Problem. Sie können das Problem beheben, indem Sie find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rmdas Null-Byte als Datensatztrennzeichen verwenden (was in Dateinamen nicht zulässig ist). III) Auch in diesem Fall ist die Annahme angemessen. Andernfalls könnten Sie ein sort -nzwischen findund einfügen awkoder findzu einer Datei umleiten und sie sortieren, wie Sie möchten.
user593851
3
Ah, dann verwenden Sie wahrscheinlich OSX. Das ist eine ganz andere Implementierung von find. Das Hauptproblem ist jedoch, dass Sie davon ausgehen, dass findeine sortierte Liste zurückgegeben wird. Das tut es nicht.
Terdon
0

Um nur bash zu verwenden, würde mein erster Ansatz darin bestehen, 1. alle Dateien, die Sie behalten möchten, in ein anderes Verzeichnis zu verschieben (dh alle Dateien, deren Dateiname ein Vielfaches von 12 ist) und 2. alle verbleibenden Dateien im Verzeichnis zu löschen, 3. Setzen Sie dann die 12er-Dateien, die Sie behalten haben, an den Ort, an dem sie sich befanden. So etwas könnte funktionieren:

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
geschlafen
quelle
Ich mag den Ansatz, aber wie generieren Sie das filenameTeil, wenn es nicht konsistent ist?
Arronical