Früher fehlten den Shells programmiertechnisch einige Gänge, sie taugten kaum mehr als zum Aufruf externer Programme. Inzwischen haben die Entwickler die Bash zu einer ernst zu nehmenden Programmiersprache aufgebohrt, die schon mit Bordmitteln so manches Standardtool abhängt.
Viele Skripte behandeln die Bash, als könne sie nichts anderes als externe Programme aufrufen. Dabei hat die Version 2 der Standardshell einen Befehlsumfang, der mit komplexen Stringfunktionen, regulären Ausdrücken und Arrays viele nützliche Funktionen bietet, die externe Aufrufe ersetzen. Der Vorteil interner Funktionen ist, dass die Shell keinen neuen Prozess startet, was Rechenzeit und Speicher spart. Gerade wenn Sie Programme wie »grep« oder »cut« in Schleifen verwenden, steigt die Ausführungszeit Ihres Skripts.
Die folgenden Skripte messen sich stets an der Auswertung eines herkömmlichen Apache-Logs einer Website, hier ein Auszug einer Anfrage:
84.57.16.30 - - [21/Oct/2005:04:18:26 +0200] "GET /favicon.ico HTTP/1.1" 404 209 "-" "Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.5) Gecko/20041122 Firefox/1.0"
Wenn Sie feststellen wollen, welche Seiten abgefragt wurden, lösen Sie den GET-String aus dem Log heraus.
Listing 1 zeigt einen Lösungsansatz, wie er häufig in Bash-Skripten anzutreffen ist. Der »cat«-Aufruf in Zeile 3 liest zunächst die Logdatei komplett ein und übergibt sie der »for«-Schleife als Parameterliste – so muss die Bash den gesamten Datei-Inhalt erst mal zwischenspeichern. Anschließend ruft Listing 1 in Zeile 5 für jeden einzelnen Log-Eintrag »cut« als externes Programm auf. Bei einem rund 600 KByte großen Apache-Log dauerte das auf einem Pentium III mit 750 MHz über 18,5 Sekunden.
Das Listing 2 hingegen ist mit gerade mal 3,3 Sekunden fast sechsmal schneller: Es öffnet die Logdatei über den File Descriptor 3 und liest in der Schleife jeweils eine Zeile in die Variable »Anfrage«. Die Bash braucht also stets nur einen Logeintrag zwischenzuspeichern. Anschließend entfernt es vom Anfang der Zeile alle Zeichen bis einschließlich »GET « und vom Ende alle Zeichen bis einschließlich » HTTP/«.
Eine weitere Zehntelsekunde lässt sich einsparen, wenn Sie vom Ende der Anfrage alle Zeichen bis einschließlich des ersten Leerzeichens entfernen, weil so der Stringvergleich der Bash-internen Funktion einfacher ist.
Ersatz-Funktionen
Auch die Programme »basename« und »dirname« lassen sich mit Bash-Mitteln sehr leicht ersetzen. Dazu dient die Stringfunktion »${Variable%Pattern}«, sie entfernt vom Ende der Zeichenkette das kürzeste zutreffende Pattern, sowie »${Variable##Pattern}«, die vom Anfang der Zeichenkette das längste zutreffende Pattern löscht. Letztlich genügt für »dirname« ein einfaches »alias«:
alias dirname=echo ${1%/*};
Die Funktionalität von »basename« ist nur unwesentlich komplexer, hier müssen Sie berücksichtigen, dass »basename« auch die Datei-Endung entfernen kann. Kombinieren Sie daher beide Stringfunktionen miteinander:
function basename() {
B=${1##*/}
echo ${B%$2}
}
Das Ergebnis der ersten String-Manipulation in der Variablen »B« zu speichern ist effizienter, als etwa mittels »set« erneut den ersten Parameter zu belegen und den zweiten Parameter zu übergeben. Sie sollten jedoch nicht ausnahmslos jeden externen Programmaufruf durch Bash-interne Befehle ersetzen. Ein gutes Beispiel dafür ist Listing 3, das ein rudimentäres »grep« implementiert: Obwohl das Skript nur aus wenigen Zeilen besteht und auf den ersten Blick relativ effizient aussieht, braucht es über 2 Minuten, um das 600-KByte-Log des Webservers nach dem Namen einer Datei zu durchsuchen – »grep« hingegen liefert die Ergebnisse in 0,03 Sekunden.
Der größte Performance-Killer ist das Suchmuster: In Zeile 4 wird die aus der Datei eingelesene Zeile mittels Suchen undErsetzen komplett gelöscht, falls es eine Übereinstimmung gibt. Durch das Suchmuster »*${1}*« versucht die Bash bei jedem einzelnen Zeichen der Zeile eine Übereinstimmung zu finden. Verwenden Sie hingegen das Suchmuster »#*${1}*«, wonach das Muster am Zeilenanfang übereinstimmen muss, führt die Bash nur einen Vergleich pro Zeile durch – was die Gesamtlaufzeit des Skripts auf unter 3 Sekunden reduziert. Dennoch ist der externe Aufruf von »grep« um den Faktor 100 schneller.
IPs zerlegen
Auch wenn die Stringfunktionen im vorigen Beispiel nicht der Engpass waren, gibt es je nach Situation elegantere und schnellere Methoden, eine Zeichenkette zu zerlegen. Wenn Sie etwa die IP-Adressen sortieren wollen, von denen die Anfragen kamen, können Sie dies nicht mit Hilfe von »sort« oder einer einfachen lexikalischen Sortierung erledigen, da sonst »217.83.13.152« vor »62.104.118.59« einsortiert würde. Sie müssen die einzelnen Bytes der IP-Adressen herauslösen, in eine sortierbare Form bringen und dann sortieren und vereinzeln.
Die Listings 4 und 5 zeigen dafür zwei mögliche Ansätze, die unterschiedlich performant sind. Das Skript in Listing 4 liest zunächst die Logdatei zeilenweise ein (Zeile 3), um dann die an erster Stellen stehende IP-Adresse in Zeile 4 herauszutrennen. Die Zeilen 6 bis 10 verkürzen danach die IP-Adresse von hinten um jeweils ein Glied und speichern die Adress-Bytes als Dezimalzahlen im Array »IP«.
Der »printf«-Aufruf in Zeile 11, übrigens auch ein Bash-interner Befehl, hat die Funktion, die 4 Bytes der IP-Adresse dreistellig in Dezimalschreibweise mit führenden Nullen und durch Punkte getrennt auszugeben. Die Ausgabe wird in der letzten Zeile per Pipe dem externen Programm »sort« übergeben, anschließend entfernt »uniq« die doppelten Einträge. Für diese Aufgabe benötigte Listing 4 bei einer 600 KByte großen Apache-Logdatei rund 2,6 Sekunden.
Kompakt-Funktionen
Das Programm aus Listing 5 erledigt die gleiche Aufgabe wie Listing 4, jedoch mit rund 1,6 Sekunden fast 40 Prozent schneller. Die Stringfunktionen in den Zeilen 4 bis 10 von Listing 4 sind für den Laufzeitunterschied verantwortlich: Statt die IP-Adresse zunächst herauszulösen und dann mit sieben Funktionsaufrufen zu zerlegen, benutzt Listing 5 die Bash-interne Funktion »read« sowie die Spezialvariable »IFS«. Die Bash betrachtet alle in »IFS« gespeicherten Zeichen als Trennzeichen für Parameter – standardmäßig sind dies das Leerzeichen, das Tabulatorzeichen sowie Newline.
In Zeile 3 von Listing 5 werden aber der Punkt sowie das Leerzeichen als Trenner festgelegt. Wird »read« mit dem Parameter »-a« aufgerufen, liest die Funktion nicht eine komplette Zeile in eine Variable, sondern benutzt die Trennzeichen aus »IFS« und schreibt die Eingabe Komponente für Komponente in die angegebene Array-Variable »IP«.
Also befinden sich die einzelnen Bytes der IP-Adresse unmittelbar nach dem Aufruf von »read« in den Variablen »IP[0]« bis »IP[3]«. (Der Inhalt der übrigen Komponenten des Array ist hier nicht wichtig.) Ergebnis: Ein einziger Funktionsaufruf in Listing 5 ersetzt die Zeilen 3 bis 10 aus Listing 4.
Die externen Programmaufrufe von »sort« und »uniq« ließen sich zwar grundsätzlich ebenfalls durch entsprechende Bash-Funktionen ersetzen – an die Effizienz des C-Programms »sort« wird ein Bash-Skript jedoch kaum herankommen. Wie im richtigen Leben belohnt die Bash nicht jede Tuningmaßnahme mit mehr Effizienz oder einer höheren Geschwindigkeit – es kommt immer darauf an, am richtigen Ende zu schrauben.
Listing 1: Externe Log-Auswertung |
01 #!/bin/bash
02 IFS=$'n'
03 for l in `cat access.log`; do
04 IFS=" "
05 echo "${l}" | cut -d" " -f7
06 done
|
Listing 2: Interne Log-Auswertung |
01 #!/bin/bash
02 exec 3<access.log
03 while read -u 3 Anfrage; do
04 Anfrage="${Anfrage##*GET }"
05 echo "${Anfrage%% HTTP/*}"
06 done
|
Listing 3: Internes »grep« |
01 #!/bin/bash
02 exec 3<$2
03 while read -u 3 Zeile; do
04 if [ -z "${Zeile/*${1}*}" ]; then
05 echo "$Zeile"
06 fi
07 done
|
Listing 4: Stringfunktionen |
01 #!/bin/bash
02 function GetIP() {
03 while read -u $1 Anfrage; do
04 tmp="${Anfrage%% *}"
05 IP[1]="${tmp%%.*}"
06 IP[4]="${tmp##*.}"
07 tmp="${tmp%.*}"
08 IP[3]="${tmp##*.}"
09 tmp="${tmp%.*}"
10 IP[2]="${tmp##*.}"
11 printf "%03d.%03d.%03d.%03dn" ${IP[1]} ${IP[2]} ${IP[3]} ${IP[4]}
12 done
13 }
14 exec 3<access.log
15 GetIP 3 | sort | uniq
|
Listing 5: »read«-Vereinfachung |
01 #!/bin/bash
02 function GetIP() {
03 IFS=". "
04 while read -u $1 -a IP; do
05 printf "%03d.%03d.%03d.%03dn" ${IP[0]} ${IP[1]} ${IP[2]} ${IP[3]}
06 done
07 }
08 exec 3<access.log
09 GetIP 3 | sort | uniq
|
Infos |
|
[1] Beispielprogramme: [https://www.linux-magazin.de/Service/Listings/2006/01/bash/] |






