% $Id$ \chapter{Schmutzige Tricks :-)} Eigentlich sind diese Tricks gar nicht so schmutzig. Hier ist lediglich eine Sammlung von Beispielen, die genial einfach oder genial gut programmiert sind. Das bedeutet nicht, daß jeder Shell-Programmierer diese Techniken benutzen sollte. Ganz im Gegenteil. Einige Mechanismen bergen Gefahren, die nicht auf den ersten Blick erkennbar sind. Mit anderen Worten: \textbf{Wenn Du diese Techniken nicht verstehst, dann benutze sie nicht!} Die Intention hinter diesem Abschnitt ist es, dem gelangweilten Skripter etwas interessantes zum Lesen zu geben. Das inspiriert dann vielleicht dazu, doch einmal in den fortgeschrittenen Bereich vorzustoßen, neue Techniken kennenzulernen. Außerdem kann das Wissen über gewisse Techniken eine große Hilfe beim Lesen fremder Skripte darstellen, die eventuell von diesen Techniken Gebrauch machen. Diese Techniken sind nicht `auf meinem Mist gewachsen', sie stammen vielmehr aus Skripten, die mir im Laufe der Zeit in die Finger gekommen sind. Ich danke an dieser Stelle den klugen Köpfen, die sich solche Sachen einfallen lassen haben. \section{Die Tar-Brücke} Eine sogenannte Tar-Brücke benutzt man, wenn eine oder mehrere Dateien zwischen Rechnern übertragen werden sollen, aber kein Dienst wie SCP oder FTP zur Verfügung steht. Außerdem hat die Methode den Vorteil, daß Benutzerrechte und andere Dateiattribute bei der Übertragung erhalten bleiben\footnote{Vorausgesetzt natürlich, daß der Benutzer auf der entfernten Seite über die nötigen Rechte verfügt.}. Der Trick besteht darin, auf einer Seite der Verbindung etwas mit \texttt{tar} einzupacken, dies durch eine Pipe auf die andere Seite der Verbindung zu bringen und dort wieder zu entpacken. Wenn dem Kommando \texttt{tar} an Stelle eines Dateinamens ein Minus-Zeichen als Archiv gegeben wird, benutzt es~--~je nach der gewählten Aktion~--~die Standard-Ein- bzw. -Ausgabe. Diese kann an ein weiteres \texttt{tar} übergeben werden um wieder entpackt zu werden. Ein Beispiel verdeutlicht diese Kopier-Fähigkeit: \lstinline_tar cf - . | ( cd /tmp/backup; tar xf - )_ Hier wird zunächst der Inhalt des aktuellen Verzeichnisses `verpackt'. Das Resultat wird an die Standard-Ausgabe geschrieben. Auf der Empfängerseite der Pipe wird eine Subshell geöffnet. Das ist notwendig, da das empfangende \texttt{tar} in einem anderen Verzeichnis laufen soll. In der Subshell wird zunächst das Verzeichnis gewechselt. Dann liest ein \texttt{tar} von der Standard-Eingabe und entpackt alles was er findet. Sobald keine Eingaben mehr kommen, beendet sich der Prozeß mitsamt der Subshell. Am Ziel-Ort finden sich jetzt die gleichen Dateien wie am Quell-Ort. Das ließe sich lokal natürlich auch anders lösen. Man könnte erst ein Archiv erstellen, das dann an anderer Stelle wieder auspacken. Nachteil: Es muß genügend Platz für das Archiv vorhanden sein. Denkbar wäre auch ein in den Raum gestelltes \lstinline_cp -Rp * /tmp/backup_ Allerdings fehlen einem dabei mitunter nützliche \texttt{tar}-Optionen\footnote{Mit \texttt{-l} verläßt \texttt{tar} beispielsweise nicht das File-System. Nützlich wenn nur eine Partition gesichert werden soll.}, und die oben erwähnte Brücke wäre mit einem reinen \texttt{cp} nicht möglich. Eine Seite der Pipe kann nämlich auch ohne Probleme auf einem entfernten Rechner `stattfinden'. Kommandos wie \texttt{ssh} oder \texttt{rsh} (letzteres nur unter Vorsicht einsetzen!) schlagen die Brücke zu einem anderen System, dort wird entweder gepackt und versendet oder quasi die Subshell gestartet und gelesen. Das sieht wie folgt aus: \lstinline_ssh 192.168.2.1 tar clf - / | (cd /mnt/backup; tar xf - )_ Hier wird auf einem entfernten Rechner die Root-Partition verpackt, per SSH in das lokale System geholt und lokal im Backup-Verzeichnis entpackt. Der Weg in die andere Richtung ist ganz ähnlich: \lstinline_tar cf - datei.txt | ssh 192.168.2.1 "(mkdir -p $PWD ;cd $PWD; tar xf -)"_ Hier wird die Datei verpackt und versendet. Eine Besonderheit gegenüber dem vorigen Beispiel besteht darin, daß das Zielverzeichnis bei Bedarf erstellt wird, bevor die Datei dort entpackt wird. Zur Erklärung: Die Variable \texttt{\$PWD} wird, da sie nicht von Ticks `gesichert' wird, schon lokal durch die Shell expandiert. An dieser Stelle erscheint also auf dem entfernten System der Name des aktuellen Verzeichnisses auf dem lokalen System. \section{Binaries inside} Software wird meistens in Form von Paketen verteilt. Entweder handelt es sich dabei um auf das Betriebssystem abgestimmte Installationspakete (rpm, deb, pkg usw.), gepackte Archive (zip, tgz) oder Installationsprogramme. Unter Unix-Systemen bietet sich für letztere die Shell als Trägersystem an. Shell-Skripte sind mit wenigen Einschränkungen plattformunabhängig, sie können also ohne vorherige Installations- oder Compilier-Arbeiten gestartet werden und die Umgebung für das zu installierende Programm testen und / oder vorbereiten. Abgesehen davon können Skripte mit den hier vorgestellten Techniken auch andere Daten, z. B. Bilder oder Töne, enthalten. Doch wie werden die~--~üblicherweise binären~--~Pakete auf das Zielsystem gebracht? Im Prinzip gibt es dafür zwei unterschiedliche Verfahren: \subsection{Binäre Here-Dokumente} \index{Here-Dokument} Eine Möglichkeit ist es, die binäre Datei in Form eines Here-Dokuments mitzuliefern. Da es aber in der Natur einer binären Datei liegt nicht-druckbare Zeichen zu enthalten, kann die Datei mit Hilfe des Tools \texttt{uuencode} vorbereitet werden. Das Tool codiert Eingabedateien so, daß sie nur noch einfache Textzeichen enthalten. Sehen wir uns das folgende einfache Beispiel an. Es ist etwas wild konstruiert und nicht sehr sinnvoll, aber es zeigt das Prinzip. \begin{lstlisting} #!/bin/sh echo "Das Bild wird ausgepackt..." uudecode << 'EOF' begin 644 icon.png MB5!.1PT*&@H````-24A$4@```!8````6"`8```#$M&P[````"7!(67,```L3 M```+$P$`FIP8````!&=!34$``+&.?/M1DP```"!C2%)-``!Z)0``@(,``/G_ \end{lstlisting} Nach einem Hinweis wird also das Here-Dokument als Eingabe für das Tool \texttt{uudecode} benutzt. Erstellt wurde das Dokument mit einer Zeile der folgenden Form: \lstinline|uuencode icon.png icon.png| Wie man sieht ist der Name der Datei in dem Here-Dokument enthalten. Die Datei wird entpackt und unter diesem gespeichert. In der `realen Welt' muß an der Stelle auf jeden Fall sichergestellt werden, daß keine existierenden Dateien versehentlich überschrieben werden. Um diesen Abschnitt nicht allzu lang werden zu lassen überspringen wir einen Teil der Datei. \begin{lstlisting}[firstnumber=38] M#-""F4%,@%4.GUZ``"(*`6VW6!S#\>C_?/;__Q@``B K!>E;2S-,]5!A7`,,U'0@GQ6?8H```P`#@&?)O'P'L0````!)14Y$KD)@@@`` ` end EOF if [ $? -ne 0 ]; then echo "Fehler beim Auspacken der Datei" exit 1 fi display icon.png \end{lstlisting} Nach dem Entpacken wird noch der Exit-Code von \texttt{uudecode} überprüft und im Fehlerfall eine Ausgabe gemacht. Im Erfolgsfall wird das Bild mittels \texttt{display} angezeigt. \subsection{Schwanz ab!} Diese Variante basiert darauf, daß die binäre Datei ohne weitere Codierung an das Shell-Skript angehängt wurde. Nachteil dieses Verfahrens ist, daß das `abschneidende Kommando' nach jeder Änderung der Länge des Skriptes angepaßt werden muß. Dabei gibt es zwei Methoden, die angehängte Datei wieder abzuschneiden. Die einfachere Methode funktioniert mit \texttt{tail}: \lstinline|tail -n +227 $0 > icon.png| Dieses Beispiel geht davon aus, daß das Skript selbst 227 Zeilen umfaßt. Die binäre Datei wurde mit einem Kommando wie \lstinline|cat icon.png >> skript.sh| an das Skript angehängt. Für die etwas kompliziertere Variante muß die Länge des eigentlichen Skript-Teiles genau angepaßt werden. Wenn das Skript beispielsweise etwa 5,5kB lang ist, müssen genau passend viele Leerzeilen oder Kommentarzeichen angehängt werden, damit sich eine Länge von 6kB ergibt. Dann kann das Anhängsel mit dem Kommando \texttt{dd} in der folgenden Form abgeschnitten werden: \lstinline|dd bs=1024 if=$0 of=icon.png skip=6| Das Kommando kopiert Daten aus einer Eingabe- in eine Ausgabedatei. Im einzelnen wird hier eine Blockgröße (blocksize, bs) von 1024 Bytes festgelegt. Dann werden Eingabe- und Ausgabedatei benannt, dabei wird als Eingabedatei \texttt{\$0} und somit der Name des laufenden Skriptes benutzt. Schließlich wird festgelegt, daß bei der Aktion die ersten sechs Block~--~also die ersten sechs Kilobytes~--~übersprungen werden sollen. Um es nochmal zu betonen: Diese beiden Methoden sind mit Vorsicht zu genießen. Bei der ersten führt jede zusätzliche oder gelöschte Zeile zu einer kaputten Ausgabedatei, bei der zweiten reichen schon einzelne Zeichen. In jedem Fall sollte nach dem Auspacken noch einmal mittels \texttt{sum} oder \texttt{md5sum} eine Checksumme gezogen und verglichen werden. \section{Dateien, die es nicht gibt} Eine Eigenart der Behandlung von Dateien unter Unix besteht im Verhalten beim Löschen. Gelöscht wird nämlich zunächst nur der Inode, also die Markierung im Dateisystem unter der die Datei gefunden werden kann. Physikalisch besteht die Datei noch, sie wird lediglich im Verzeichnis nicht mehr angezeigt. Hat ein Prozeß die Datei noch in irgendeiner Form geöffnet, kann er weiter darauf zugreifen. Erst wenn er die Datei schließt ist sie tatsächlich und unwiederbringlich `weg'. Dieser Effekt der `nicht existenten Dateien' läßt sich an verschiedenen Stellen geschickt einsetzen. \subsection{Daten aus einer Subshell hochreichen}\label{daten_hochreichen} Ein immer wieder auftretendes und oft sehr verwirrendes Problem ist, daß Variablen die in einer Subshell definiert wurden außerhalb dieser nicht sichtbar sind (siehe Abschnitt \ref{subshellschleifen}). Dies ist um so ärgerlicher, als daß Subshells auch bei vergleichsweise einfachen Pipelines geöffnet werden. Ein Beispiel für ein mißlingendes Skriptfragment wäre das folgende: \begin{lstlisting} nCounter=0 cat datei.txt | while read VAR; do nCounter=`expr $nCounter + 1` done echo "nCounter=$nCounter" \end{lstlisting} Die Variable nCounter wird mit 0 initialisiert. Dann wird eine Datei per Pipe in eine \texttt{while}-Schleife geleitet. Innerhalb der Schleife wird für jede eingehende Zeile die Variable hochgezählt. Am Ende der Schleife enthält die Variable tatsächlich den korrekten Wert, aber da die Pipe eine Subshell geöffnet hat ist der Wert nach Beendigung der Schleife nicht mehr sichtbar. Das \texttt{echo}-Kommando gibt die Zahl 0 aus. Es gibt mehrere Ansätze, diesem Problem zu begegnen. Am einfachsten wäre es in diesem Fall, dem Rat aus Abschnitt \ref{subshellschleifen} zu folgen und die Subshell geschickt zu vermeiden. Doch das ist leider nicht immer möglich. Wie geht man in solchen Fällen vor? Bei einfachen Zahlenwerten könnte beispielsweise ein Rückgabewert helfen. Komplexere Informationen können in eine temporäre Datei geschrieben werden, die danach geparst werden müßte. Wenn die Informationen in Zeilen der Form \lstinline|VARIABLE="Wert"| gespeichert werden, kann die Datei einfach mittels \texttt{source} (Abschnitt \ref{source}) oder einem Konstrukt der folgenden Art gelesen werden: \lstinline|eval `cat tempfile`| Und genau mit dieser Überlegung kommen wir zu einem eleganten~--~wenn auch nicht ganz einfachen~--~Trick. Anstatt die Daten in eine temporäre Datei zu schreiben, wo sie womöglich durch andere Prozesse verändert oder ausgelesen werden könnten, kann man sie auch in `nicht existente' Dateien schreiben. Das folgende Beispiel demonstriert das Verfahren: \begin{lstlisting} #!/bin/sh -x TMPNAME="/tmp/`date '+%Y%m%d%H%M%S'`$$.txt" exec 3> "$TMPNAME" exec 4< "$TMPNAME" rm -f "$TMPNAME" \end{lstlisting} Bis hierher wurde zunächst eine temporäre Datei angelegt. Die Filehandles 3 und 4 wurden zum Schreiben bzw. Lesen mit dieser Datei verbunden. Daraufhin wurde die Datei entfernt. Die Filehandles verweisen weiterhin auf die Datei, obwohl sie im Dateisystem nicht mehr sichtbar ist. Kommen wir zum nützlichen Teil des Skriptes: \begin{lstlisting}[firstnumber=6] nCounter=0 cat datei.txt | ( while read VAR; do while read VAR; do nCounter=`expr $nCounter + 1` done echo "nCounter=$nCounter" ) >&3 \end{lstlisting} Hier wurde wieder die Variable nCounter initialisiert und in der Subshell die Zeilen gezählt wie im ersten Beispiel. Allerdings wurde explizit eine Subshell um die Schleife gestartet. Dadurch steht die in der Schleife hochgezählte Variable auch nach Beendigung der Schleife zur Verfügung, allerdings immernoch nur in der Subshell. Um das zu ändern, wird in Zeile 11 der Wert ausgegeben. Die Ausgaben der Subshell werden in den oben erstellen Deskriptor umgeleitet. \begin{lstlisting}[firstnumber=13] echo "(vor eval) nCounter=$nCounter" eval `cat <&4` echo "(nach eval) nCounter=$nCounter" \end{lstlisting} Das \texttt{echo}-Kommando in Zeile 13 beweist, daß der Wert von nCounter tatsächlich außerhalb der Subshell nicht zur Verfügung steht. Zunächst. In Zeile 14 wird dann die ebenfalls oben schon angesprochene \texttt{eval}-Zeile benutzt, um die Informationen aus dem Filedeskriptor zu lesen, die die Schleife dort hinterlassen hat. Abschließend zeigt die Zeile 15, daß der Transport tatsächlich funktioniert hat, die Variable nCounter ist mit dem Wert aus der Subshell belegt. \subsection{Dateien gleichzeitig lesen und schreiben} Es kommt vor, daß man eine Datei bearbeiten möchte, die hinterher aber wieder unter dem gleichen Namen zur Verfügung stehen soll. Beispielsweise sollen alle Zeilen aus einer Datei entfernt werden, die nicht das Wort \textit{wichtig} enthalten. Der erste Versuch an der Stelle wird etwas in der Form \lstinline|grep wichtig datei.txt > datei.txt| sein. Das kann funktionieren, es kann aber auch in die sprichwörtliche Hose gehen. Das Problem an der Stelle ist, daß die Datei an der Stelle gleichzeitig zum Lesen und zum Schreiben geöffnet wird. Das Ergebnis ist undefiniert. Eine Elegante Lösung besteht darin, einen Filedeskriptor auf die Quelldatei zu legen und diese dann zu löschen. Die Datei wird erst dann wirklich aus dem Dateisystem entfernt, wenn kein Deskriptor mehr auf sie zeigt. Dann kann aus dem gerade angelegten Deskriptor gelesen werden, während eine neue Datei unter dem alten Namen angelegt wird: \begin{lstlisting} #!/bin/sh FILE=datei.txt exec 3< "$FILE" rm "$FILE" grep "wichtig" <&3 > "$FILE" \end{lstlisting} Allerdings sollte man bei dieser Methode beachten, daß man im Falle eines Fehlers die Quelldaten verliert, da die Datei ja bereits gelöscht wurde. \section{Auf der Lauer: Wachhunde}\label{wachhunde}\index{Watchdog} Es kommt vor, daß man einen Prozeß startet, bei dem man sich nicht sicher sein kann daß er sich auch in absehbarer Zeit wieder beendet. Beispielsweise kann der Timeout für einen Netzwerkzugriff deutlich höher liegen als erwünscht, und wenn der `gegnerische' Dienst nicht antwortet bleibt einem nur zu warten. Es sei denn, man legt einen geeigneten Wachhund\footnote{Der englische Begriff `Watchdog' ist in diesem Zusammenhang wahrscheinlich geläufiger...} auf die Lauer, der im Notfall rettend eingreift. In einem Shell-Skript könnte das wie folgt aussehen: \begin{lstlisting} #!/bin/sh timeout=5 ping 192.168.0.254 & cmdpid=$! \end{lstlisting} Bis hierher nichts aufregendes. Eine Variable wird mit dem Timeout belegt, also mit der Anzahl an Sekunden nach denen der zu überwachende Prozeß unterbrochen werden soll. Dann wird der zu überwachende Prozeß gestartet und mittels \& in den Hintergrund geschickt. Die Prozeß-ID des Prozesses wird in der Variablen cmdpid gesichert. \begin{lstlisting}[firstnumber=5] (sleep $timeout; kill -9 $cmdpid) & watchdogpid=$! \end{lstlisting} In Zeile 5 findet sich der eigentliche Watchdog. Hier wird eine Subshell gestartet, in der zunächst der oben eingestellte Timeout abgewartet und dann der zu überwachende Prozeß getötet wird. Diese Subshell wird ebenfalls mit \& in den Hintergrund geschickt. Die ID der Subshell wird in der Variablen watchdogpid gesichert. \begin{lstlisting}[firstnumber=7] wait $cmdpid kill $watchdogpid > /dev/null 2>&1 exit 0 \end{lstlisting} Dann wird durch ein \texttt{wait}\index{wait} darauf gewartet, daß sich der überwachte Prozeß beendet. Dabei würde \texttt{wait} bis in alle Ewigkeit warten, wäre da nicht der Watchdog in der Subshell. Wenn dem die Ausführung zu lange dauert, sorgt er dafür daß der Prozeß beendet wird. Kommt der überwachte Prozeß aber rechtzeitig zurück, sorgt \texttt{kill} in Zeile 8 dafür daß der Wachhund `eingeschläfert' wird. Auf diese Weise ist sichergestellt, daß der \texttt{ping} auf keinen Fall länger als fünf Sekunden läuft.