Unterabschnitte

Standarddateien

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.


Pipeline: Datenfluß durch mehrere Kommandos

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.

Beispiel 9   Die Eingabe wird nacheinander durch drei Kommandos gefiltert: ein sed, welches alle zerronnen durch vergoren ersetzt, dann ein fgrep, welches nur Zeilen passieren läßt, die den Text gewonnen enthalten, und dann ein weiteres sed, welches gewonnen durch verloren ersetzt (die lange Eingabezeile ist mit einem \ 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.


Ein-/Ausgabeumlenkung

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.

Entsorgen unerwünschter Ausgaben:

Will man eine der Ausgaben überhaupt nicht haben, kann sie unter Unix nach /dev/null umgeleitet werden (oder beide zusammengelegt, wie oben beschrieben).

Umleitung von/zu geöffneten Dateien:

Weiterhin kann man anstatt eines Dateinamens auch (nach einem &-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.

Anhängen statt Überschreiben:

Eine als Ausgabe angegebene Datei wird durch die Umleitung gelöscht, falls sie bereits existiert (das kann man mit dem Kommando set -C verhindern). Will man an eine Datei etwas anhängen, verwendet man >> statt >.

Öffnen von Dateien

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.

here-Dokumente:

Eng mit der Eingabeumlenkung verwandt sind here-Dokumente. Dabei wird die Eingabe eines Kommandos direkt im Kommando erzeugt, indem als Operator << 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
ichhabefertig
Hier 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