CLI-Adventskalender, Tag 8: find

CLI-Adventskalender: find

Avatar von Eric

Weil heute der 8. Dezember ist, geht es um find. Weil ihr fragt euch ja bestimmt auch, wo all die Zeit hingekommen ist und wieso das mit den Weihnachtsgeschenken schon langsam eng wird …

Man hat ja so selten Zeit zum aufräumen. Auch wenn ich – durch nachdrückliche Motivation seitens meiner Frau – inzwischen nicht mehr jedes Zimmer das ich bewohne in einen Drachenhort aus Müll verwandle. Auch wenn meine Schubladen inzwischen Label haben und die Grenzscharmützel in der Besteckschublade nur noch alle paar Monate aufkommen, sind meine digitalen Daten eine andere Geschichte.

Ich meine hier nicht die Git-Repos, an denen wir arbeiten. Die zwingen mich zur Ordnung (oder verstecken meinen Müll in den Ramschschubladen von git stash). Ich meine den Ramsch außen herum. Ich spreche von meinen Download-Ordner, von den Schlachtfeldern alter Mayday-Projekte, von den externen Festplatten auf denen mein digitales Leben der letzten zwanzig Jahre liebevoll gesichert ist.

Wer soll da noch irgendwas finden? In endlicher Zeit? Mit granularen Filtern?

find

Glücklicherweise gibt es ja Suchfunktionen und das Terminal ist da keine Ausnahme.

find /home/eric -name "passwords.kdbx"

find ist ein beeindruckendes Arbeitspferd. Es geht durch den kompletten Verzeichnisbaum und sucht alle vorkommenden Dateien, auf die die Filter zutreffen. Das heißt natürlich auf der anderen Seite, dass man es manchmal liebevoll von bestimmten Verzeichnissen wegsteuern möchte.

find -name 'node_modules' -prune -o -name 'ThingExplainer.js' -print

Filter lassen sich also aneinanderreihen, ohne dass man auf Pipes zurückgreifen muss. -prune bedeutet hier „wirf das hier weg“. Wenn das passiert ist -prune true und der zweite Teil (hinter dem -o, Kurzform von -or) wird nicht mehr ausgewertet.

Natürlich lässt sich das auch gleich mit einer fuzzysearch verbinden und direkt in das entsprechende Programm füttern.

cvlc "$(find ~/Downloads -name '*.mp3' | fzf)"

Löschen, sammeln oder weiterverarbeiten?

Wer mal seinen inneren Dalek rauslassen will und alle Dateien eines bestimmten Typs systematisch loswerden möchte, freut sich über diesen Befehl:

find git_repos/projekt -name "*.swp" -delete

find bringt außerdem ein eigenen Mechanismus mit, um Befehle auf jeder gefundenen Datei auszuführen.

find /tagebuch -name "*.txt" -exec tar -rvf archiv.tar {} \;

Schlüssel ist hier das -r, dass alle Dateien in das bestehende Archiv hinzufügt.

Natürlich hält uns niemand davon ab, unseren alten Freund xargs zu bemühen.

find /tmp -name '*mayflower*' -type f -print0 | xargs -I{} -0 cp {} /home/eric/will_ich_behalten/

-type f beschränkt uns hier auf Dateien. -print0 terminiert die Strings mit 0-Bytes anstelle von Zeilenumbrüchen. Das erlaubt uns Dateinamen mit Whitespace-Zeichen.

Granulare Filter

Okay, okay. Wir haben jetzt eine grobe Idee davon, was man damit machen kann. Welche Filter sind nun eigentlich möglich? So ziemlich alle Attribute, die eine Datei so hat. Alle Dateien größer als 50 Mb?

find /home/eric/Downloads -type f -size +50M

Alle Order die vor 2023 angelegt wurden? Ein wenig kniffliger, denn entweder nehmen wir -mtime und parsen ein Datum in Tagen rein …

find /home/eric/Documents -type d -mtime +$(($(date +%s -d '2023-01-01') / 86400 - $(date +%s) / 86400))


… oder wir negieren -newermt

find /home/eric/Documents -type d ! -newermt '2023-01-01'

Ein „älter als ein Jahr“ lässt -mtime wieder praktischer aussehen.

find /home/eric/Documents -mtime +365

Wir können auch anhand von Besitzern suchen …

find /tmp -user kevin

… oder Gruppenzugehörigkeit.

find /var/www -group www-data

Wir können auch nach gesetzten Berechtigungen suchen.

find /etc -perm 0777

Und ein besonderes Highlight ist das Finden von leeren Verzeichnissen und Dateien.

find /home/eric/repos -type d -empty

Engermaschige Netze

Alle diese Filter kann man beliebig kombinieren. Die logischen Operatoren verhalten sich so, wie man es erwartet.

  • -not oder ! bindet am stärksten.
  • -and oder -a bindet stärker als …
  • -or oder -o

Sobald ein Teil eines Oder-Blocks true ist, wird nichts weiter angewendet. Das ist wichtig, wenn man Blöcke mit -prune („Wirf das hier weg, geh hier nicht tiefer, nichts zu sehen, weitergehen …“) und -print („Das möchte ich in meinem Output haben.“) kennzeichnet. Ein erfolgreiches -prune evaluiert zu true.

Das -and ist in den meisten Fällen implizit, deswegen sieht man häufig nur -o.

find /home/eric/repos/projekt (-name '.git' -o -name 'node_modules') -prune -o -type f -size +4M -newermt '2023-01-01' ! -name '*.jpg'

Alle Dateien im Projekt (außer in .git und node_modules), die größer als 4 Mb sind und seit Anfang diesen Jahres hinzugefügt wurden und keine jpg-Dateien sind.

Pipes rein und raus

Wir haben gesehen, dass man den Output von find leicht in Pipes wiederverwenden kann. Aber man kann auch eine Liste von Verzeichnissen als Startpunkte in find hineinpipen! Wichtig ist hier wieder, die Zeilenumbrüche in Nullbytes zu übersetzen.

cat verzeichnisliste.txt | tr '\n' '\0' | find -files0-from=- -type f

find nistet sich also hervorragend in unseren Kommandozeilenwerkzeugkasten ein.

Shell-Weisheit des Tages
Bau Deine eigenen Werkzeuge!

Developerzeit ist kostbar. Dinge zu automatisieren ist verführerisch, dauert aber immer länger als man denkt. Dennoch …

Wenn man merkt, dass man dieselben Befehle immer wieder ausführt, wenn man merkt, dass sich Muster wiederholen, ist es vielleicht an der Zeit, ein kleines Skript zu schreiben.

Diese Weisheiten fangen langsam an, aufeinander aufzubauen. Da „Shell für die Ewigkeit“ ist, lohnt es sich also immer wieder, einen Blick in die manpages zu werfen. Aus diesem Grund – und weil „Automatisierung […] fest integriert“ ist – lohnt es sich, auf Kommandozeilenwerkzeuge zurückzugreifen. Vor allem iterativ.

Das Skript aus dem ersten Blogpost dieser Serie (mit dem man seine Stunden buchen kann), lässt sich mit Sicherheit noch mehr parametrisieren und erweitern. Es könnte zum Beispiel gleich auf das Ticket buchen, auf dessen Branch wir unterwegs sind.

--- SCHNIPP ---
# feature/TICKET-123 -> TICKET-123
# genauso für pr/TICKET-123, bugfix/TICKET-123, usw.
ticket=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | awk -F'/' '{print $NF}') 
# awk splittet den String auf dem "/" und nimmt das letzte Feld. Mehr zu awk hinter einem anderen Adventstürchen :)

if [ -n "$2" ]
then
  ticket=$2
fi

if [ -z "$ticket" ] || [ "$ticket" = "HEAD" ]
then
    echo "Kein Git-Repo oder detached HEAD."
    echo "Bitte Ticket-Nummer angeben."
    exit 1
fi
--- SCHNIPP ---

Wir wissen nie alles von vorne herein, selbst wenn es Arbeiten sind, die wir gefühlt hundertmal ausgeführt haben. Dieses „Bastler-Mindset“, über die eigene Arbeitsweise zu reflektieren, monotone Arbeiten nicht einfach hinzunehmen und immer wieder kleine Verbesserungen zu machen, ist das, was uns voranbringt. Das Leben ist geprägt von ständigem Lernen, Automatisierung ist nur die logische Fortführung desselbigen.

Avatar von Eric

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.