Egal ob Konfigurations-, Logdatei oder Adressbuch – viele Dinge lassen sich in einer Shell effizient mit einfachen Textdateien lösen. Dieser Artikel behandelt die Fülle an Linux Kommandozeilen-Tools, die Texte verbinden, sortieren, umleiten oder analysieren.
Der Befehl wc (word count) ist ein einfacher Filter, um die Anzahl der Zeilen, Schriftzeichen (Bytes) oder Wörter in einer Datei zu ermitteln. Meistens benutzt man den Befehl, um die Zeilen einer Textdatei zu zählen, dazu benutzt man einfach wc -l:
$ wc -l kern.log 1026 kern.log
Wenn Sie keinen Dateinamen angeben, liest wc aus der Standardeingabe. Diese Funktion verwenden wir im folgenden nützlichen Ausdruck für das Zählen der Dateien in einem Verzeichnis:
$ ls | wc -l 138
Die Anzahl der Bytes in einer Datei ermitteln Sie mit wc -c:
$ wc -c kern.log 106932 kern.log
Die Verwendung von wc -c bei einer einzigen Datei ist zugegebenermaßen nicht besonders interessant – die gleiche Information finden Sie auch in der Ausgabe von ls -l. Wenn Sie aber wc mit dem find-Befehl kombinieren, erhalten Sie die Anzahl der Bytes von allen Dateien eines ganzen Verzeichnisbaums:
$ find /var/log -type f -exec wc -c {} \;
79680 /var/log/kern.log.6.gz
3781 /var/log/dpkg.log.4.gz
106932 /var/log/kern.log
…Zu diesem Beispiel kehren wir später wieder zurück, sehen wir uns aber zuerst noch einige weitere Shell-Tricks an.
Ein anderes Paar von einfachen Textverarbeitungsfiltern bilden head und tail, die je nachdem die ersten 10 oder die letzten 10 Zeilen aus einer Eingabe extrahieren. Sie können auch eine größere oder kleinere Anzahl von Zeilen vorgeben. Auch diesen Befehl nutzt man meist in Kombination mit einem anderen Kommando, zum Beispiel um die zuletzt geänderten Dateien aufzulisten:
$ ls -t | head -1 kern.log
Und wenn Sie die letzten paar Zeilen dieser Datei sehen wollen:
$ tail -3 kern.log Nov 21 09:00:19 elk kernel: [11936.090452] [UFW BLOCK INPUT]: IN=eth0 OUT=… Nov 21 09:00:21 elk kernel: [11938.083655] [UFW BLOCK INPUT]: IN=eth0 OUT=… Nov 21 09:00:25 elk kernel: [11942.134431] [UFW BLOCK INPUT]: IN=eth0 OUT=…
Hier ein hübscher kleiner Trick, um durch die Kombination von head in tail eine bestimmte Zeile auszulesen:
$ head -13 /etc/passwd | tail -1 www-data:x:33:33:www-data:/var/www:/bin/sh
In diesem Fall haben wir die dreizehnte Zeile von /etc/passwd über die Pipe (|) als Eingabe von tail weitergeleitet. Eine weitere nützliche Funktion des tail-Befehls ist die Option -f, die wie gewohnt die letzten 10 Zeilen einer Datei angezeigt. Mit -f bleibt die Datei aber geöffnet, und tail zeigt alle neuen Zeilen an, die an das Ende der Datei entstehen. Dies ist besonders nützlich bei Log-Dateien, die Sie in Auge behalten möchten, zum Beispiel tail -f kern.log.
Mit head und tail kommen Sie zwar an bestimmte Zeilen heran, es lassen sich damit aber keine Bereiche aus einer Zeile absondern. Weist eine Ausgabe regelmäßige Trennzeichen auf, dann erledigen Sie diese Aufgabe am besten über cut:
$ cut -d: -f1,6 /etc/passwd root:/root daemon:/usr/sbin bin:/bin …
Die Option -d gibt das Trennzeichen an, die für die Trennung der Felder in jeder Zeile verwendet werden soll, und mit -f bestimmen Sie die zu extrahierenden Felder. In diesem Fall picken wir die Benutzernamen und das Home-Verzeichnis von jedem Benutzer heraus.
Der cut-Befehl ermöglicht es Ihnen auch, bestimmte Zeichenfolgen (caracters) herauszufiltern. Dazu benutzen Sie die Option -c. Das folgende Beispiel filtert die Ausgabe von ls -l so, dass wir nur die Berechtigungen und die Dateinamen sehen:
$ ls -l | cut -c2-10,52- otal 1540 rwxr-xr-x acpi rw-r--r-- adduser.conf rw-r--r-- adjtime …
Dummerweise enthält die Ausgabe auch die Kopfzeile von ls -l. Zum Glück kann uns tail weiterhelfen:
$ ls -l | tail -n +2 | cut -c2-10,52- rwxr-xr-x acpi rw-r--r-- adduser.conf rw-r--r-- adjtime …
Das sieht schon besser aus! Beachten Sie die Syntax von tail: Die Option -n ist die (POSIX-mäßig richtige) Alternative für die Bestimmung der Anzahl Zeilen, die tail ausgeben soll. So sind tail -10 und tail -n 10 gleichbedeutend. Wenn Sie der Anzahl Zeilen ein +-Zeichen voranstellen, wie in obigem Beispiel, dann bedeutet dies "beginne mit der angegebenen Zeile". Dadurch beginnt tail die Ausgabe bei der zweiten Zeile. Die Syntax + funktioniert nur nach -n.
Während cut sehr viele Aufgaben wunderbar löst, stolpert es bei Ausgaben, die durch Leerzeichen getrennt sind oder keinerlei regelmäßige Muster aufweisen. Hier kommt awk ins Spiel:
$ ps -ef | awk '{print $1 "\t" $2 "\t" $8}'
UID PID CMD
root 1 /sbin/init
root 2 [kthreadd]
root 3 [migration/0]
…
Das Tool awk zerlegt alle Eingabezeilen bei einem Leerzeichen und weist jedem Feld automatisch eine Variabla mit den Namen $1, $2 und so weiter zu. awk ist eine voll funktionsfähige Skript-Sprache mit diversen Fähigkeiten, aber am einfachsten verwenden Sie den Befehl print, um bestimmte Eingabefelder auszugeben.
Durch eine Textmuster oder eine andere Bedingung filtert awk auch bestimmte Zeilen, wodurch Ihnen die Mühe erspart wird, den Input zunächst mit grep oder einem anderen Werkzeug zu filtern. Angenommen, Sie möchten den ps-Ausgang von oben, aber nur für die Prozesse von Anna:
$ ps -ef | awk '/^anna / {print $1 "\t" $2 "\t" $8}'
hal 7445 /usr/bin/gnome-keyring-daemon
hal 7460 x-session-manager
hal 7566 /usr/bin/dbus-launch
…
Hier habe ich den Operator (/^anna /) verwendet, um nur für die Zeilen eine Ausgabe zu erzeugen, die mit anna Leerzeichen
beginnen. Ich wäre mit ps -ef | awk '($1 == "anna") ... zum gleichen Ergebnis gekommen.
Möchten Sie anstelle des Leerzeichens einen anderes Trennzeichen benutzen, dann übergeben Sie das per -F. So lässt sich awk an Stellen benutzen, wo Sie normalerweise cut verwenden würden.
Angenommen, Sie möchten Benutzernamen und Home-Verzeichnisse ausgeben, ähnlich wie im ersten Beispiel mit cut. Die Ausgabe soll aber nur die Nutzer zeigen, deren Home-Verzeichnis unter /home liegt:
$ awk -F: '($6 ~ /^\/home\//) { print $1 ":" $6 }' /etc/passwd
sabayon:/home/sabayon
hal:/home/hal
anna:/home/anna
Anstatt die gesamte Zeile wieder auszuwählen, verwenden wir hier den Operator ~, um nur ein bestimmtes Feld durch einen Mustervergleich auszugeben.
Bei längeren Ausgaben hilft sort:
$ awk -F: '($6 ~ /^\/home\//) { print $1 ":" $6 }' /etc/passwd | sort
anna:/home/anna
hal:/home/hal
sabayon:/home/sabayon
Standardmäßig erstellt sort eine einfache alphabetische Liste nach dem Anfangsbuchstaben der Zeilen. Manchmal möchsten Sie jedoch auch numerisch sortieren, vielleicht sogar nach einem bestimmten Feld in der Eingabe. Hier ein klassisches Beispiel, das zeigt, wie Sie die password-Datei nach der Benutzer-ID sortieren (nützlich für das Auffinden doppelter UID-s und wenn jemand illegale UID-0-Konten hinzugefügt hat):
$ sort -n -t: -k3 /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh …
Die Option -n bewirkt die numerische Sortierung, -t bestimmt das Trennzeichen zwischen den Feldern (wie cut -d oder awk -F), und -k bestimmt das Feld oder die Felder, wonach sortiert wird.
Mit -r erhalten Sie eine absteigende Sortierung:
$ ls /etc/rc3.d | sort -r S99stop-readahead S99rmnologin S99rc.local …
Erinnern Sie sich noch an den find Befehl, bei dem wir wc -c benutzt haben, um die Bytes jeder Datei in einem bestimmten Verzeichnis zu zählen? Wir können diese Ausgabe nun sortieren, um dann mit head die 10 größten Dateien zu ermitteln:
$ find /var/log -type f -exec wc -c {} \; | sort -nr | head
44962814 /var/log/vnetlib
24748291 /var/log/syslog
24708201 /var/log/mail.log
24708201 /var/log/mail.info
10243792 /var/log/ConsoleKit/history
3902994 /var/log/syslog.0
3782642 /var/log/mail.log.0
3782642 /var/log/mail.info.0
1039348 /var/log/vmware/hostd-7.log
804391 /var/log/installer/partman
Manchmal möchten Sie nicht wissen, wie oft etwas vorkommt, sondern nur ob es vorkommt. Dazu dient der Befehl uniq. Es sortiert unmittelbar aufeinanderfolgende gleiche Ausdrücke aus und wird deshalb in der Regel zusammen mit sort benutzt:
$ ps -ef | awk '{print $1}' | sort | uniq
apache
dbus
dovecot
…
Da diese Aufgabe so oft vorkommt, besitzt sort eine Option -u, die quasi uniq ersetzt. Das oben aufgeführte Beispiel könnte man also umschreiben zu ps -ef | awk '{print $1}' | sort -u.
Der uniq-Befehl hat viele nützliche Optionen. Zum Beispiel ermittelt uniq -c die Anzahl aller Zeilen, die zusammengeführt worden sind, über uniq -d finden Sie Zeilen, die wiederholt (doppelt) vorkommen. Um zum Beispiel doppelte UID-s in Ihrer Passwort-Datei zu finden, geben Sie folgenden Befehl ein:
$ cut -d: -f3 /etc/passwd | sort -n | uniq -d
In diesem Fall haben wir keine Ausgabe erhalten – keine doppelten UID-s – das ist genau das, was wir eigentlich sehen wollen.
Der Befehl paste fügt zwei Dateien Zeile für Zeile zusammen, standardmäßig mit einem Tabulator als Trennzeichen. Die Dateien gross und klein (mit Groß- beziehungsweise Kleinbuchstaben) lassen sich somit mit folgendem Befehl zusammenfügen:
$ paste gross klein A a B b C c …
Das Trennzeichen legen Sie über -d fest:
$ paste -d, gross klein A,a B,b C,c …
Während diese Aufgabe in der Praxis kaum vorkommt, benötigt man oft ein Tool, um zwei Dateien anhand eines bestimmten Feldes miteinander zu vergleichen: das macht join. Die Syntax ist relativ kompliziert, deshalb hier ein einfaches Beispiel mit den Buchstabendateien.
$ join <(nl gross) <(nl klein) 1 A a 2 B b 3 C c …
Zunächst fügen wir über nl in den beiden Dateien Zeilennummern ein. Der join-Befehl fügt dann die Ausgabe aufgrund der Zeilennummern zusammen und liefert eine Datei mit Zeilennummern, gross und klein. Beachten Sie die schlaue <(...) Bash-Syntax, die anstelle des Dateinamens gleich einen kompletten Befehl einliest.
Normalerweise ist das Leben mit join nicht so einfach. Man stößt dauernd auf irgendeine verrückte Kombination von Feldern und Trennzeichen. Nehmen wir an, Sie hätten eine CSV-Datei (bevoelkerung), in der die 20 bevölkerungsreichsten Länder zusammen mit ihren Bevölkerungszahlen aufgelistet sind:
1,China,1330044544 2,Indien,1147995904 3,Amerika,303824640 …
In einer zweiten Datei (staedte) finden sich die Hauptstädte aller Länder der Welt:
Afghanistan,Kabul Albanien,Tirane Algerien,Algiers …
Die Aufgabe ist nun denkbar einfach: Aufgrund von Feld 2 in der ersten Datei und Feld 1 in der zweiten Datei möchten Sie die Dateien miteinander kombinieren.
Die komplizierte Sache bei join ist, dass es nur dann funktioniert, wenn sich die Felder, nach denen wir sortieren und zusammenfügen möchten, in beiden Dateien in derselben Reihenfolge sind. In der Regel benötigt man deshalb zunächst ein Tool zur Vorsortierung und wie oben schon gezeigt, lassen sich zwei Befehle wunderbar kombinieren:
$ join -t, -1 2 -2 1 <(sort -t, -k2 bevoelkerung) <(sort staedte) Bangladesh,7,153546896,Dhaka Brasilien,5,196342592,Brasilia China,1,1330044544,Peking …
Der join-Befehl gibt zuerst das Trennzeichen an (-t,) dann die Felder, über die die Zusammenführung erfolgen soll: die erste Datei (-1 2), dann die zweite (-2 1). Er benutzt die Bash-Syntax <(...), um die Eingabe mit sort entsprechend vorzubereiten.
Die Ausgabe ist zugegebenermaßen nicht besonders hübsch, da join zuerst das angeknüpfte Feld aus den Ländernamen einfügt, dann die verbleibenden Felder aus der ersten Datei und schließlich die restlichen Felder aus der Datei staedte. Mit cut und sort kann man hier etwas für Ordnung sorgen:
$ join -t, -1 2 -2 1 <(sort -t, -k2 most-populous) <(sort cities) | cut -d, -f1,3,4 | sort -nr -t, -k2 China,1330044544,Peking Indien,1147995904,New Delhi Amerika,303824640,Washington D.C. …
Das nennt man Textverarbeitung auf der Shell.
Das Zusammenfügen von Dateien ist schön und gut, aber manchmal möchte man auch Dateien auftrennen. Zum Beispiel ein Passwort-Crack-Wörterbuch, um die Verarbeitung auf mehrere Systeme zu verteilen:
$ split -d -l 1000 crack crack. $ wc -l * 98569 crack 1000 crack.00 1000 crack.01 1000 crack.02 …
Hier hat split die Datei crack in Stücke zu 1000 Zeilen aufgespaltet (-l 1000 ist zugleich die Standardeinstellung). Der Grundname für die daraus resultierenden Dateien soll crack. heißen (beachten Sie den abschließenden Punkt), und ein numerisches Suffix bekommen (-d). wc -l prüft, ob das Ergebnis den Erwartungen entspricht.
Beachten Sie, dass Sie anstelle eines Dateinamens auch - (die Standard-Eingabe) als Input nutzen können, um beispielsweise die Ausgabe eines sehr ausführlichen Befehls in überschaubare Einheiten aufzuteilen (zum Beispuel tcpdump | split -d -l 100000 - packet-info.).
Der Befehl tr wandelt Zeichensätze um. Das klassische Beispiel ist die Umwandlung von Großbuchstaben in Kleinbuchstaben. Als Beispiel nehmen wir die oben bereits genutzte Datei gross:
$ tr A-Z a-z < gross a b c …
Ein zugegebenermaßen einfältiges Beispiel, hier ein etwas praxisnaherer Einsatz:
$ cd /proc/self $ cat environ GNOME_KEYRING_SOCKET=/tmp/keyring-lFz8t4/socketLOGNAME=halGDMSESSION=default… $ tr \\000 \\n <environ GNOME_KEYRING_SOCKET=/tmp/keyring-lFz8t4/socket LOGNAME=hal GDMSESSION=default …
In der Regel sind Dateien unter /proc durch Nullen (ASCII zero) abgegrenzt. Möchten Sie eine solche Datei im Terminalfenster ansehen, fließt alles zusammen, wie oben im cat-Aufruf zu sehen.
Durch die Umwandlung der Nullstellen (\000) in Zeilenumbrüche (\n) wird alles viel besser lesbar. (Die zusätzlichen Schrägstriche (\ sind notwendig, da die Shell den Backslash als Sonderzeichen interpretiert.)
Anstatt Zeichen zu ersetzen, kann tr diese über die Option -d auch gleich löschen. Für alle Matrix-Fans hier ein Befehl, der zufällige Zeichen im Terminal ausgibt:
$ tr -d -c [:print:] </dev/urandom
Der Eintrag [:print:] steht für die Menge an druckbaren Zeichen. Die Option -c (compliment) weist tr an, alle Zeichen außer diesem Satz anzuzeige. So löschen wir letztendlich alles bis auf die druckbaren Zeichen.
Das war eine High-Speed-Einführung in einige der Textverarbeitungs- und Textfilterungsfunktionen von Linux. Wie bei jeder Einführung haben wir dabei lediglich an der Oberfläche leicht gekratzt. Es gibt eine Menge von Webseiten im Internet, wo Sie mehr Beispiele und Anregungen finden, zum Beispiel http://shelldorado.com, http://commandlinefu.com und den wöchentliche Blog, den ich mit mehreren Freunden zusammen auf http://blog.commandlinekungfu.com betreue. Die Manpages sind auch sehr nützlich – und vergessen Sie nicht man -k bei Stichwortsuchen, wenn Ihnen der Name eines Befehls gerade nicht einfällt oder Sie nicht sicher sind, wo Sie anfangen sollen! Die besten Lehrer sind: Übung, Übung und nochmals Übung.