Unter Unix (und den meisten gängigen anderen Betriebssystemen ebenso) haben neu gestartete Programme drei Dateien zur Verfügung, die nicht explizit geöffnet werden müssen:
Bei interaktiven Systemen sind die Standarddateien meistens mit den Ein- und Ausgabegeräten verbunden, mit denen der Benutzer arbeitet. Also wird Tastatureingabe in der Standardeingabe landen, und alles zur Standardausgabe und zur Standardfehlerausgabe geschriebene wird auf den Bildschirm oder in ein Fenster dort ausgegeben werden.
Bei einem Programmlauf im Stapelbetrieb wird entsprechender Ersatz geschaffen werden, beispielsweise Standardeingabe aus einem Lochkartenstapel oder einem Modem, und die Ausgabekanäle möglicherweise zu einem Drucker.
Wie alle anderen Dateien auch, werden (innerhalb von C-Programmen) diese mit den üblichen Funktionen (read(), write(), fcntl(), dup(), ...) verwendet.
Da die Dateien aber schon bei Programmstart geöffnet sind, ist ein open() beziehungsweise creat() sinnlos. Bei explizit geöffneten Dateien liefern diese beiden Funktionen aber den Dateideskriptor (eine ganze Zahl), der für alle Dateioperationen nötig ist.
Stattdessen werden für die vorgeöffneten Dateien feste Zahlen verwendet, und zwar für die Standardeingabe der Wert 0, für die Standardausgabe 1 und für die Standardfehlerausgabe 2; mit diesen Konstanten als Dateideskriptoren kann man also die Dateifunktionen ansprechen7.
Viele Programme verwenden nun für die Ein- und Ausgabe gar keine
konkreten Dateien, sondern lesen einfach nur von der Standardeingabe
und schreiben das gewünschte Ergebnis zur Standardausgabe. Treten
Fehler auf (oder sollen andere Informationen ausgegeben werden, die
nicht zum normalen Ergebnis gehören; beispielsweise Warnungen oder
statistische Angaben wie der verbrauchten CPU-Zeit), dann wird
dafür die Standardfehlerausgabe verwendet.
Anmerkung: Solche Programme, die diesem einfachen Schema
Lesen einer Zeichenfolge von der Standardeingabe, irgendwie
verarbeiten, Schreiben zur Standardausgabe entsprechen, heißen
Filterprogramme.
Dies vereinfacht die Programme deutlich, weil keine Dateinamen erfragt und benutzt werden müssen, und macht sie zumindest unter unixähnlichen Systemen gleichzeitig viel flexibler. Die Zuordnung der drei vordefinierten Dateideskriptoren zu konkreten Dateien oder zur Verknüpfung mit anderen Programmen kann nämlich vom Aufrufer in der Shell vorgenommen werden, ohne daß sich die aufgerufenen Programme darum kümmern müssen.
Auf die Voreinstellung der Standarddateien kann mittels Pipelines (Pipeline: Datenfluß durch mehrere Kommandos) und Ein-/Ausgabeumlenkung (Ein-/Ausgabeumlenkung) Einfluß genommen werden.
Für die folgenden Beispiele wird das Unixkommando sed verwendet, ein ganz typisches Filterprogramm. sed steht für stream editor: das Programm liest einen Zeichenstrom von der Standardeingabe, bearbeitet ihn wie ein Editor anhand von Kommandos, die als Parameter übergeben werden, und schreibt den möglicherweise geänderten Text wieder zur Standardausgabe. Ein Kommando kann mit der Option -e angegeben werden, dem der auszuführende Befehl folgt (siehe man 1 sed). Hier wird nur ein Kommando verwendet, nämlich s (für substitute) zur Ersetzung von Text. Hinter s folgen drei Schrägstriche; zwischen dem ersten und dem zweiten steht der zu ersetzende Text, zwischen dem zweiten und dritten der Ersatztext.
Damit könnte ein Aufruf von sed so aussehen:
klaus@aw35: ~ >
sed -e s/gewonnen/verloren/
wir haben die Wahl gewonnen!
wir haben die Wahl verloren!
die anderen sind die schlechteren.
die anderen sind die schlechteren.
Wie gewonnen, so zerronnen!
Wie verloren, so zerronnen!
Ein weiteres in den folgenden Beispielen verwendete Kommando ist fgrep; dieses Kommando bekommt in seiner einfachsten Form ein
Argument übergeben, liest seine gesamte Eingabe und leitet zur Ausgabe
alle Zeilen weiter, in denen das übergebene Argument enthalten ist:
klaus@athlon1:/lap2/klaus/skript+shellprogrammierung >
fgrep onnen
wir haben die Wahl gewonnen!
wir haben die Wahl gewonnen!!
die anderen sind die schlechteren.
Wie gewonnen, so zerronnen!
Wie verloren, so zerronnen!
Hier wird die zweite Eingabezeile unterdrückt, weil sie nicht den Text onnen enthält.
Eine Pipeline besteht aus einem oder mehreren einfachen Kommandos,
die (falls es mehrere sind) durch das
pipeline symbol (das ist das
Zeichen |
) getrennt sind.
Alle Kommandos einer Pipeline laufen gleichzeitig, wobei das erste Kommando die normale Standardeingabe zu sehen bekommt, während alle anderen Kommandos als Eingabe jeweils die Standardausgabe ihres vorhergehenden Kommandos erhalten. Die Standardausgabe des letzten Kommandos der Pipeline wird dann zur Ausgabe der gesamten Pipeline.
Die Standardfehlerausgabe aller Kommandos wird durch diesen Mechanismus nicht beeinflußt.
\
umgebrochen,
damit sie auf die Seite paßt):
klaus@aw35: ~ >
sed -e s/zerronnen/vergoren/ | \
fgrep gewonnen | sed -e s/gewonnen/verloren/
wir haben die Wahl gewonnen!
die anderen sind die schlechteren
Wie gewonnen, so zerronnen.
wir haben die Wahl verloren!
Wie verloren, so vergoren.
Anmerkung: Durch die interne Pufferung wird jetzt nicht mehr nach jeder Eingabezeile sofort die Ausgabe sichtbar, sondern erst nach dem Ende der Eingabe mit Strg-D. Dies hat aber auf die beschriebenen Mechanismen keinen weiteren Einfluß. Aber in jedem Fall kann man sich nicht darauf verlassen, wann die Ausgabe (bezogen auf die Eingabe) erfolgt. Allerdings kann man sich darauf verlassen, daß die Reihenfolge innerhalb des Eingabestroms und innerhalb der Ausgabeströme von der Shell nicht manipuliert wird.
(Man kann sich zum Verständnis eine Pipeline auch so vorstellen, daß das erste Kommando wie üblich aufgerufen wird, aber seine gesamte Ausgabe irgendwo heimlich gesammelt wird. Anschließend wird das zweite Kommando aufgerufen, und mit der gesammelten Ausgabe des Vorgängers gefüttert, wobei dessen Ausgabe wiederum gesammelt wird, und so weiter.)
Damit können mehrere Kommandos zu einem komplexeren zusammengefaßt werden, und erscheinen von außen betrachtet wieder wie ein Kommando.
Optional darf vor der gesamten Pipeline noch
time
oder
time -p
geschrieben werden.
Dadurch wird die anschließend angegebene Pipeline wie üblich ausgeführt, und zuletzt die verbrauchte Rechen- und Gesamtzeit ausgegeben.
In der -p-Variante erfolgt diese Ausgabe nach POSIX-Standard (in der bash).
Tip: Wenn man in einer Pipeline wissen möchte, was zwischen zwei
Kommandos übertragen wird, kann man das leicht protokollieren. Dazu
gibt es das Kommando tee (gesprochen wie der englische Buchstabe
T). Es wird mit einem Dateinamen aufgerufen, und funktioniert wie ein
T-Stück in der Pipeline: alles, was zu seiner
Eingabe kommt, wird sowohl unverändert zur Ausgabe kopiert, als auch in
die angegebene Datei geschrieben:
...kommando_n-1 | tee a.log | kommando_n...
erzeugt in der Datei a.log eine Kopie der Daten, die von
kommando_n-1
nach kommando_n
fließen.
Für eine gesamte Pipeline (Pipeline: Datenfluß durch mehrere Kommandos), also im einfachsten Fall auch für ein einfaches Kommando, können die drei Standarddateien auf andere Dateien umgeleitet werden.
Diese drei Umlenkungen können einzeln verwendet werden, oder kombiniert. Die Reihenfolge untereinander ist insoweit egal.
In jedem Fall gelten die Umleitungen für die gesamte Pipeline; die Eingabe wird also bei mehreren Programmen in der Pipeline für das erste Kommando umgeleitet, und die beiden Ausgaben für das letzte Kommando der Pipeline.
Für die Eingabe geschieht dies, indem hinter8 der Pipeline nach einem
Kleinerzeichen (<
) ein Dateiname angegeben wird:
fgrep gewonnen <a.txt
führt wie sonst auch das Kommando fgrep gewonnen
aus,
allerdings wird die Eingabe nicht von der Tastatur gelesen, sondern
aus der Datei a.txt.
Analog kann man die Standardausgabe mit >
Dateiname und
die Standardfehlerausgabe mit 2>
Dateiname in eine Datei
umleiten (die Ziffer 2 in 2>
kommt davon, daß die
Standardfehlerausgabe intern den Dateideskriptor 2 hat).
Generell kann man vor das Umleitungszeichen den zugehörigen
Dateideskriptor schreiben, sodaß <
nur ein Synonym für
0<
ist, und analog >
und 1>
identisch sind.
&
-Zeichen)
den Deskriptor einer
geöffneten Datei angeben (falls man ihn kennt natürlich).
Dies wird meistens dazu genutzt, die Standardfehlerausgabe mit der
Standardausgabe zusammenzulegen:
Kommando 2>&1
leitet die Fehlerausgabe (2) auf die Standardausgabe (1)
um.Dabei muß man allerdings beachten, daß eine weitere Umleitung der
Standardausgabe die Fehlerausgabe nicht ,,mitnimmt``. Will man also
beide Ausgaben in einer einzigen Datei wiederfinden, muß man das so
schreiben (die Umleitungen werden immer von links her ausgeführt!):
Kommando >
Dateiname 2>&1
Im umgekehrten Fall ( 2>&1
>
Dateiname) würde
erst die Fehlerausgabe zur Standardausgabe umgelenkt; letztere dann in
die Datei, aber ohne die Fehlerausgabe mitzunehmen.
Bei der bash gibt es zum Zusammenlegen der beiden Ausgaben
in eine Datei auch die Kurzform &>
Dateiname.
Analog kann man auch für ein Kommando in einer Pipeline erreichen, daß
seine Fehlerausgabe in die Pipeline einfließt:
Kommando1 2>&1 |
Kommando2
Dadurch werden sowohl Standardausgabe als auch die
Standardfehlerausgabe von Kommando1 an Kommando2 weitergereicht.
Wenn es nur die Fehlerausgabe sein soll, geht das natürlich auch:
Kommando1 2>&1 >/dev/null |
Kommando2
(die Reihenfolge ist wichtig!).
Die direkte Angabe der Dateideskriptoren kann man weiterhin nutzen, um
in einem eigenen Programm Dateien zu öffnen, und anschließend ein
weiteres Programm aufzurufen, wobei dessen Standarddateien auf die
eigenen geöffneten umgebogen werden können:
int eingabe = open( "eingabe.txt", O_RDONLY );
int ausgabe = creat "ausgabe.txt", 0600 );
// ... eingabe, ausgabe prüfen ...
char kommando[1024];
sprintf( kommando, "prg arg <&%d 2>%d", eingabe, ausgabe );
system( kommando );
Dabei wird in kommando eine Kommandozeile zusammengebaut; wenn
die Deskriptoren beispielsweise die Werte 12 und 14 haben, dann würde
die Kommandozeile den Inhalt prg arg <&12 2>%14
haben; also
liest das mit system() gestartete Programm aus der Datei
eingabe.txt und schreibt nach ausgabe.txt.
>>
statt >
.
Falls man mehr Dateien als die drei vordefinierten benötigt, kann man
am Ende einer Pipeline weitere öffnen mit einer Konstruktion der Art
Deskriptor<
Dateiname (zum Lesen)
beziehungsweise Deskriptor>
Dateiname zum
Schreiben. Dabei ist Deskriptor eine ganze Zahl (größer als 2
natürlich, weil 0, 1, und 2 ja schon vergeben sind). Von allen so
geöffneten Dateien kann dann in der Pipeline lesen oder oder darauf
schreiben durch Angabe des Deskriptors.
Es hängt von der Shell ab, wieviel Dateien geöffnet werden können. Portabel kann man bis zur Nummer 9 gehen; die bash limitiert nur durch die maximal mögliche Anzahl offener Dateien des Betriebssystems.
<<
angegeben wird,
gefolgt von einer frei wählbaren Endemarkierung. Aus dem Skript (oder
von der Tastatur bei einem interaktiven Kommando, da macht es nur
keinen Sinn) wird dann Text gelesen und an das Kommando
weitergereicht, bis die Endemarkierung am Anfang einer Zeile
auftritt:
fgrep Hallo << ichhabefertig Hallo, Max Holdrio Hallo, mein Name ist $USER ichhabefertigHier wird alles nach dem fgrep-Kommando bis (ausschließlich) der ichhabefertig-Zeile als Standardeingabe an das fgrep gereicht.
In dieser Form wird innerhalb des here-Dokuments wie üblich eine
Expansion von Variablen, Kommandos und arithmetischen Ausdrücken
durchgeführt. Der Endetext hinter Operator <<
wird dagegen
generell nicht ausgewertet.
Allerdings kann er ganz oder teilweise in Gänsefüßchen oder Apostrophe
eingeschlossen werden (quoted); dann findet innerhalb des
here-Dokuments auch keine Ersetzung mehr statt (beispielsweise
für $USER
).
Eine weitere Variante ist die Verwendung des
Operators <<-
anstatt <<
. Dadurch werden im gesamten here-Dokument in jeder
Zeile führende Leerzeichen und Tabulatoren entfernt. Dadurch lassen
sich Skripte schöner schreiben, ohne die Einrückungen zu
unterbrechen.
AnyWare@Wachtler.de