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

Fr Jul 30 13:54:24 CEST 2021
patent_button.gif valid-html401.png elektra.jpg fsfe-logo.png valid-css.png vim.gif anybrowser.gif