Shell: # find -exec funktioniert nicht

  • Zugriff per SSH auf die NAS shell

    In einem Verzeichnis mit vielen Ordnern liegen viele Dateien die mehrfach vorhanden sind. Typischerweise ist deren Name wie

    [Dateiname](1).xxx und [Dateiname](2).xxx und seltener [Dateiname](3).xxx. Diese sollen alle entfernt (gelöscht) werden.


    Test: Gebe ich in einem Unterverzeichnis den Befehl ein:

    # find ./ -type f -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*'

    dann wird ausgegeben (Beispiel):

    Code
    ./20160415-QNAP_Turbo_NAS_Hardware_Manual_German-Kopieren(1).pdf
    ./QTS_4.3.x_SMB_de-Kopieren(1).pdf
    ./TS-X51_20180315-4.3.4.0516-Kopieren(1).zip
    ./QTS_4.3.x_SMB_de-Kopieren(2).pdf
    ./TS-X51_20180315-4.3.4.0516-Kopieren(2).zip


    Wenn ich diese mit -exec rm nun löschen möchte und den Befehl mit Enter eingebe

    # find ./ -type f -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' -exec rm -vrf '{}' \;

    ist das Resultat:

    Code
    #

    aber nichts wurde gelöscht. Im Beispiel hätten die oben aufgelisteten Dateien entfernt werden müssen. Sie sind aber nach wie vor vorhanden.


    Habe jetzt schon eine Stunde im Internet gesucht und die Linux find man-page gelesen, kann aber keinen Fehler entdecken. :handbuch:

    Es muss wohl eine Besonderheit der Shell in QTS 5.00.1986 sein oder gar ein Bug - Oder, was mach ich falsch?


    Danke für Rückmeldungen und Hinweise vorab.


    Grüße von Tom

  • -exec rm -vrf '{}' \;

    Warum wollen alle eigentlich immer unnötige Kind-Prozesse von rm erzeugen nur um eine Datei zu löschen?

    # find ./ -type f -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' -delete

    Dann kann das alles EIN find-Prozess erledigen, ohne das "23" rm-Prozesse geforkt werden müssen.

    Außerdem, wenn Du den Befehl auf einem QNAP machst könnte die Chance hoch sein, dass der rm einfach silent terminieren, weil er -v nicht kennt und -r auf eine Datei keinen Sinn macht.


    Das sagt mein QNAP, wenn ich nach rm -? frage.

    Code
    Usage: rm [-irf] FILE...
    
    Remove (unlink) FILEs
    
            -i      Always prompt before removing
            -f      Never prompt
            -R,-r   Recurse
  • Danke Barungar für deine Antwort. OK der 'rm' Befehl braucht hier kein '-r' und '-v' funktionert mit 'rm' nicht - eigentlich weiß ich das, hatte aber etwas kopiert, was schon falsch war, so ist das, wenn man copy&paste macht.

    Nun habe ich meine eigentlichen Fehler gefunden.
    Die Syntax des find-Befehls war in mehrfacher Hinsicht falsch. Was zur Ausgabe der Dateinamen funktioniert, geht nicht mit '-exec rm' und auch nicht mit

    '-delete'.

    Der erste Punkt ist der ODER Operator in

    Code
    -name '*(1).*' -or -name '*(2).*' ... 

    dieser funktioniert NICHT.

    Der zweite Punkt ist, dass für '-delete' und auch für '-exec rm' das Muster korrekt angegeben werden muss, wozu '(' ')' '.' escapt werden müssen. Der Befehl der funktioniert ist - aufgegliedert in 3 sep. Befehlszeilen:

    Code
    find ./ -type f -name '*\(1\)\.*' -delete   (oder -exec rm -f '{}' \;)
    find ./ -type f -name '*\(2\)\.*' -delete   (oder -exec rm -f '{}' \;)
    find ./ -type f -name '*\(3\)\.*' -delete   (oder -exec rm -f '{}' \;)

    Es gibt zwar laut find man page auch eine Lösung für "Traversing the filesystem just once - for 2 different actions" - doch das hat auch nicht funktioniert und wird wahrscheinlich nicht von BusyBox v1.24.1 unterstützt.

    6 Mal editiert, zuletzt von tomybyte () aus folgendem Grund: Antwort komplett überarbeitet

  • Nee, das Muster in -name '*(1).*' ist voll okay, da muss man nix escapen. Du musste es allerdings auch nicht so kompliziert machen.


    Deine Version:find ./ -type f -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' -exec rm -vrf '{}' \;

    Du braucht weder den / hinter dem Punkt, noch musst Du den Name-Pattern dreimal mit einem -or wiederholen, und das -exec-Thema mit dem unnötigen Fork hatten wir schon. ;)


    Maximal optimiert bzw. gekürzt heißt der Befehl einfach nur: find . -type f -name '*([123]).*' -delete

  • Die 3 folgenden Befehle haben in sehr kurzer Zeit alle überzähligen Kopien in einem großen Verzeichnis mit 195 GByte entfernt - das hatte ohne escapen NICHT funktioniert - wieso auch immer:

    Code
    find ./ -type f -name '*\(1\)\.*' -delete
    find ./ -type f -name '*\(2\)\.*' -delete
    find ./ -type f -name '*\(3\)\.*' -delete

    Die Größe dieses Verzeichnis ist nun 80 GByte was wohl stimmen dürfte.

    ---

    PS das funktioniert auch, hab es getestet:

    Code
    find ./ -type f -name '*\(1\)\.*' -exec rm -f '{}' \; 

    Was nun schneller ausgeführt wird könnte man mal exemplarisch testen. Spielt für mich aber keine Rolle. Übrigens bin ich nur alle paar Wochen mal an der Shell und muss mich mit Linux beschäftigen, entweder an meiner TS251 oder an meinem Ubuntu Webserver. Programmieren mache ich mit PHP und JavaScript. Meine Erfahrungen mit regulären Ausdrücken an der Shell ist, dass man in Linux sich leicht irren kann, was geht und was nicht, da sich hier die Distributionen z.T. unterscheiden und QTS BusyBox dazu noch sehr speziell ist.

    2 Mal editiert, zuletzt von tomybyte ()

  • Natürlich kannst Du ein paar Messungen versuchen. Das würde dann ideal, wenn der Messfehler nicht zu groß wird, die praktische Antwort liefern.

    Das hier (fast) jede der Lösungen funktioniert habe ich auch nicht bezweifelt, ich könnte Dir auch noch vier weitere Lösungen liefern, die zum Ziel führen werden.

    Wenn es aber darum geht einen optimierten Befehl abzusetzen, dann kann man das Folgende sagen.


    Code: Codemuster A
    # find ./ -type f -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' -exec rm -vrf '{}' \;

    Ich habe das "Codemuster A" korrigiert, so das es auch funktioniert -->

    Code
    # find ./ -type f \( -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' \) -exec rm -vrf '{}' \; 

    Und ja, Du hattest eigentlich nur vergessen die Conditions zu klammen. Ich habe den fehlenden Klammern mal in rot hervorgehoben. Ein "escapen" von Zeichen ist auch weiterhin nicht notwendig.


    Code: Codemuster B
    # find ./ -type f -name '*\(1\)\.*' -delete
    # find ./ -type f -name '*\(2\)\.*' -delete
    # find ./ -type f -name '*\(3\)\.*' -delete

    Im Vergleich zu "Codemuster A" wurde, anstatt die Condition zu klammern, der Befehl in drei getrennte Befehle überführt, zusätzlich wurden einige Zeichen "escaped". - Kann man machen, muss man aber an der Stelle nicht.


    Code: Codemuster C
    # find . -type f -name '*([123]).*' -delete


    So, nun ein paar theoretische Gedanken zur Performace der drei Codemuster.

    • Codemuster A
      Es wird der Datenbestand einmal von find durchsucht. Es wird auf Dateien (files) gefiltert. Die Namen werden auf drei Muster überprüft.
      Bei jedem einzelnen Treffer wird eine neue Instanz von rm erstellt, die den Auftrag erhält diesen einen Treffer zu löschen.
    • Codemuster B
      Es wird der Datenbestand dreimal von find durchsucht. Es wird bei jedem Durchlauf auf Dateien (files) gefiltert. Die Namen werden auf ein Muster geprüft.
      Bei einem Treffer wird die Datei von find gelöscht.
    • Codemuster C
      Es wird der Datenbestand einmal von find durchsucht. Es wird auf Dateien (files) gefiltert. Die Namen werden auf drei Muster überprüft.
      Bei einem Treffer wird die Datei von find gelöscht.


    Die Ausführungstheorie sagt nun, am schlechtesten ist Codemuster B, dann folgt Codemuster A und optimal sollte (in der Theorie!) Codemuster C performen. Das sind, wie eingangs erwähnt, theoretische Gedanken, die sind in der Praxis nicht getestet.


    Ergänzung

    Ich habe mir ein kleines Testfeld gebaut... dort befanden sich 100 Testdatei, drei Dateien mit passenden Namen "Datei_(1).txt", "Datei_(2).txt" und "Datei_(3).txt". Die anderen 97 Dateien einfach irgendwelcher Schrott.


    Gemessen wurde mit dem unix-Befehle time.


    Codemuster A:

    Code
    # time find ./ -type f \( -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' \) -exec rm -vrf '{}' \;
    real    0m0.006s
    user    0m0.002s
    sys  0m0.004s

    Codemuster B:

    Code
    # time find ./ -type f -name '*\(1\)\.*' -delete; find ./ -type f -name '*\(2\)\.*' -delete; find ./ -type f -name '*\(3\)\.*' -delete
    real    0m0.010s
    user    0m0.003s
    sys  0m0.007s

    Codemuster C:

    Code
    # time find . -type f -name '*([123]).*' -delete
    
    real    0m0.004s
    user    0m0.002s
    sys  0m0.002s

    Natürlich ist das ein extrem kleines Testfeld und somit sind die Messfehler vermutlich sehr hoch. Ich hatte aber gerade keine Lust mir ein Test-Labor mit 95 G Daten in 20.000 Dateien zu bauen. :D Aber was man sieht... Code A läuft 6 ms, Code B läuft 10 ms (also mehr als 1,5x so lange) und Code C braucht 4 ms - war also in diesem Setup der optimale Befehl.

    Betrachtet man die Laufzeit relativ von Code C aus, so läuft Code A 50% und Code B 150% länger.


    Da Code A und Code C beide nur einmal die Daten vom Datenträger lesen und gegen ein dreifach Muster vergleichen, kann einzig der Fork des rm-Prozesses die Erhöhung der Laufzeit verschulden. Bei Code B reißt das dreimalige Lesen der Daten vom Datenträger jegliche Hoffnung auf Performance mit sich ins Verderben.


    Interessant wäre die "Messung" auf einem richtig dicken Datenbestand. ;)

    8 Mal editiert, zuletzt von Barungar ()

  • Schätzungsweise 100.000 Dateien mit ca. 100 GByte Daten wurden mit der 3 geteilten Befehlsfolge:

    Code
    # find ./ -type f -name '*\(1\)\.*' -delete
    # find ./ -type f -name '*\(2\)\.*' -delete
    # find ./ -type f -name '*\(3\)\.*' -delete

    in wenigen Sekunden gelöscht - es ging so schnell, dass ich sehr erstaunt war, wie schnell das ging.

    Wenn ich die Dateien noch hätte würde ich das nun mal exakt testen. Aber Zeittests an der Shell sind mit Vorsicht zu genießen, vor allem mit sehr wenig Dateien.


    Ich denke nach dieser Erfahrung, dass eine Unterscheidung der verwendeten Befehlszeilen zwar theoretisch interessant sind, praktisch jedoch weniger relevant.


    Wie gesagt, leider habe ich die Duplikate nicht mehr, sonst würde ich das erneut testen und einen Screencast machen. Denn ich hatte sehr wohl die Befehle mit -or als auch ohne Escapen der speziellen Zeichen getestet. Zwar ließen sich diese fehlerfrei aufrufen, der Test mit ls zeigte jedoch, dass die Dateien, die eigentlich hätten gelöscht sein sollten, noch im Verzeichnis waren, wieso auch immer. Und ich habe das mehrfach versucht. Die Befehlszeilenfolge wie angegeben lief dann problemlos durch. Der Test mit

    Code
    find ./ -type f -name '*(1).*' -or -name '*(2).*' -or -name '*(3).*' 

    lieferte kein Ergebniss. Alle Kopien waren demnach gelöscht.


    Das sind meine Erfahrungen. Ein weiteres Interesse habe ich jetzt daran nicht, das nächste mal werde ich dann intensiver meine Kenntnis regulärer Ausdrücke zur Erstellung der Suchpattern mit find nutzen, jetzt weiß ich, dass das funktionieren sollte.