Shell-Programmierung/schmutzige_tricks.tex

206 lines
7.9 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
als Archiv gegeben wird, benutzt es~---~je nach der gew<65>hlten Aktion~---~die
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:
\texttt{tar cf - . | ( cd /tmp/backup; tar xf - )}
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
gestelltes \texttt{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 eine Partition
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:
\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 <20>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<65>ber dem
vorigen Beispiel bestehtdarin, da<64> das Zielverzeichnis bei Bedarf erstellt
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}
2004-11-05 16:20:53 +00:00
TODO!!! binaries inside
2001-07-02 12:52:18 +00:00
\subsection{Bin<EFBFBD>re Here-Dokumente}
2004-11-05 16:20:53 +00:00
TODO!!! bin<69>re Here-Dokumente
2001-07-02 12:52:18 +00:00
\subsection{Schwanz ab!}
2004-11-05 16:20:53 +00:00
TODO!!! Schwanz ab
2001-07-02 12:52:18 +00:00
\section{Dateien, die es nicht gibt}
2004-11-05 16:20:53 +00:00
TODO!!! Dateien, die es nicht gibt
2001-07-02 12:52:18 +00:00
\subsection{Speichern in nicht existente Dateien}
2004-11-05 16:20:53 +00:00
TODO!!! Speichern in nicht existente Dateien
2001-07-02 12:52:18 +00:00
2004-12-02 13:54:06 +00:00
\subsection{Subshell-Schleifen vermeiden}\label{subshellschleifen}
2002-03-25 13:48:40 +00:00
Wir wollen ein Skript schreiben, das die \texttt{/etc/passwd} liest und dabei
z<EFBFBD>hlt, wie viele Benutzer eine UID kleiner als 100 haben.
Folgendes Skript funktioniert nicht:
2004-12-10 14:38:03 +00:00
\begin{lstlisting}
2002-03-25 13:48:40 +00:00
#!/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
2004-12-10 14:38:03 +00:00
\end{lstlisting}
2002-03-25 13:48:40 +00:00
Was ist passiert?
Dieses Skript besteht im Wesentlichen aus einer Pipe. Wir haben ein
2004-11-19 12:09:34 +00:00
\texttt{cat}-Kom\-man\-do, das den Inhalt der \texttt{/etc/passwd} durch eben
diese Pipe an eine Schleife <20>bergibt. Das \texttt{read}-Kommando in der
Schleife liest die einzelnen Zeilen aus, dann folgt ein Bi<42>chen Auswertung.
2002-03-25 13:48:40 +00:00
Es ist zu beobachten, da<64> bei der Ausgabe in Zeile 7 die Variable
\texttt{\$count} korrekte Werte enth<74>lt. Um so unverst<73>ndlicher ist es, da<64> sie
nach der Vollendung der Schleife wieder den Wert 0 enth<74>lt.
Das liegt daran, da<64> diese Schleife als Teil einer Pipe in einer Subshell
ausgef<EFBFBD>hrt wird. Die Variable \texttt{\$count} steht damit in der Schleife
praktisch nur lokal zur Verf<72>gung, sie wird nicht an das umgebende Skript
2004-11-05 16:20:53 +00:00
`hochgereicht'.
2002-03-25 13:48:40 +00:00
Neben der Methode in \ref{daten_hochreichen} bietet sich hier eine viel
einfachere L<>sung an:
2004-12-10 14:38:03 +00:00
\begin{lstlisting}
2002-03-25 13:48:40 +00:00
#!/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
2004-12-10 14:38:03 +00:00
\end{lstlisting}
2002-03-25 13:48:40 +00:00
Hier befindet sich die Schleife nicht in einer Pipe, daher wird sie auch nicht
in einer Subshell ausgef<65>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}
2001-07-02 12:52:18 +00:00
2004-11-05 16:20:53 +00:00
TODO!!! Daten aus einer Subshell hochreichen
\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
\section{Auf der Lauer: Wachhunde}
TODO!!! Auf der Lauer: Wachhunde