2004-11-21 Autor: Markus Raab viele Dateien bearbeiten 1. Einleitung Oft steht man vor der Aufgabe viele Dateien mit gleichen oder ähnlichen Operationen zu verändern. Die Kommandozeile ist ideal dafür geeignet. Es ist damit möglich Dateinamen nach einem bestimmten Muster zu verändern, Konvertierungsbefehle unter bestimmten Bedingungen für bestimmte Dateien durchzuführen oder es können auch einzelne Zeichen in Dateinamen ausgetauscht werden. 2. Konstrukte 2.1 for variable in filelist Eine Häufige Variante ist die for Schleife. Doch sie hat einen entscheidenen Nachteil. Die Dateiliste will man meistens mit einem find Befehl übergeben. Es funktioniert auch alles so weit, außer es kommen Leerzeichen innerhalb des Dateinamens vor. Dann bekommt die Varible den Wert von den einzelnen Wörtern und nicht von den Dateinamen, was natürlich nicht erwünscht ist. markus@superbyte:~/test$ touch 'with spaces' markus@superbyte:~/test$ for i in *; do echo $i; done with spaces markus@superbyte:~/test$ for i in `find .`; do echo $i; done . ./with spaces 2.2 Abhilfe: while read Um diese und andere Probleme zu verhindern, gibt es eine andere Konstruktion, die mit read funktioniert. markus@superbyte:~/test$ find . | while read file; do echo "$file"; done . ./with spaces 2.2.1 read Read ist ein built-in command der Bash. Man kann damit von der Standardeingabe oder Dateideskriptoren (-u) zeilenweise einer Variable zuweisen. Die Variable wird als letzter Parameter angegeben. Wird sie weggelassen, wird der Wert automatisch der Variable REPLY zugewiesen. Besonders intressant von den Parametern ist -d womit man den Delimiter einstellen kann. Damit kommt man auf die komplett richtige Version, die wirklich alle Möglichkeiten von Dateinamen richtig handhabt. In Dateinamen können nämlich auch zeilenterminierenden Zeichen vorkommen (wie z.b. Enter). markus@superbyte:~/test$ touch "with > enter" markus@superbyte:~/test$ find . | while read file; do echo "Datei: $file"; done Datei: . Datei: ./with Datei: enter Datei: ./with spaces Man sieht, dass der Dateiname mit Enter nicht richtig funktioniert, da read das Enter als terminierend ansieht und damit glaubt with und enter sind zwei eigene Zeilen. Mit -n kann man die obere Grenze von eingelesenen Zeichen von read festlegen. Dabei wird aber natürlich der komplette Pfad kontrolliert, nicht wie lange der tatsächliche Dateiname lang ist. 2.2.2 Endversion Für genau diesen Fall gibt es bei find (siehe 2.2.2) den Parameter -print0, wo die Dateinamen mit einem Nullbyte terminiert ausgegeben werden. markus@superbyte:~/test$ find . -print0 | while read -d "" file > do echo "Datei: $file"; done Datei: . Datei: ./with enter Datei: ./with spaces 2.2.3 mit xargs Mit xargs ist eine ähnliche Konstruktion möglich. Der Vorteil ist, dass der Befehl immer automatisch mit der maximalen Anzahl von Argumenten aufgerufen wird und dafür um etliches schneller ist. Der Nachteil (aus dem selben Grund) ist, dass nicht jeder Befehl verwendet werden kann. Z.b. cp, mv verlangen immer Quelle und Ziel und nicht eine Liste von allen Dateien. find-print0 | xargs -0 Beispiel [0]: find /etc -type f 2> /dev/null -print0 | xargs -0 grep -i imap 2&1 | less Mehr dazu siehe (Kapitel 5). 2.2.2 find 2.2.2.1 Syntax Find wird dazu verwendet eine Liste von Dateien zu erzeugen. Die grundsätzliche Syntax lautet folgendermaßen: find [Verzeichnis] [-Tests] [-Aktionen] Für alle Optionen ist am besten die man-Page zu konsultieren. 2.2.2.2 Tests Die häufigsten Tests sind: -group Name Die Datei gehört zur Gruppe Name. -name Muster Der Name der Datei paßt zu dem Muster. Bei dem Muster (siehe 3.) sollte man '' verwenden, damit Spezialzeichen wie *,? o.ä. von find und nicht von der Bash interpretiert werden. Anmerkung: Mit iname wird die Groß- u. Kleinschreibung ignoriert. -path Muster Der Name des Pfades paßt zu Muster. Anmerkung: Mit ipath wird die Groß- u. Kleinschreibung ignoriert. -type Hier kann man nach bestimmten Typen von Dateien suchen. So ist es möglich nur Dateien, Verzeichnisen oder symbolische Links zu finden. b gepufferte Gerätedatei für ein blockorientiertes Gerät c ungepufferte Gerätedatei für ein zeichenorientiertes Gerät d Verzeichnis p benannte Pipeline (FiFo) f normale Datei l symbolischer Link s Socket -user Name Die Datei gehört dem Benutzer Name. Tests werden normalerweise und verknüpft, d.h. nur wenn alle wahr sind, wird die Datei tatsächlich verwendet. Reicht es, wenn eine Bedingung wahr ist, kann man mit oder verknüpfen, indem man zwischen jedem Test ein -or einfügt. 2.2.2.3 Aktionen * Die wichtigsten Aktionen sind: -print0 Gibt den Dateinamen mit dem Nullzeichen terminiert aus. 2.2.2.4 Beispiele find / -name '*.jpg' -or -name '*.bmp' Findet alle Dateien mit den Dateiendungen: jpg und bmp auf dem System. find /home -group user -type d Listet alle Verzeichnisse von der Gruppe User auf, welche sich im Verzeichnis /home befinden. find / -name '*~' -path '*tmp*' Sucht Backupdateien mit ~ (Tilde) am Schluß in allen temporären Ordnern systemweit. 2.3 test test ist ein built-in der bash, es ist aber auch als normales Programm vorhanden, ist also nicht an die bash gebunden. Mit diesem Programm kann man Strings und Zahlen vergleichen und außerdem diverse Abfragen in Bezug auf Dateien durchführen. Ich möchte an dieser Stelle auf die man-page hinweisen schreibe aber die wichtigsten Befehle hinaus. AUSDRUCK -a AUSDRUCK ... "und" Verknüpfung AUSDRUCK -o AUSDRUCK ... "oder" Verknüpfung ! AUSDRUCK ... invertiere Ausdruck STRING = STRING ... Zeichenketten sind gleich ZAHL -eq ZAHL ... Zahlen sind gleich -e DATEI ... Datei existiert -f DATEI ... - "" - und ist regulär -d DATEI ... - "" - und ist ein Verzeichnis 2.3.1 Beispiele if test -e beispiel.txt then echo "Datei existiert" else echo "Datei existiert nicht" fi 2.4 tr Auf dieses Tool sei hingewiesen, da man es ideal verwenden kann um einzelne Zeichen auszutauschen. Die man page gibt genaue Infos darüber. 2.5 Zusammenfassung: SUCHBEFEHL | while read VARIABLE ist eine elegante Möglichkeit in einer Schleife bestimmte Operationen mit einer gewünschten Liste von Dateien durchzuführen. Der SUCHBEFEHL muss eine Liste von Dateien ausgeben. Die Zeilen können mit Enter terminiert werden. Kommen in Dateinamen auch Enter vor, können die Zeilen in der Liste auch mit Nullzeichen terminiert werden, dann muß aber beim read Befehl noch zusätzlich der Parameter -d "" übergeben werden. 3. kleine Einführung in Muster 3.1 Allgemeines 3.1.1 Backslash Spezialzeichen werden mit vorangestellten Backslash geschrieben. \a Alarmton \b Rückschritt \c Abbruch der Ausgabe \f Seitenvorschub \n Zeilenvorschub \r Wagenrücklauf \t horizontaler Tabulator \v vertikaler Tabulator \\ der Backslash selbst \nnn Oktal Wert von einem Zeichen \xHH Hexadezimal Wert von einem Zeichen \cx Ein Control-X Zeichen 3.1.2 Zeichenklassen [...] Trifft auf alle Zeichen die innerhalb der Klammern stehen zu. Es sind darin auch Bereichsoperatoren erlaubt, wie a-z oder 1-9. 3.2 Globes In den Tests -name und -path von find werden Globes verwendet. Zudem expandiert die Bash automatisch Dateinamen nach folgenden Kriterien: * Trifft auf alles zu. ? Trifft auf ein beliebiges Zeichen zu Zeichnenklassen (siehe 3.1.2) *.mp3 Findet alle Dateien mit der Endung .mp3 *.[cC] Findet alle Dateien die mit .c oder .C enden. *tr* Findet alle Dateien von die Zeichenfolge tr vorkommt wie: translate, notra, this_truncate *.sx? Findet alle Dateien mit der Dateiendung .sx und einem beliebigen Zeichen wie z.b. sxw, sxc,... *.mp[1-9] Findet alle Audiolayerdateien von mpeg, wie mp1, mp2, mp3 und was vielleicht noch künftig herauskommt. 3.3 Regular Expressions Dieses Theme würde den Umfang dieses Werkes um Längen sprengen. Ich will dennoch versuchen eine kurze Einleitung zu geben, die nicht einmal einen kleinen Teil aller Möglichkeiten anschneidet, aber hoffentlich trotzdem nicht nutzlos ist. Mit den regulären Ausdrücken ist es möglich, eine schnelle und flexible Behandlung von Strings zu realisieren. Ein regulärer Ausdrück, oft auch als Suchmuster bezeichnet, ist eine Schablone die auf einen gegebenen String paßt oder auch nicht.[1] 3.3.1 einfache Zeichen Einfache Zeichen werden einfach innerhalb einer Zeichenkette gesucht. Es wird dabei ein Treffer unabhängig von den vorigen oder nachfolgenden Zeichen erzielt. Da es allgemein üblich ist, werde ich Suchmuster immer innerhalb von Slashes schreiben. Es können auch Zeichenklassen (siehe 3.1.2) verwendet werden. Beispiel: /in/ passt auf "in the", "insulin" oder aber auch "notingham" 3.3.2 Metazeichen Die Metazeichen unterscheiden sich in regulären Ausdrücken zu denen die bei Globbing vorkommen. Der Punkt ist ein Metazeichen für ein beliebiges Zeichen. Es passt immer genau auf ein Zeichen. Beispiel: Hel.o paßt auf "Hello", "Hel=lo" oder auch "Hel3lo". Ein anderes Metazeichen ist der bereits erwähnte Backslash (siehe 3.1.1). Zusätlich zu erwähnen ist nur, dass der Backslash den allen Metasymbolen ihre besondere Bedeutung wieder wegnimmt. So ist /\./ für das Satzzeichen Punkt und /\\/ erkennt ein Backslash. 3.3.3 Quantifier Oft kommt es vor, dass man überprüfen will, ob eine Zeichenklasse oder Zeichen mehrmals vorkommen. Der Stern (*) trifft zu für eine beliebige Anzahl (keinmal oder öfter). Das Pluszeichen (+) findet das Vorangehende eimal oder öfters. Das Fragezeichen (?) macht das vorige Zeichen optional. Allgemein kann man mit {min,max} die genaue Zahl aussuchen. Lässt man die obere Grenze aus, wird unendlich angenommen. Daraus folgt: {0,} ist gleichbedeutend mit * {1,} ist gleichbedeutend mit + {0,1} ist gleichbedeutend mit ? {n} muss genau n mal vorkommen. Beispiele: /evo+lution/ passt auf evolution mit beliebig vielen o, also evolution, evoooolution, evoooooooolution,... /ple{2,4}ase/ passt auf please mit 2 bis 4 e, also pleease, pleeese und pleeeese. /why.*is/ passt auf why beliebiger Text und dann is. /html?/ passt auf html und htm. 3.3.4 Alternativen Mit | kann man alternative Auswahlen geben. /this|z gives|z you/ statt s darf auch z geschrieben werden. 3.3.5 Gruppierung Mit Klammern kann man Gruppieren. Das macht die Alternativen und Quantifier viel attraktiver. Diese gelten dann nämlich auf die ganze Gruppe und nicht auf ein Zeichen. Beispiele /(Frodo)?ring/ passt auf Frodoring, oder einfach auf ring. /(fri)+ede/ passt auf frifrifriede oder auch einfach nur friede. /(Sam)*/ ist auch glücklich mit einem Leerstring. 3.4 Programme Globbing kann wie erwähnt bei find und bei der bash verwendet werden. Für reguläre Ausdrücke kann man z.b. sed und perl verwenden. Sie haben durchaus unterschiede diese zuvor genannten Punkte sind aber so allgemein, dass sie 1:1 in beiden Programmen verwendet werden können. Intressant wird das Verwenden von regulären Ausdrücken, noch mehr, wenn man auch Texte ersetzen kann. Dazu wird nicht der // sondern der s/// Operator verwendet. 3.4.1 Syntax perl -p -e 's/MUSTER/ERSETZUNG/' [Dateien...] sed -e 's/MUSTER/ERSETZUNG/' [Dateien...] Damit kann man Muster wie zuvor erklärt suchen und die Fundstelle wird durch ERSETZUNG ersetzt. Wenn keine Datei angegeben wird, wird von der Standard- eingabe gelesen. Bei Perl wird die Option -p benötigt, sonst fehlt die print() Anweisung. Beispiele: echo "Hallo" | sed -e 's/l/to/' Hallo wird zu Hatolo. 3.4.2 Modifier Nach dem s/// Befehl kann man noch Modifier setzen. Wird der Modifier nicht verwendet, gilt das Gegenteil. i Groß- und Kleinschreibung wird ignoriert g Ersetzt global, d.h. alle Vorkommen Beispiele echo "Hallo" | sed -e 's/l/to/g' Hallo wird zu Hatotoo echo "GrUsua" | sed -e s/u/t/ig' GrUsua wird zu Grtsta 4. Verwendung Wenn wir das bisher gewonnene Wissen kombinieren können damit echt geniale Konstrukte erzeugt werden. 4.1 Beispiele: 4.1.1 find . -name '*.bmp' -print0 | while read -d "" file > do rename=$(echo "$file" | sed -e 's/.bmp$/.jpg/i') > if test -e $rename > then echo "Datei existiert bereits!"; continue > fi > echo "Converting $file to $rename" > convert "$file" "$rename" > done Sucht alle .bmp Dateien, konvertiert sie zu .jpg Dateien und gibt die Namen aus. Es werden auch Dateinamen mit Enter und Spaces korrekt behandelt. Sollte die .jpg Datei bereits vorhanden sein, wird diese Datei übersprungen. 4.1.2 find . -print0 | while read -d "" file > do rename=$(echo "$file" | sed -e 's/_/ /g') > mv "$file" "$rename" > done Ausgehend vom aktuellen Ordner werden überall die Unterstriche zu Leerzeichen. 4.1.3 find . -iname '*.html' -o -iname '*.htm' -print0 | while read -d "" file > do rename=$(echo "$file" | perl -p -e 's/html?$/html/i') > mv "$file" "$rename" > done Benennt alle komischen htm, HTML, htM Dateieindungen zu dem richtigen html. 4.1.4 Bestimmte Zeichen ersetzen Als Skript abgespeichert kann man ersetze_zeichen /pfad "'" "ä" und es wird rekursiv für alle Dateien diese ersetzung durchgeführt! find . -name "$1" -print0 | while read -d "" old do new=$( echo "$old" | tr "$2" "$3" ) [ "$old" = "$new" ] || mv "$old" "$new" done 4.1.5 Sucht alle mp3s im aktuellen Ordner und rekursiv auch in den Unterordner. Eine Datei in Artist/Album/Name.mp3 wird dann im Zielordner /home/mp3 als Artist - Album - Name.mp3 abgespeichert. find . -name '*.mp3' -print0 | while read -d "" file do mv "$file" "/home/mp3/`echo "$file" | sed -e 's!^\./!!' | sed -e 's!/! - !g'`"; done 4.2 Eigene Skripte schreiben Nur die wenigsten Leute können solche kurzen Skripte wie diese obigen sofort ohne Fehler eintippen. Normalerweise erfolgt eine kurze Debugzeit bevor man diese Biester tatsächlich auf seine eigenen Dateien loslässt. 4.2.1 Richtiges Muster finden while true; do read info; echo "$info" | perl -p -e 'mein_code'; done Mit diesem Code kann man sein Muster testen. Einfach Wörter eingeben und schauen ob die Ersetzung wie erwünscht vorgenommen wird. Öfters will man dann doch globale Ersetzung oder die Groß- u. Kleinschreibung ignorieren. 4.2.2 Nicht sofort Dateioperationen durchführen Sinnvoll ist es auch, ein Testlauf durchzuführen. mv "$file" "$rename" wird einfach auf echo "mv $file $rename" abgeändert. Dann kann man sich die Operationen anschauen, ohne fürchten zu müssen, dass man danach eine leere Rootpartition hat. 4.2.3 Perl abkürzung find . -name '*.orig' -print0 | perl -n0e unlink Siehe find Parameter. Vorteil: Kein Problem mit Enter im Dateinamen oder zu vielen Dateien. 5. Parallele Abarbeitung Die oben vorgestellten Moeglichkeiten haben den Nachteil, dass immer nur ein Prozess gestartet wird. Da es heutzutage immer interessanter wird auch parallel Prozesse auszufuehren, hier eine einfache Moeglichkeit das zu tun: find path -print0 | xargs -0 -n 1 -P 4 command Und zwar wird hier das Feature von xargs ausgenuetzt, dass mit -n nicht mehr als nur ein Argument pro aufruf uebergeben wird. Mit -P kann die Anzahl der parallel gestarteten Prozesse gewaehlt werden. Wie oben bereits erklaert, muss -print0 verwendet werden, damit auch Zeilenumbrueche in Dateinamen problemlos funktionieren. Das Gegenstueck dazu in xargs heisst -0. Uebrigens koennen viele der obigen Schleifen sehr einfach mit xargs ausgetauscht werden. Referenzen: [0] Linux Magazin 05/2004 "Dienstgespräche" [1] Definitionen übernommen von "Einführung in Perl" von Randal L. Schwartz. [2] Linux Magazin 03/2009 "Leserbriefe" siehe http://www.linux-magazin.de/heft_abo/ausgaben/2009/03/leserbriefe