Aus Linux-Magazin 06/2010

Shellskripte aus der Stümper-Liga - Folge 9: Viel finden

Zu viele Dateien, zu viele Verzeichnisse: Wer gezielt nach Daten sucht und wiederholbare Arbeiten plant, schreibt ein Skript. Doch Obacht: Fallen lauern .

Manche Muster kommen in Programmen immer wieder vor. Manche Fehler von Programmierern finden aufmerksame Code-Studierer ebenfalls regelmäßig. Solche Anpassungen von Code und das Verbessern von oft wiederholten Unregelmäßigkeiten rufen nach einem Skript, das die lästige Aufgabe übernimmt. Dazu liefert beispielsweise die norwegische PHP-Schmiede EZ Systems in ihrem Paket “EZ Components” ihres Content-Management-Systems ein Bash-Skript mit [1], das einen PHP-Verzeichnisbaum durchsucht und eine Reihe automatischer Änderungen der Syntax vornimmt, um Coderichtlinien einzuhalten (siehe Listing 1).

Listing 1:
»syntax-check.sh«

01 #!/bin/sh
02 for i in `find . -name *.php`; do
03     php -l $i | grep -v "No syntax errors"
04 done
05 
06 for i in `find . -name *.php`; do
07     cat $i | 
08     sed -e 's/[[:space:]]while(/ while (/' | 
09     sed -e 's/[[:space:]]if(/ if (/' | 
10     sed -e 's/[[:space:]]else(/ else (/' | 
11     sed -e 's/[[:space:]]elseif(/ elseif (/' | 
12     sed -e 's/[[:space:]]catch(/ catch (/' | 
13     sed -e 's/[[:space:]]foreach(/ foreach (/' | 
14     sed -e 's/[[:space:]]switch(/ switch (/' 
15                    > /tmp/temporary.php
16   cp /tmp/temporary.php $i
17 done
18 
19 echo "Checking for boolean/integer"
20 
21 for i in `find . -name *.php | grep trunk/src`; do
22     cat $i | sed -e 's/@param boolean/@param bool/'> /tmp/temporary.php; cp /tmp/temporary.php $i; done
23 for i in `find . -name *.php | grep trunk/src`; do
24     cat $i | sed -e 's/@param integer/@param int/'> /tmp/temporary.php; cp /tmp/temporary.php $i; done
25 [...]
26 
27 echo "Checking wrong braces placement for functions"
28 
29 grep -rn "function" * | grep "{" | grep -v "{@" | grep -v svn | grep ".php:"
30 grep -rn "class" * | grep "{" | grep -v "{@" | grep -v svn | grep ".php:"
31 grep -rn "interface" * | grep "{" | grep -v "{@" | grep -v svn | grep ".php:"
32 
33 echo "Checking for wrong if/else + brackets"
34 
35 grep -rn "if" * | grep "{" | grep -v svn | grep ".php:"
36 grep -rn "else" * | grep "{" | grep -v svn | grep ".php:"
37 
38 echo "Checking for wrong 'try' syntax':"
39 
40 grep -nr "try" * | grep "[}{]" | grep -v svn-base | grep ".php"
41 
42 echo "Checking for wrong closing bracket:"
43 
44 grep -nr "[^[:space:](]);" * | grep -v svn-base | grep -v tests | grep ".php"
45 
46 echo "Checking for wrong opening bracket:"
47 
48 grep -nr "([^[:space:]C)]" * | grep -v svn-base | grep -v tests | grep -v "(string)" | grep -v "(int)" | grep -v "(float)" | grep -v "*" | grep ".php:"
49 [...]

Best of Bashing

Das Skript ist insofern ein Klassiker, als dass es alle Wesenszüge eines typischen Tools im Praxiseinsatz trägt: Es funktioniert auf dem aktuellen Datenbestand, es ist über Jahre gewachsen und es macht Fehler hinsichtlich Robustheit und Performance. Wer die tatsächliche, inhaltliche Intention des Helfer-Werkzeugs beiseite lässt,die die EZ-Entwickler ohnehin weitgehend mittels in das Skript mit eingebettetem PHP verfasst haben, findet viele Verbesserungsmöglichkeiten, die vergangene Bash Bashings thematisiert haben: So nutzt etwa das Skript bereits in Zeile 2 die nicht schachtelbaren und mit vielfältigen Quoting-Problemen behafteten Backquotes, die moderne Basher besser mit »$(Kommando)« notieren [2].

Ebenfalls regelmäßig gehen Tools dann baden, wenn sie Dateien mit Leerzeichen bearbeiten sollen. Weil die Entwickler des Syntax-Checkers die Variablenauswertung weder in Zeile 3 noch sonst irgendwo im kompletten Skript schützen, scheitert das Programm an Dateien wie »habe leerzeichen.php« – vielleicht ungewöhnlich, aber denkbar und allemal erlaubt. Robustheit heißt schließlich, auch in ungewöhnlichen Situationen noch korrekt zu reagieren [3].

Massig unnötige Befehle

Zeile 7 leitet mit einem unnötigen Aufruf von »cat«, der sich auch »< “$i”« notieren ließe [4], einen Reigen von vielen »sed«-Prozessen ein. Auch wenn Entwickler die einzelnen Aufrufe mit etwas Aufwand sogar ebenfalls innerhalb der Bash realisieren könnten [5], ist es nicht jedermanns Sache, die sehr spezielle Syntax zu lernen. In jedem Fall reicht aber ein einzelner Aufruf innerhalb der Schleife. Im Trunk-Verzeichnis der EZ-Components finden sich allein über 3 600 PHP-Dateien, jede einzelne aktiviert zwischen Zeile 6 und 17 daraufhin neun eigene Prozesse, nämlich einmal »cat«, siebenmal »sed« und ein problematisches »cp«. Das sind weit über 30 000 Prozesse nur für diesen Abschnitt. Ein halbes Dutzend Variationen von diesem Programmmuster finden sich in den kommenden Zeilen.

Perlen an einer Kette

Kürzer und effizienter wäre, die einzelnen »sed«-Kommandos intern zu verketten und mit einem Strichpunkt voneinander zu trennen. Statt »sed ‘s/Apfel/appel/’ | sed ‘s/Birne/pear/’« notiert sich

sed 's/Apfel/appel/g;s/Birne/pear/g'

deutlich kürzer und ersetzt zusätzlich auch noch mehrfache Vorkommen von Begriffen in einer Zeile durch den Modifier »g« am Ende der Substitutionen. Das Ergebnis der Ersetzungen legt Zeile 15 in der hart kodierten Datei »/tmp/temporary.php« ab, was gleich mehrere Probleme birgt.

Erstens ist das Skript nicht parallelisierbar und lässt sich zweitens auch nicht von mehreren Benutzern gleichzeitig auf demselben Rechner aufrufen, da der mehrfache Zugriff auf diese Datei unweigerlich zu Chaos und Verwirrung führen würde. Der bereits 1973 entwickelte Stream-Editor [6] kennt in modernen Versionen die Option »-i«, die für englisch “in-place” steht und die Eingangsdatei direkt ändert. Das ist zwar ein wenig gegen die reine Lehre der Unix-Tools, aber allemal praktisch. Auf Wunsch legt die Option sogar noch eine Sicherheitskopie an, wenn der Anwender hinter ihr noch einen Suffix angibt, etwa »-i.bak«.

Bash keck? Cash
back!

Seit mehreren Folgen prügelt das “Bash Bashing” auf Unsitten und Missverständlichkeiten der Shell ein – und gibt Fingerzeige, es besser zu machen. Wem daher ein bashenswürdiges Skript vor die Tastatur kommt, schreibt an [redaktion@linux-magazin.de] und kassiert wahlweise 50 Euro Finderlohn oder ein Jahresabo des Linux-Magazins – sollte die Redaktion den Tipp abdrucken. Bei besonders haarsträubenden Fällen sichert die Redaktion selbstredend vertraulichen Informaten- und Opferschutz zu.

Wer trotz Pipes Zwischendateien etwa in »/tmp« benötigt, ist versucht, die Shellvariable »$$« verwenden. Sie enthält die jeweilige Prozess-ID, ist aber konstant pro Shell-Instanz, also für einen Skriptaufruf. Sie kann durchaus überlaufen, wie das Beispiel der 30 000 Prozesse zeigte. Typischerweise fängt Linux bei PIDs größer als 32767 wieder bei kleinen Zahlen an. Mehr Komfort versprechen hier die beiden externen Kommandos »tempfile« und »mktemp«, die jeweils eindeutige Temporärdateien anlegen und sich auch um Zugriffsrechte kümmern.

Letzte Mäkelei an Zeile 14: Der Kopierbefehl aktualisiert die Ursprungsdatei mit dem Ergebnis der Umformungen, wieso aber als Kopie? Der »mv«-Befehl wäre hier sicherlich angebrachter, zumal das Skript am Ende die übriggebliebene »/tmp/temporary.php« nicht aufräumt. In anderem Kontext könnte das gar Auswirkungen auf die Sicherheit haben, wenn die verarbeiteten Dateien etwa sensible Daten enthalten.

Wer sucht, der findet

Viele Skriptprogrammierer versuchen sich an »find«, um bestimmte Dateien ausfindig zu machen. Gerade, wenn diese Dateien in Verzeichnisstrukturen angeordnet sind, ist das auch der prinzipiell richtige Weg. Leider kommt »find« mit einer komplizierten Syntax, die nicht in allen Details den Denkregeln anderer Unix-Kommandos folgt. Deshalb sind hinreichend moderne Implementationen von »grep« ebenfalls in der Lage, Ordnerbäume mit der Option »-r« für “rekursiv” zu durchsuchen. Die Zeilen 29 bis 31 demonstrieren, wie das funktioniert. Bash-Puristen dürfen dazu auch den Operator »**« verwenden, der ab Version 4 der Shell zu einem ganzen Subtree expandiert [7].

Was zunächst vorbildlich aussieht, verkehrt sich anschließend wieder ins Gegenteil: Zeile 48 möchte offenkundig eine Reihe von Begriffen nicht im Ergebnis sehen und filtert durch eine lange Kaskade von »grep -v« diese aus. Alternativ wäre

egrep -v '(Exkl1|Exkl2)'

kürzer und sparsamer, da die Dateien nur durch einen Prozess fließen müssen anstatt durch acht.

Schleifen eliminieren

Vermeintlich kleine Skripte können einen erheblichen Ressourcenhunger entwickeln, besonders, wenn sie rekursiv Verzeichnisse durchsuchen und auf den gefundenen Dateien Operationen ausführen, die mehrere externe Prozesse benötigen. Jede Einsparung im Schleifenrumpf wirkt sich dann gleich dramatisch auf die Gesamtlaufzeit aus.

Es muss nicht immer pure Bash sein, denn viele der klassischen Unix-Tools wie »sed«, »grep« oder »find« bieten bei ihren internen Kommandos manche Handreichung. Wer dann noch teure, fragile und mitunter gar gefährliche Temporärdateien vermeidet, kann sich beruhigt wieder seinen wirklichen Problemen widmen.

Infos

[1] PHP-Syntax-Check von EZ-Publish:[http://svn.ez.no/svn/ezcomponents/scripts/syntax-check.sh]

[2] Bernhard Bablok, “Bash Bashing 2:Quoting”, Linux-Magazin 11/09, S. 104

[3] Nils Magnus, “Bash Bashing 1:Leerzeichen”, Linux-Magazin 10/09, S. 108

[4] Nils Magnus, “Bash Bashing 4:Unsinniges und unnötiges »cat«”, Linux-Magazin 01/10, S. 108

[5] Bernhard Bablok, “Bash Bashing 5: Vermeiden von »sed« und »grep«”, Linux-Magazin 02/10, S. 106

[6] Michael Hauben (Hrsg.), “On the Early History and Impact of Unix”, in: “Netizens: An Anthology”, Chapter 9, 1996: [http://www.columbia.edu/~rh120/ch106.x09]

[7] Bernhard Bablok, Nils Magnus, “Aufpoliert: Neue Funktionen in der Bash 4”, Linux-Magazin 06/09, S. 46

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben