Shell-Programmierung/schmutzige_tricks.tex
2005-01-06 10:48:37 +00:00

206 lines
7.9 KiB
TeX

% $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:
\texttt{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 \texttt{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 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:
\texttt{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:
\texttt{tar cf - datei.txt | ssh 192.168.2.1 \dq(mkdir -p \$PWD ;cd \$PWD; tar xf -)\dq}
Hier wird die Datei verpackt und versendet. Eine Besonderheit gegenüber dem
vorigen Beispiel bestehtdarin, 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}
TODO!!! binaries inside
\subsection{Binäre Here-Dokumente}
TODO!!! binäre Here-Dokumente
\subsection{Schwanz ab!}
TODO!!! Schwanz ab
\section{Dateien, die es nicht gibt}
TODO!!! Dateien, die es nicht gibt
\subsection{Speichern in nicht existente Dateien}
TODO!!! Speichern in nicht existente Dateien
\subsection{Subshell-Schleifen vermeiden}\label{subshellschleifen}
Wir wollen ein Skript schreiben, das die \texttt{/etc/passwd} liest und dabei
zählt, wie viele Benutzer eine UID kleiner als 100 haben.
Folgendes Skript funktioniert nicht:
\begin{lstlisting}
#!/bin/sh
count=0
cat /etc/passwd | while read i; do
uid=`echo $i | cut -f 3 -d:`
if [ $uid -lt 100 ]; then
count=`expr $count + 1`
echo $count
fi
done
echo Es sind $count Benutzer mit einer ID kleiner 100 eingetragen
\end{lstlisting}
Was ist passiert?
Dieses Skript besteht im Wesentlichen aus einer Pipe. Wir haben ein
\texttt{cat}-Kom\-man\-do, das den Inhalt der \texttt{/etc/passwd} durch eben
diese Pipe an eine Schleife übergibt. Das \texttt{read}-Kommando in der
Schleife liest die einzelnen Zeilen aus, dann folgt ein Bißchen Auswertung.
Es ist zu beobachten, daß bei der Ausgabe in Zeile 7 die Variable
\texttt{\$count} korrekte Werte enthält. Um so unverständlicher ist es, daß sie
nach der Vollendung der Schleife wieder den Wert 0 enthält.
Das liegt daran, daß diese Schleife als Teil einer Pipe in einer Subshell
ausgeführt wird. Die Variable \texttt{\$count} steht damit in der Schleife
praktisch nur lokal zur Verfügung, sie wird nicht an das umgebende Skript
`hochgereicht'.
Neben der Methode in \ref{daten_hochreichen} bietet sich hier eine viel
einfachere Lösung an:
\begin{lstlisting}
#!/bin/sh
count=0
while read i; do
uid=`echo $i | cut -f 3 -d:`
if [ $uid -lt 100 ]; then
count=`expr $count + 1`
echo $count
fi
done < /etc/passwd
echo Es sind $count Benutzer mit einer ID kleiner 100 eingetragen
\end{lstlisting}
Hier befindet sich die Schleife nicht in einer Pipe, daher wird sie auch nicht
in einer Subshell ausgeführt. Man kann auf das \texttt{cat}-Kommando verzichten
und den Inhalt der Datei durch die Umlenkung in Zeile 9 direkt auf die
Standardeingabe der Schleife (und somit auf das \texttt{read}-Kommando) legen.
\subsection{Daten aus einer Subshell hochreichen}\label{daten_hochreichen}
TODO!!! Daten aus einer Subshell hochreichen
\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}
TODO!!! Auf der Lauer: Wachhunde