Shell-Programmierung/schmutzige_tricks.tex

400 lines
16 KiB
TeX
Raw Normal View History

2003-04-11 15:05:25 +00:00
% $Id$
2001-07-02 12:52:18 +00:00
\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<64> 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<74>en, neue Techniken
kennenzulernen. Au<41>erdem kann das Wissen <20>ber gewisse Techniken eine gro<72>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<42>cke}
2005-01-06 10:48:37 +00:00
Eine sogenannte Tar-Br<42>cke benutzt man, wenn eine oder mehrere Dateien zwischen
Rechnern <20>bertragen werden sollen, aber kein Dienst wie SCP oder FTP zur
Verf<EFBFBD>gung steht. Au<41>erdem hat die Methode den Vorteil, da<64> Benutzerrechte und
andere Dateiattribute bei der <20>bertragung erhalten
bleiben\footnote{Vorausgesetzt nat<61>rlich, da<64> der Benutzer auf der entfernten
Seite <20>ber die n<>tigen Rechte verf<72>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
2005-01-14 16:27:08 +00:00
als Archiv gegeben wird, benutzt es~--~je nach der gew<65>hlten Aktion~--~die
2005-01-06 10:48:37 +00:00
Standard-Ein- bzw. -Ausgabe. Diese kann an ein weiteres \texttt{tar} <20>bergeben
werden um wieder entpackt zu werden.
Ein Beispiel verdeutlicht diese Kopier-F<>higkeit:
2005-01-21 17:23:30 +00:00
\lstinline_tar cf - . | ( cd /tmp/backup; tar xf - )_
2005-01-06 10:48:37 +00:00
Hier wird zun<75>chst der Inhalt des aktuellen Verzeichnisses `verpackt'. Das
Resultat wird an die Standard-Ausgabe geschrieben. Auf der Empf<70>ngerseite der
Pipe wird eine Subshell ge<67>ffnet. Das ist notwendig, da das empfangende
\texttt{tar} in einem anderen Verzeichnis laufen soll. In der Subshell wird
zun<EFBFBD>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<7A> mitsamt der Subshell.
Am Ziel-Ort finden sich jetzt die gleichen Dateien wie am Quell-Ort.
Das lie<69>e sich lokal nat<61>rlich auch anders l<>sen. Man k<>nnte erst ein Archiv
erstellen, das dann an anderer Stelle wieder auspacken. Nachteil: Es mu<6D>
gen<EFBFBD>gend Platz f<>r das Archiv vorhanden sein. Denkbar w<>re auch ein in den Raum
2005-01-21 17:23:30 +00:00
gestelltes
\lstinline_cp -Rp * /tmp/backup_
Allerdings fehlen einem dabei mitunter n<>tzliche
\texttt{tar}-Optionen\footnote{Mit \texttt{-l} verl<72><6C>t \texttt{tar}
beispielsweise nicht das File-System. N<>tzlich wenn nur eine Partition
2005-01-06 10:48:37 +00:00
gesichert werden soll.}, und die oben erw<72>hnte Br<42>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<42>cke zu einem anderen System,
dort wird entweder gepackt und versendet oder quasi die Subshell gestartet und
gelesen. Das sieht wie folgt aus:
2005-01-21 17:23:30 +00:00
\lstinline_ssh 192.168.2.1 tar clf - / | (cd /mnt/backup; tar xf - )_
2005-01-06 10:48:37 +00:00
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 <20>hnlich:
2005-01-21 17:23:30 +00:00
\lstinline_tar cf - datei.txt | ssh 192.168.2.1 "(mkdir -p $PWD ;cd $PWD; tar xf -)"_
2005-01-06 10:48:37 +00:00
Hier wird die Datei verpackt und versendet. Eine Besonderheit gegen<65>ber dem
vorigen Beispiel besteht darin, da<64> das Zielverzeichnis bei Bedarf erstellt
2005-01-06 10:48:37 +00:00
wird, bevor die Datei dort entpackt wird. Zur Erkl<6B>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.
2004-11-12 12:07:32 +00:00
2001-07-02 12:52:18 +00:00
\section{Binaries inside}
2005-01-14 16:27:08 +00:00
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<54>gersystem an.
Shell-Skripte sind mit wenigen Einschr<68>nkungen plattformunabh<62>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~--~<7E>blicherweise bin<69>ren~--~Pakete auf das Zielsystem
gebracht?
Im Prinzip gibt es daf<61>r zwei unterschiedliche Verfahren:
2001-07-02 12:52:18 +00:00
\subsection{Bin<EFBFBD>re Here-Dokumente}
2005-01-14 16:27:08 +00:00
\index{Here-Dokument}
Eine M<>glichkeit ist es, die bin<69>re Datei in Form eines Here-Dokuments
mitzuliefern. Da es aber in der Natur einer bin<69>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<64> 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
2005-02-03 22:24:18 +00:00
\texttt{uudecode} benutzt. Erstellt wurde das Dokument mit einer Zeile der
folgenden Form:
\lstinline|uuencode icon.png icon.png|
2005-01-14 16:27:08 +00:00
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<6D> an der
Stelle auf jeden Fall sichergestellt werden, da<64> keine existierenden Dateien
versehentlich <20>berschrieben werden.
Um diesen Abschnitt nicht allzu lang werden zu lassen <20>berspringen wir einen
Teil der Datei.
2001-07-02 12:52:18 +00:00
2005-01-14 16:27:08 +00:00
\begin{lstlisting}[firstnumber=38]
M#-""F4%,@%4.GUZ``"(*`6VW6!S#\>C_?/;__Q<R_S]<P/F7AXDA'I\>@``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} <20>berpr<70>ft und
im Fehlerfall eine Ausgabe gemacht. Im Erfolgsfall wird das Bild mittels
\texttt{display} angezeigt.
2001-07-02 12:52:18 +00:00
\subsection{Schwanz ab!}
2005-01-14 16:27:08 +00:00
Diese Variante basiert darauf, da<64> die bin<69>re Datei ohne weitere Codierung an
das Shell-Skript angeh<65>ngt wurde. Nachteil dieses Verfahrens ist, da<64> das
`abschneidende Kommando' nach jeder <20>nderung der L<>nge des Skriptes angepa<70>t
werden mu<6D>.
Dabei gibt es zwei Methoden, die angeh<65>ngte Datei wieder abzuschneiden. Die
einfachere Methode funktioniert mit \texttt{tail}:
2005-02-03 22:24:18 +00:00
\lstinline|tail -n +227 $0 > icon.png|
2005-01-14 16:27:08 +00:00
Dieses Beispiel geht davon aus, da<64> das Skript selbst 227 Zeilen umfa<66>t. Die
2005-02-03 22:24:18 +00:00
bin<EFBFBD>re Datei wurde mit einem Kommando wie \lstinline|cat icon.png >> skript.sh|
an das Skript angeh<65>ngt.
2005-01-14 16:27:08 +00:00
F<EFBFBD>r die etwas kompliziertere Variante mu<6D> die L<>nge des eigentlichen
Skript-Teiles genau angepa<70>t werden. Wenn das Skript beispielsweise etwa 5,5kB
lang ist, m<>ssen genau passend viele Leerzeilen oder Kommentarzeichen angeh<65>ngt
werden, damit sich eine L<>nge von 6kB ergibt. Dann kann das Anh<6E>ngsel mit dem
Kommando \texttt{dd} in der folgenden Form abgeschnitten werden:
2005-02-03 22:24:18 +00:00
\lstinline|dd bs=1024 if=$0 of=icon.png skip=6|
2005-01-14 16:27:08 +00:00
Das Kommando kopiert Daten aus einer Eingabe- in eine Ausgabedatei. Im
einzelnen wird hier eine Blockgr<67><72>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<69>lich
wird festgelegt, da<64> bei der Aktion die ersten sechs Block~--~also die ersten
sechs Kilobytes~--~<7E>bersprungen werden sollen.
Um es nochmal zu betonen: Diese beiden Methoden sind mit Vorsicht zu genie<69>en.
Bei der ersten f<>hrt jede zus<75>tzliche oder gel<65>schte Zeile zu einer kaputten
Ausgabedatei, bei der zweiten reichen schon einzelne Zeichen. In jedem Fall
2005-01-14 16:27:08 +00:00
sollte nach dem Auspacken noch einmal mittels \texttt{sum} oder \texttt{md5sum}
eine Checksumme gezogen und verglichen werden.
2001-07-02 12:52:18 +00:00
\section{Dateien, die es nicht gibt}
Eine Eigenart der Behandlung von Dateien unter Unix besteht im Verhalten beim
L<EFBFBD>schen. Gel<65>scht wird n<>mlich zun<75>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.
2001-07-02 12:52:18 +00:00
Hat ein Proze<7A> die Datei noch in irgendeiner Form ge<67>ffnet, kann er weiter
darauf zugreifen. Erst wenn er die Datei schlie<69>t ist sie tats<74>chlich und
unwiederbringlich `weg'.
2001-07-02 12:52:18 +00:00
Dieser Effekt der `nicht existenten Dateien' l<><6C>t sich an verschiedenen Stellen
geschickt einsetzen.
2001-07-02 12:52:18 +00:00
2002-03-25 13:48:40 +00:00
\subsection{Daten aus einer Subshell hochreichen}\label{daten_hochreichen}
2001-07-02 12:52:18 +00:00
2005-01-14 16:27:08 +00:00
Ein immer wieder auftretendes und oft sehr verwirrendes Problem ist, da<64>
Variablen die in einer Subshell definiert wurden au<61>erhalb dieser nicht
sichtbar sind (siehe Abschnitt \ref{subshellschleifen}). Dies ist um so
<EFBFBD>rgerlicher, als da<64> Subshells auch bei vergleichsweise einfachen Pipelines
ge<EFBFBD>ffnet werden.
Ein Beispiel f<>r ein mi<6D>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<65>hlt. Am Ende der Schleife enth<74>lt die
Variable tats<74>chlich den korrekten Wert, aber da die Pipe eine Subshell
ge<EFBFBD>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<6E>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<6F>re Datei geschrieben werden, die
danach geparst werden m<><6D>te. Wenn die Informationen in Zeilen der Form
\lstinline|VARIABLE="Wert"| gespeichert werden, kann die Datei einfach mittels
2005-02-03 22:24:18 +00:00
\texttt{source} (Abschnitt \ref{source}) oder einem Konstrukt der folgenden Art
gelesen werden:
\lstinline|eval `cat tempfile`|
2005-01-14 16:27:08 +00:00
Und genau mit dieser <20>berlegung kommen wir zu einem eleganten~--~wenn auch
nicht ganz einfachen~--~Trick.
Anstatt die Daten in eine tempor<6F>re Datei zu schreiben, wo sie wom<6F>glich durch
andere Prozesse ver<65>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<75>chst eine tempor<6F>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<65>hlt wie im ersten Beispiel. Allerdings wurde explizit eine Subshell
um die Schleife gestartet. Dadurch steht die in der Schleife hochgez<65>hlte
Variable auch nach Beendigung der Schleife zur Verf<72>gung, allerdings immernoch
nur in der Subshell. Um das zu <20>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<64> der Wert von nCounter
tats<EFBFBD>chlich au<61>erhalb der Subshell nicht zur Verf<72>gung steht. Zun<75>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<EFBFBD>end zeigt die Zeile 15, da<64> der Transport tats<74>chlich funktioniert
hat, die Variable nCounter ist mit dem Wert aus der Subshell belegt.
\subsection{Dateien gleichzeitig lesen und schreiben}
Es kommt vor, da<64> man eine Datei bearbeiten m<>chte, die hinterher aber wieder
unter dem gleichen Namen zur Verf<72>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
2004-12-10 14:38:03 +00:00
\lstinline|grep wichtig datei.txt > datei.txt|
sein. Das kann funktionieren, es kann aber auch in die sprichw<68>rtliche Hose
gehen. Das Problem an der Stelle ist, da<64> die Datei an der Stelle gleichzeitig
zum Lesen und zum Schreiben ge<67>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:
2004-12-10 14:38:03 +00:00
\begin{lstlisting}
#!/bin/sh
FILE=datei.txt
exec 3< "$FILE"
rm "$FILE"
grep "wichtig" <&3 > "$FILE"
2004-12-10 14:38:03 +00:00
\end{lstlisting}
2002-03-25 13:48:40 +00:00
Allerdings sollte man bei dieser Methode beachten, da<64> man im Falle eines
Fehlers die Quelldaten verliert, da die Datei ja bereits gel<65>scht wurde.
2004-11-05 16:20:53 +00:00
2005-01-14 16:27:08 +00:00
\section{Auf der Lauer: Wachhunde}\label{wachhunde}\index{Watchdog}
Es kommt vor, da<64> man einen Proze<7A> startet, bei dem man sich nicht sicher sein
kann da<64> er sich auch in absehbarer Zeit wieder beendet. Beispielsweise kann
der Timeout f<>r einen Netzwerkzugriff deutlich h<>her liegen als erw<72>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<65>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 <20>berwachende Proze<7A> unterbrochen
werden soll. Dann wird der zu <20>berwachende Proze<7A> gestartet und mittels \& in
den Hintergrund geschickt. Die Proze<7A>-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<75>chst der oben eingestellte Timeout abgewartet und dann
der zu <20>berwachende Proze<7A> get<65>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<64> sich der
<EFBFBD>berwachte Proze<7A> beendet. Dabei w<>rde \texttt{wait} bis in alle Ewigkeit
warten, w<>re da nicht der Watchdog in der Subshell. Wenn dem die Ausf<73>hrung zu
lange dauert, sorgt er daf<61>r da<64> der Proze<7A> beendet wird.
Kommt der <20>berwachte Proze<7A> aber rechtzeitig zur<75>ck, sorgt \texttt{kill} in
Zeile 8 daf<61>r da<64> der Wachhund `eingeschl<68>fert' wird.
2004-11-05 16:20:53 +00:00
2005-01-14 16:27:08 +00:00
Auf diese Weise ist sichergestellt, da<64> der \texttt{ping} auf keinen Fall
l<EFBFBD>nger als f<>nf Sekunden l<>uft.
2004-11-05 16:20:53 +00:00