Um meine Commandline-Fu Skills zu trainieren hab ich mir mal awk vorgenommen. awk gilt bei vielen Linux-Anwendern als eines der kompliziertesten Kommandos. Um mit awk zurecht zu kommen ist es jedoch einfacher es als sehr leichte Programmiersprache zu sehen und nicht als kompliziertes Kommando (zumindest ging es mir so). awk ist auch eigentlich eine Programmierspache primär um Textdateien zu verarbeiten. Sie eignet sich auch hervorragend für die Standardeingabe STDIN. Aber auch wer nicht programmieren kann, wird vielleicht an den einfachen Konstrukten gefallen finden. Dies hier wird jedoch keine Programmieranleitung.
Ich gehe hier auf nawk (new awk) ein, dass auf den meisten Linux-Distributionen der Standard ist. Es gibt noch eine erweiterte Version namens gawk (GNU awk), auf der die hier genannten Beispiele aber genauso funktionieren.
Wer irgendeine Programmiersprache (am besten C, Java oder Perl) beherrscht, wird mit awk recht schnell zurecht kommen. awk liest meist eine Textdatei (oder andere Eingabe) ein und zerstückelt sie direkt in Zeilen und diese wiederrum in Felder (Wörter). Dadurch hat man sich schon etwas Programmieraufwand gespart. Nun geht awk den Text Zeile für Zeile durch und führt das Programm auf jeder einzelnen Zeile aus. Es gibt einige Standardvariablen, die man verwenden kann:
- $0 ist die aktuelle Zeile
- $1 .. $n sind die Wörter der Zeile
- NF (Number of Fields) ist die Anzahl der Felder der aktuellen Zeile
- NR (Number of Records) ist die Nummer der aktuellen Zeile
Mit diesen Variablen kann man schon recht praktische Programme schreiben. Dabei wird für jede Zeile nach folgendem Schema vorgegangen
Wobei das Muster meist ein regulärer Ausdruck ist, bei dessen Gültigkeit (für die Zeile) die Aktion auf die Zeile ausgeführt wird. Es kann jedoch eine beliebige Bedingung sein. Oft wird das Muster weggelassen, was dazu führt, dass die Aktion auf jede Zeile ausgeführt wird.
Ein paar einfache Beispiele:
awk '{print $2}' datei.txt
Gibt von jeder Zeile das 2. Wort aus
awk '{print NF}' datei.txt
Gibt von jeder Zeile die Anzahl der Wörter aus
awk '{print $NF}' datei.txt
Gibt von jeder Zeile das letzte Wort aus
awk '{print NR ": " $0}' datei.txt
Gibt jede Zeile mit vorangestellter Zeilennummer aus.
Wie man sieht können Strings einfach verkettet werden indem sie aneinander geschrieben werden (ein Leerzeichen dazwischen erhöht die Lesbarkeit). Variablen werden hingegen mit , konkateniert. Ein Semikolon trennt (wie bei vielen Programmiersprachen) 2 Anweisungen.
awk 'END{print NR}' datei.txt
Gibt die Anzahl der Zeilen aus. Das END ist ein Muster, das besagt, dass die Aktion erst ausgeführt werden soll, wenn alle Zeilen bearbeitet wurden. Entsprechend gibt es ein Muster BEGIN, dessen Aktion vor dem Einlesen der Datei ausgeführt wird.
Die Hochkommata umschließen das eigentliche Programm und verhindern, dass die Shell Zeichen daraus interpretiert. Entsprechend müssen Strings innerhalb des Programms mit doppelten Anführungszeichen gekennzeichnet werden.
Variablen
Es gibt nur 2 Typen von Variablen: Zahlenwerte und Strings. Variablen brauchen nicht deklariert zu werden und werden standardmäßig mit 0 bzw. “” initialisiert. Wie bei Perl wird der Typ automatisch bestimmt.
Ein einfaches Beispiel könnte so aussehen:
awk '{sum+=$NF} END{print sum}' datei.txt
Dieses Programm addiert das letzte Wort jeder Zeile zu der Variablen sum hinzu und gibt am Ende den Inhalt von sum aus. Dies ist recht praktisch, wenn man sich die einzelnen Posten einer Bestellung in eine Datei schreibt wobei als letztes Wort jeweils der Preis steht. So kann einfach die Gesamtsumme berechnet werden ohne jeden Betrag einzeln in einen Taschenrechner tippen zu müssen.
Ein weiteres schönes Beispiel ist den Durchschnitt zu errechnen:
awk '{sum+=$NF} END{print sum/NR}' datei.txt
Hier wird das Ergebnis noch durch die Anzahl der Zeilen geteilt. Dies ist praktisch z.B. für einen Notendurchschnitt.
Ich kenne kaum eine Programmiersprache, bei der man mit so wenig Kenntnissen (Variablen, Print-Anweisung und Arithmetik) schon so viele praktische Dinge erledigen kann. Nimmt man nun noch einfache reguläre Ausdrücke hinzu, kann hat man noch mehr praktische Möglichkeiten.
Reguläre Ausdrücke als Muster
Die folgenden Beispiele ließen sich so oder ähnlich auch mit anderen Kommandos realisieren, doch hier geht es ja um awk:
Gibt alle png-Dateien in einem Verzeichnis aus. Statt print $0 reicht auch ein einfaches print, da ohne ein Argument standardmäßig die aktuelle Zeile ausgegeben wird. Dies ist also die gleiche Funktionalität wie ein simples grep. Genauso kann man auch wie mit grep Dateien durchsuchen:
awk '/foo/{print}' datei.txt
Gibt (wie grep) alle Zeilen aus, die “foo” enthalten. Um auch die Zeilennummern mit auszugeben, braucht man nur zusätzlich noch NR:
awk '/foo/{print NR": "$0}' datei.txt
Hier noch einige Beispiele mit dem Kommando ls:
ls -l | awk '/png$/{sum+=$5} END{print sum}'
Gibt die Gesammtgröße aller png-Dateien in einem Verzeichnis aus.
ls -l | awk '/png$/{sum+=$5; print} END{ print sum/(1024*1024)" MB"}'
Zeigt die Ausgabe von ls -l für alle png-Dateien an und gibt am Ende die Gesamtgröße in Megabytes aus.
ls -l | awk '/png$/{sum+=$5; anz++; print} END{print "Anzahl der PNG-Dateien: "anz; print "Gesammtgröße der PNG-Dateien: "sum/1024" kB ( "sum" Bytes )"; print "Durchschnittliche Größe einer PNG-Datei: "(sum/anz)/1024" kB"}'
Gibt alle PNG-Dateien mit ls -l aus und darunter, wieviele Dateien es waren, wie viel Speicher von allen png-Dateien im Verzeichnis verbraucht werden und wie groß eine Datei im Durchschnitt ist.
awk kennt auch Arrays, die beliebig dimensional sein können (mehr- und gemischtdimensional) und deren Länge vorher nicht festgelegt werden braucht. Auch assoziative Arrays sind möglich. Beispiele:
arr[5]=7; # setzt den 6. Wert des Arrays auf 7
arr[5,3]="hallo"; # setzt den 4. Wert des 6. Arrays auf "hallo". (Genauer gesagt setzt es "5 SUPSEP 3" auf "hallo")
arr["first"]=8; # assoziatives Array
Die kürzeren Programme überlegt man sich in der Regel jedes mal neu und verwirft sie nach Gebrauch wieder. Bei längeren Programmen lohnt es sich jedoch auch mal sie zu speichern. Dafür wird der Teil in den Hochkommata in eine Datei geschrieben, die nun mit awk -f aufgerufen werden kann. Statt dessen kann man auch in die erste Zeile
schreiben und die Datei ausführbar machen. Dann kann man sie jederzeit aufrufen.
Vordefinierte Funktionen
Bevor ich nun weiter auf die Kontrollstrukturen eingehe, möchte ich kurz einige vordefinierte Funktionen vorstellen, mit denen sich wieder viele Probleme lösen lassen.
Am praktischsten ist wohl getline, mit dem die nächste Zeile in $0 geladen wird. So kann man sich mittels
ifconfig | awk '/eth0/{getline; print $2}'
die IP-Adresse von eth0 anzeigen lassen. Doch es steht noch ein “Adresse:” davor. Um dieses Wort noch weg zu bekommen (um die IP-Adresse z.B. in Scripten verwenden zu können) kann man die Funktionen substr(s,i[,n]) und index(s,t) nutzen. substr gibt einen Teilstring von s ab der Position i aus. Das Optionale n gibt an wieviele Zeichen ausgegeben werden sollen. index gibt die Position des ersten vorkommens von t in s aus.
Daraus kann man das Programm verfeinern, so dass wirklich nur die IP-Adresse ausgegeben wird:
ifconfig | awk '/eth0/{getline; print substr($2,index($2,":")+1)}'
Ein substr($2,9) hätte es in diesem Fall zwar auch getan, das hätte bei einer englischen Ausgabe jedoch nicht mehr funktioniert (und ich habe so direkt 2 Funktionen erklärt )
Einige weitere vordefinierte Funktionen sind:
- sin(x),cos(x) - Sinus und Cosinus von x (mit x in Radiant)
- int(x) - Gibt Ganzzahl zurück, indem Nachkommastellen von x abgeschnitten werden
- sqrt(x) - Wurzel von x
- rand() - Zufallswert zwischen 0 und 1
- and(a,b), or(a,b), xor(a,b), compl(a) - Logische Operationen
- gsub(r,s[,t]) - ersetzt jedes vorkommen von von r durch s in der Variablen t (bzw. $0, falls t nicht gesetzt)
- length(s) - gibt Länge von s zurück (ohne Argument Länge von $0)
Das folgende Beispiel gibt die längste Zeile eines Textes mit Zeilennummer aus:
awk 'length($0) > longest {line=$0; longest=length($0); num=NR} END{print num": "line}' datei.txt
Hier wurde als Muster eine Bedingung gewählt, deren Aktion ausgeführt wird, wenn sie wahr ist.
Kontrollstrukturen
Die Kontrollstrukturen von awk sind denen von C, Perl und Java sehr ähnlich. Die if, while und for Konstrukte sind syntaktisch identisch. Daher werde ich hier nur ein Beispiel angeben. Wegen der Lesbarkeit habe ich das Programm in mehrere Zeilen geschrieben. Man kann jedoch auch einfach alles in eine Zeile schreiben.
awk '{
for(i=1;i<=NF;i++){
if(length($i)>length(biggest)){
biggest=$i;
}
}
}
END{print biggest}' datei.txt
gibt das längste Wort einer Datei aus. Für jede Zeile wird eine Variable i von 1 bis “Anzahl der Felder” hochgezählt und die Länge des entsprechenden Feldes mit dem bisher längsten Feld (biggest) verglichen. Ist das aktuell verglichene Feld größer als das bisher größte, so wird das größte Feld auf das aktuelle gesetzt. Am Ende wird das längste Feld ausgegeben. Als Einzeiler sieht das Programm so aus:
awk '{for(i=1;i<=NF;i++){if(length($i)>length(biggest)){biggest=$i;}}}END{print biggest}' datei.txt
Operatoren
Die Operatoren sind im Prinzip auch die gleichen wie in anderen Programmiersprachen und einige davon wurden ja auch schon benutzt. Interessant sind hier die Operatoren ~ und !~, die prüfen, ob ein regulärer Ausdruck matcht (bzw. nicht matcht).
Das folgende Beispiel bestimmt die IP-Adresse von eth0. Hier wird zusätzlich zu den oben angegebenen Programmen noch geprüft, ob das entsprechende Wort eine Folge von Ziffern mit abschließendem Punkt beinhaltet. Wenn nicht, wird ein leerer String ausgegeben:
ifconfig | awk '/eth0/{getline; if($2 ~ /[0-9]+\./){print substr($2,index($2,":")+1)} else {print ""}}'
Funktionen
Natürlich lassen sich in awk auch eigene Funktionen schreiben. Diese sind einfach nach der Syntax
function name(arg1, arg2){
...
}
zu schreiben und können wie gewohnt aufgerufen werden. Das folgende Beispiel definiert eine Funktion, die prüft, ob das übergebene Argument eine Primzahl ist. Das Programm nimmt nun jede Zeile der übergebenen Datei, die nur aus Ziffern besteht und gibt sie aus. dahinter wird geschrieben ob es eine Primzahl ist oder nicht.
#!/usr/bin/awk -f
function isprim(number){
if(number==1)
{
return 0;
}
else if(number<4)
{
return 1;
}
else if(number%2==0)
{
return 0;
}
else if(number<9)
{
return 1;
}
else if(number%3==0)
{
return 0;
}
else
{
r=int(sqrt(number)+0.5);
f=5;
while(f<=r)
{
if(number%f==0)
{
return 0;
}
if(number%(f+2)==0)
{
return 0;
}
f=f+6;
}
}
return 1;
}
/^[0-9]+$/{
if(isprim($0)){
prim=": prim";
}else{
prim="";
}
print $0,prim;
}
Ändern der Wort und Zeilentrenner
Welche Zeichen 2 Wörter bzw. Zeilen voneinander trennen, kann auch festgelegt werden. Dafür sind die beiden Variablen FS und RS zuständig. Setzt man z.B. FS=’,’ so kann man recht gut csv-Dateien (csv=Comma Separated Values) verarbeiten. Dabei dürfen die Variablen mit regulären Ausdrücken belegt werden, so dass man mit RS=’\. |\.\n|\! |\!\n|\? |\?\n’ mit Sätzen statt Zeilen arbeitet.
Ein awk-Programm, welches jeden Satz in einer eigenen Zeile ausgibt wäre z.B.
awk 'BEGIN{RS="\. |\.\n|\! |\!\n|\? |\?\n"} {print}' datei.txt
Da print ohne Parameter automatisch $0 ausgibt.
Die Variablen FS und RS können jederzeit und überall im Programm geändert werden.
Dies war jetzt nur ein kurzer Anriß um awk ein wenig zu erklären. Es gibt natürlich noch Zahlreiche weitere Funktionen, vordefinierte (oder definierbare) Variablen, Aufrufoptionen, Kontrollstrukturen, Operatoren,…
Aber mit diesem Wissen kann man schon recht schöne kleine awk-Programme schreiben um sich das Leben zu erleichtern. Auch für das Commandline-Fu sollten die hier vorgestellten Aspekte zumindest eine Zeit lang ausreichen.
Eine sehr schöne und ausführliche Anleitung zu awk gibt es unter http://www.ostc.de/awk.pdf.
Ein schönes cheat sheet bekommt man unter http://www.catonmat.net/download/awk.cheat.sheet.pdf (englisch).
Ansonsten hält wie immer die manpage weitere Informationen bereit.