Script-Programmierung

Datenströme

  1. Datenstrommodell

  2. Datenausgabe

  3. Dateneingabe

  4. Datenverkettung durch Pipes

  5. Zeilenorientiertes Lesen von Standardin mit read


Durch die bash hat man mehrere Möglichkeiten, Daten aus einem Script an andere Kommandos oder Scripte zu übertragen.

  1. Datenstrommodell

    Um die Funktion der Shell zu verstehen, ist die Kenntnis des Datenstrommodells wichtig. Nach dem EVA-Prinzip, kann man sich die Funktionsweise von Programmen so vorstellen, dass sie Eingaben (E) entgegennehmen, diese Verarbeiten (V) und danach das Ergebnis ausgeben (A).

    Die Funktionsweise des Programms ist dabei durch Parameter (P) steuerbar, die dem Programm beim Aufruf mitgegeben werden. Auf die Prozesse im Linuxsystem übertragen, sieht es nun so aus, dass jeder Prozess mindestens einen Dateneingang und mindestens einen Datenausgang haben kann. Diese Datenkanäle sind standardisiert und werden numerisch verwaltet. Der Dateneingang eines Prozesses wird Standardeingabe (stdin, Nummer 0) und der Datenausgang Standardausgabe (stdout, Nummer 1) genannt. Der Prozess selbst muss sich nicht darum kümmern, wo die Datenquelle (Quelle des Datenstroms) und die Datensenke (Ziel der Datenausgabe) wirklich liegen, er verarbeitet nur den Datenstrom, der ihm an stdin angeboten wird und schafft die Ergebnisse nach stdout.

    Den Prozessen wird weiterhin noch der Datenkanal Nummer 2 (stderr) zur Verfügung gestellt, auf dem der Prozess Fehlermeldungen ausgeben kann. Wohin diese Kanäle nun letztendlich zeigen, legt die Shell fest. Durch spezielle Sonderzeichen kann man sehr flexibel und einfach auch weitere Eingabe- und Ausgabekanäle definieren, über die der Prozess mit anderen Datenquellen und senken kommunizieren kann.

    Befinden wir uns in einer Interaktiven-Loginshell, so sind die Datenkanäle 0, 1 und 2 standardmäßig durch die bash mit dem aufrufenden Terminal verbunden. Wenn wir nun einen Prozess starten, indem wir ein Kommando eingeben, dann werden die Datenkanäle 0,1 und 2 des gestarteten Prozesses mit unserem Terminal verbunden. Der Prozess liest also Eingaben aus stdin von der Tastatur unseres Terminals und damit landen die Tastatureingaben im Standardeingabekanal stdin des Prozesses. Da das Terminal im Normalfall so eingestellt ist, dass es die Eingabedaten erst nach einem <newline> absendet, kann der Prozess die Daten auch erst dann empfangen. Es wird also nicht jede einzelne Tastatureingabe an den Prozess gesendet. Das ist von großem Vorteil, weil es zum einen die Bandbreite der Datenübertragung schont und Korrekturmöglichkeiten bei der Eingabe (backspace etc) auf das Terminal auslagert und so den Linuxrechner von simplen Eingabefunktionen befreit. Der Komfort bei der Editierung der Eingabe ist also komplett Sache des Terminals. Auf Wunsch kann man das Verhalten des Terminals aber ändern. Das nach (fast) jeder gedrückten Taste unserer Tastatur aber ein Zeichen auf dem Bildschirm erscheint ist keine Funktionalität der bash, sondern Ergebnis der aktivierten echo-Funktion des Terminals. Der Prozess selbst erfährt im Normalfall erst nach dem abschliessenden <Enter> von unseren Eingaben. Die Ausgaben des Prozesses auf stdout (Kanal 1) und stderr (Kanal 2) werden an das Terminal zurückgesendet und landen auf dem Bildschirm. Das bedeutet, dass die Ausgaben des Prozesses und seine Fehlermeldungen auf das gleiche Ziel geschrieben werden und deshalb auf dem Bildschirm vermischt werden. Diese Vermischung tritt nur ein, weil beide Kanäle auf die selbe Datensenke geleitet werden und deshalb fällt die Unterscheidung am Bildschirm manchmal schwehr. Trotzdem können und werden die Meldungen unterschiedlich behandelt und ausgewertet. Die Zuordung der Filedescriptoren hat also die folgende Form:
    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdouttty-in1 (Terminalbildschirm)
    Datenkanal 2-stderrtty-in2 (Terminalbildschirm)
    Die Besonderheit besteht nun darin, dass man diese Kanäle umleiten kann und so die Wege der Datenströme verändert werden.

  2. Datenausgabe

    Die Ausgaben der Kommandos auf stdout landen also normalerweise auf dem Bildschirm unseres Terminals. Die bash bietet nun jedoch eine Reihe von Möglichkeiten, diesen Datenstrom umzuleiten. Am Beispiel des Kommandos echo sieht das dann so aus. Mit echo haben wir also eine einfache Möglichkeit, Daten über den Standardausgabekanal wieder auszugeben. Das echo-Kommando liest eine Reihe von Parametern und gibt sie über stdout wieder aus.

    bash-2.05b$ echo "Hello World"
    Hello World
    bash-2.05b$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdouttty-in1 (Terminalbildschirm)
    Datenkanal 2-stderrtty-in1 (Terminalbildschirm)

    Wenn ich jetzt aber diese Ausgabe in eine Datei schreiben will, dann bietet echo dafür keine Optionen. Es gibt eine Reihe von Kommandos in der Konsole, die weder aus einer Datei lesen, noch in eine Datei schreiben können. In den üblichen Programmiersprachen muss mit einem speziellen Befehl (z.B. open) ein Filedescriptor zum Schreiben in eine Datei geöffnet werden. Eine solche Möglichkeit sucht man in der Shell vergeblich. Der Grund dafür ist genauso einfach wie genial. Man überläßt diese Arbeit der Shell selbst und leitet die Datenströme der Kommandos, die über einen Ausgabekanal (z.B. Standardout) gesendet werden, einfach in eine Datei um. Damit kann man durch eine einfache Umleitung die Ausgabe jedes Kommandos z.B. in Dateien ablegen, ohne dass das Kommando selbst diese Funktionalität besitzen muss. Dazu kennt die bash eine Reihe von Umleitungszeichen. Die "Hello World"-Ausgabe soll nun nicht auf dem Bildschirm landen, sondern in die Datei /tmp/test1.text geschrieben werden. Dafür bietet die Shell das Umleitungszeichen >, das an echo-Kommando angehängt wird. Genau genommen müsste das Umleitungszeichen eigentlich 1> sein, aber der Umleitungsoperator > ist eine zulässige Kurzschreibweise, da der stdout-Datenkanal 1 sehr oft umgeleitet wird. Durch diese Umleitung zeigt der datenkanal 1(stdout) nicht mehr auf das Terminal, weshalb die Meldungen nicht auf dem Bildschirm, sondern in der datei /tmp/test1.text landen. Alle anderen Datenkanäle behalten ihre Zuordnungen bei. Eventuelle Fehlermeldungen von echo landen weiterhin auf dem Bildschirm.

    bash-2.05b$ echo "Hello World" > /tmp/test1.text
    bash-2.05b$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test1.text
    Datenkanal 2-stderrtty-in2 (Terminalbildschirm)

    Auf dem Bildschirm erscheint keinerlei Ausgabe, denn durch den Umleitungsoperator > wird der Datenstrom des Ausgabekanals in die Datei /tmp/test1.text umgeleitet und erscheint deshalb nicht mehr in unserem Terminal. Sollte die Datei vorher noch nicht existiert haben, so wird sie durch die Shell angelegt. Wenn die Datei vorher schon existiert hat, wird der alte Inhalt der Datei überschrieben und geht verloren. Das Schreiben in die Datei funktioniert natürlich nur, wenn der Benutzer über die entsprechenden Rechte verfügt. Ansonsten erzeugt die Shell eine Fehlermeldung und bricht das Kommando ab. Mit dem Kommando cat können wir schnell nachschauen, was in der Datei /tmp/test1.text gespeichert ist.. Das cat-Kommando liest der Inhalt der Datei und gibt ihn auf stdout aus.

    bash-2.05b$ cat /tmp/test1.text
    Hello World
    bash-2.05b$
    Ein weiterer wichtiger Umleitungsoperator ist >>. Er funktioniert genauso wie >, nur wird der Inhalt der Datei nicht überschrieben, sondern die Ausgabe an das Ende der Datei angehängt.
    bash-2.05b$ echo "noch ein Hello" >> /tmp/test1.text
    bash-2.05b$ cat /tmp/test1.text
    Hello World
    noch ein Hello

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test1.text
    Datenkanal 2-stderrtty-in2 (Terminalbildschirm)

    Wird dem Umleitungoperator > keine Zahl mitgegeben, so interpretiert die Shell dies als 1>, dh. es wird stdout umgeleitet. Wird vor dem Operator > eine Zahl mitgegeben, so kann man auch andere Ausgabekanäle (z.B. mit 2> der Standard-Fehlerkanal) umleiten.

    Nehmen wir an, wir wollen mehrere Dateien (in diesem Fall nur zwei) mit cat nacheinander ausgeben und das Ergebnis in einer neuen Datei /tmp/test2.text speichert. Die beiden Dateien sollen /tmp/test1.text und die nicht existierende Datei /tmp/gibtsnicht sein. Wenn wir das Kommando cat /tmp/test1.text /tmp/gibtsnicht dazu in der Konsole aufrufen, ergibt sich folgendes Ergebnis:

    bash-3.1$ cat /tmp/test1.text /tmp/gibtsnicht
    Hello World
    noch ein Hello
    cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdouttty-in1 (Terminalbildschirm)
    Datenkanal 2-stderrtty-in2 (Terminalbildschirm)

    Da noch keine Umleitung definiert wurde, landen alle Ausgaben, sowohl stdout als auch stderr auf dem Bildschirm unseres Terminals. Fügen wir jetzt die Umleitung des Datenstroms mit 1>/tmp/test2.text oder einfach nur >/tmp/test2.text mit an, dann sieht das so aus:

    bash-3.1$ cat /tmp/test1.text /tmp/gibtsnicht 1> /tmp/test2.text
    cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test2.text
    Datenkanal 2-stderrtty-in2 (Terminalbildschirm)

    Wenn wir nachschauen, werden wir sehen, dass die Datei /tmp/test2.text nun existiert und dass sie den entsprechenden Text aus Datei test1.text enthält, denn die zweite datei konnte ja nicht geöffnet werden. Die Fehlermeldung dazu landet weiterhin auf unserem Terminal, denn die wurde ja nicht umgeleitet.
    Wir können die Fehlermeldung ebenfalls in eine Datei umleiten, dazu benutzen wir den Umleitungsoperator 2>/tmp/fehlermeldungen.

    bash-3.1$ cat /tmp/test1.text /tmp/gibtsnicht 1> /tmp/test2.text 2> /tmp/fehlermeldungen
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test2.text
    Datenkanal 2-stderr/tmp/fehlermeldungen

    Da nun keiner der beiden Datenkanäle 1 und 2 auf das Terminal zeigt, erfolgt auch keine Ausgabe von cat auf dem terminalbilschirm. Dafür befindet sich nun der Dateiinhalt von /tmp/test1.text in der Datei Test2.text und die Fehlermeldung "cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden" befindet sich wie gewünscht in der Datei /tmp/fehlermeldungen.

    Und nun leiten wir beide Datenkanäle in die selbe Datei /tmp/test2.text um. Man könnte nun meinen, dass das mit cat /tmp/test1.text /tmp/gibtsnicht 1> /tmp/test2.text 2> /tmp/test2.text geht. Im Grunde genommen stimmt das auch, aber wenn man sich das Ergebnis anschaut wird man feststellen, dass sich nur die Fehlermeldung in der Datei findet.
    bash-3.1$ cat /tmp/test1.text /tmp/gibtsnicht 1> /tmp/test2.text 2> /tmp/test2.text
    bash-3.1$ cat /tmp/test2.text 
    cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test2.text
    Datenkanal 2-stderr/tmp/test2.text

    Der Grund liegt darin, dass zwei Filedescriptoren geöffnet werden, die beide in eine leere (oder noch nicht vorhandene) Datei schreiben und den eventuell vorher existierenden Inhalt der Datei löschen. Also wird zuerst über einen Filedescriptor die stdout-Ausgabe von cat in die Datei /tmp/test2.text geschrieben und der alte Inhalt der Datei geht verloren. Danach kommt die Fehlermeldung und die wird über den zweiten Filedescriptor in die selbe datei geschrieben und der alte Inhalt geht verloren. Sinnvoller ist es, der bash zu sagen, dass beide Kanäle über den selben Filedescriptor umgeleitet werden sollen. Das geht mit dem Umleitungsoperator 2>&1. Dieser besagt, Der Datenkanal 2 kopiert sich den Filedescriptor, den Datenkanal 1 momentan hat. Das komplette Kommando lautet dann: cat /tmp/test1.text /tmp/gibtsnicht 1> /tmp/test2.text 2>&1.

    bash-3.1$ cat /tmp/test1.text /tmp/gibtsnicht 1> /tmp/test2.text 2>&1
    bash-3.1$ cat /tmp/test2.text 
    Hello World
    noch ein Hello
    cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test2.text
    Datenkanal 2-stderr

    Hier wird jetzt also zuerst der Datenkanal 1 (stdout) auf die Datei /tmp/test2.text umgeleitet. Danach kopiert sich Datenkanal 2 (stderr) den Filedescriptor den Datenkanal 1 jetzt hat und zeigt damit in den selben Filedescriptor wie stdout, nämlich in die Datei /tmp/test2.text.

    Würde man die beiden Umleitungsoperatoren in der Reihenfolge vertauschen, also das Kommando cat /tmp/test1.text /tmp/gibtsnicht 2>&1 1> /tmp/test2.text eingeben, so würde folgendes passieren.
    bash-3.1$ cat /tmp/test1.text /tmp/gibtsnicht 2>&1 1> /tmp/test2.text
    bash-3.1$ cat /tmp/test2.text 
    Hello World
    noch ein Hello
    cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out0 (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test2.text
    Datenkanal 2-stderrtty-in1
    Zuerst würde der Datenkanal 2 (stderr) sich den Filedescriptor kopieren, den Datenkanal 1 (stdout) gerade benutzt, das ist tty-in, also unser Terminalkanal 1 auf den Bildschirm und danach würde Datenkanal 1 des Kommandos cat eine Umleitung in die Datei bekommen. Die Ausgabe von cat würde also in der Datei /tmp/test2.text landen und die Fehlermeldung über Datenkanal1 der bash auf unserem Bildschirm.

    Das könnte z.B. sinnvoll sein, wenn man die Daten in die Datei transportieren möchte und die Fehlermeldung in einer Variable gespeichert werden soll. Da mit der Kommandosubstitution $( kommando ) nur die Ausgaben auf Datenkanal 1 (stdout) abgefangen werden, kann man so die Fehlermeldung auffangen.

    bash-3.1$ A="$( cat /tmp/test1.text /tmp/gibtsnicht 2>&1 1> /tmp/test2.text )" 
    bash-3.1$ echo $A
    cat: /tmp/gibtsnicht: Datei oder Verzeichnis nicht gefunden
    bash-3.1$

    Datenkanäle des Prozesses    Zieldescriptoren
    Datenkanal 0-stdintty-out (Terminaltastatur)
    Datenkanal 1-stdout/tmp/test2.text
    Datenkanal 2-stderrtty-in1 → VAR A

    Wichtig ist noch, das die Umleitung prinzipiell in jede Art von Datei möglich ist. Das bedeutet auch in Gerätedateien (z.B. auf die serielle Schnittstelle /dev/ttyS0) oder in Netzwerkverbindungen. Genaueres siehe man bash.

  3. Dateneingabe

    Am Beispiel des echo-Kommandos haben wir nun zwei einfache Möglichkeiten der Umleitung von Ausgabekanälen kennengelernt. Mindestens genauso wichtig ist die Dateneingabe, damit ein Script Daten entgegennehmen und verarbeiten kann, bevor sie dann wieder ausgegeben werden können. Viele Kommandos bieten die Möglichkeit, einen Dateinamen anzugeben, aus denen die Daten gelesen werden. Wird kein solcher Dateiname angegeben, so lesen die Kommandos im allgemeinen von stdin. Das kann man z.B. am Kommando cat sehen. Genau genommen liest cat den Datenstrom an stdin und gibt diesen an stdout wieder aus. Vorhin haben wir dem Kommando cat einen Dateinamen mitgegeben, und damit hat cat dann die Daten aus dieser Datei gelesen. Wenn wir nun keinen Dateinamen angeben, wartet cat auf Daten an stdin. Also geben wir in der Konsole einfach mal cat ein.

    bash-2.05b$ cat

    Es passiert scheinbar nichts und der Prompt kehrt auch nicht wieder zurück. Das cat-Kommando wartet nun auf Daten am Eingabekanal stdin, dieser ist momentan mit unserer Tastatur verbunden. Also geben wir nun einfach "Hello World" ein und schliessen mit der Entertaste ab. Das Drücken der Entertaste ist notwendig, da in den aktuellen Einstellungen des Terminals der Text erst nach Enter abgesendet wird und damit an stdin von cat ankommt.

    bash-2.05b$ cat
    Hello World
    Hello World

    Sofort nach dem Drücken der Entertaste erscheint die Ausgabe auf unserem Bildschirm und cat wartet auf weitere Daten. Weitere Eingaben landen nun ebenfalls über stdout auf unserem Bildschirm.

    bash-2.05b$ cat
    Hello World
    Hello World
    noch ein Hello
    noch ein Hello

    Das geht so lange, bis cat ein EOF (End of File) erkennt, das können wir in unserem Terminal z.B. mit <Strg>+<D> auslösen. Das cat-Kommando beendet sich und der Prompt kehrt zurück.
    Als kleine Demonstartion, was man mit den Umleitungen der Datenkanäle erreichen kann, hier zwei Beispiele. Wir legen eine Datei an, die einen Text enthält und werden eine Sicherungskopie der Datei anfertigen. Zum Anlegen der Datei benötigen wir keinen Editor, sondern benutzen einfach cat, wobei wir die Ausgaben in eine Datei umleiten. Das Kommando cat liest den Datenstrom von stdin, der von unserer Tastatur stammt, bis wir die eingabe mit <Strg>+<D> beenden. Die Ausgabe von cat auf stdout leiten wir mit dem Umleitungsoperator > in die Datei /tmp/test2.text um. Der Text soll wieder nur "Hello World" sein, könnte aber natürlich auch mehrere Zeilen umfassen. Danach muss noch ein Enter gedrückt werden, damit das Terminal den String auch absendet und danach <Strg>+<D>, damit cat auch das Ende des Datenstroms erkennt.

    bash-2.05b$ cat > /tmp/test2.text
    Hello World
    bash-2.05b$

    Vielleicht fällt jemandem hier ein scheinbarer Widerspruch auf. Da unsere Tastatureingaben nach stdin von cat geleitet werden und die Ausgaben von cat in die Datei /tmp/test2.text gehen, warum erscheinen dann unsere Tastatureingaben auf dem Bildschirm? Die Ursache liegt in einer Eigenschaft unseres Eingabeterminals, das im Normalfall mit eingeschalteter echo-Funktion läuft. Das bedeutet, dass sämtliche Tastatureingaben durch das Terminal selbst sofort auf dem Bildschirm ausgegeben werden. Die Einstellungen kann man mit dem Kommando stty -a einsehen, dort steht echo ohne vorangestelltes -, was bedeutet, dass die echo-Funktion eingeschaltet ist. Wer das testen will, kann die echo-Funktion gern mit stty -echo abschalten, nur muss er dann alle Kommandos blind eingeben, da ja nur die Ausgaben der Kommandos auf unserem Bildschirm landen. Die echo-Funktion des Terminals wird z.B. bei der Passworteingabe abgeschaltet. Man sollte die echo-Funktion also wieder mit stty echo aktivieren, denn so lässt sich etwas besser arbeiten :-)
    Nun zurück zum Thema. Mit cat können wir uns nun davon überzeugen, dass der Text wirklich in der Datei gespeichert wurde.

    bash-2.05b$ cat /tmp/test2.text
    Hello World
    bash-2.05b$

    So, nun zur Sicherungskopie. Der Inhalt der Datei /tmp/test2.text soll in eine Datei /tmp/test3.text kopiert werden. Das können wir einmal mit einer einfachen Ausgabeumleitung erreichen, indem wir cat den Dateinamen /tmp/test2.text mitteilen. Die Datei wird geöffnet und der Inhalt auf stdout ausgegeben, den wir in die Datei /tmp/test3.text umleiten.

    bash-2.05b$ cat /tmp/test2.text > /tmp/test3.text
    bash-2.05b$

    Eine weitere interessante Möglichkeit ist die Umleitung von stdin, sodass der Datenstrom nicht mehr von unserer Tastatur stammt, sondern aus einer Datei. Das bewerkstelligt der Umleitungsoperator <.

    bash-2.05b$ cat < /tmp/test2.text
    Hello World
    bash-2.05b$

    Durch diesen Umleitungsoperator verbindet die bash den Dateneingang stdin des Kommandos cat mit der Datei /tmp/test2.text. Es werden nun also nicht mehr die Tastatureingaben an den stdin von cat geleitet, sondern der Dateiinhalt von /tmp/test2.text. Nun muss nur die Ausgabe auf stdout ebenfalls umgeleitet werden und die Datei wird somit kopiert.

    bash-2.05b$ cat < /tmp/test2.text > /tmp/test3.text
    bash-2.05b$
    
        

    Die bash wird nun also das Kommando cat starten, durch die Umleitung "< /tmp/test2.text" den Inhalt der Datei /tmp/test2.text an den Dateneingabekanal stdin senden. Die Ausgaben des Kommandos cat werden durch die Umleitung "> /tmp/test3.text" in die Datei /tmp/text3.txt geschickt. Da die Umleitungen eine Funktionalität der bash sind, funktioniert das mit den anderen Kommandos in ähnlicher Weise. Bleibt noch zu bemerken, dass die Befehlszeile /tmp/test2.text > cat > /tmp/test3.text zu einer Fehlermeldung führt, da die bash in einer Befehlszeile an erster Stelle den Kommandonamen erwartet.

    Wie leistungsfähig die Umleitungsmechanismen der Shell sind, soll mit einem kleinen Beispiel verdeutlicht werden. Der folgende Einzeiler ist im Prinzip ein Mini-Webbrowser, der die Startseite von google.de anfordert und den HTML-Quellcode der Seite auf dem Terminal ausgibt.
    Der Befehl lautet: (echo -ne "GET http://www.google.de/index.html HTTP/1.0 \n\n" 1>&0 ; cat ) <> /dev/tcp/www.google.de/80
    Wenn der Rechner direkten Internetkontakt hat (nicht über einen Proxy!) und einen DNS kontaktieren kann, erscheint nach der Eingabe ein Wust von HTML-Zeichen, die den Inhalt der Google-Startseite darstellen.
    Eine kurze Erklärung zu dem Einzeiler:

    ( ........ ) <> /dev/tcp/www.google.de/80
         

    Mit dem Kontrolloperator () wird eine Subshell geöffnet. Das bedeutet, das in der Shell eine weitere Shell gestartet wird, in der mehrere Kommandos ausgeführt werden, die dann alle über die umgeleiteten Datenkanäle der Subshell kommunizieren können. Mithilfe des R/W-Umleitungsoperators <> wird auf dem Datenkanal 0 (stdin) eine Schreib- und Leseverbindung über einen TCP-Socket auf das Netzwerk geöffnet. Es wird der Host www.google.de auf dem TCP-Port 80 (http) kontaktiert. Alle Ausgaben der Subshell auf Datenkanal 0 (stdin) werden an den Host www.google.de über das Netzwerk umgeleitet und alle Ausgaben des Hosts www.google.de landen auf Datenkanal 0 (stdin) der Subshell.

    echo -ne "GET http://www.google.de/index.html HTTP/1.0 \n\n" 1>&0 ;
     

    Mit dem echo innerhalb der Subshell wird die Anfrage an den Webserver erstellt. Ein Webserver erwartet von einem Webbrowser einen String, der bei einer Get-Anfrage mit GET beginnt, gefolgt von der URL der gewünschten Datei. Die Angabe HTTP/1.0 teilt dem Webserver lediglich mit, welchen HTML-Standard der Browser versteht. Die Anfrage wird mit einem Zeilenumbruch \n und einer Leerzeile \n abgeschlossen. Die Ausgabe von echo wird mit 1>&0 auf den selben Filedescriptor wie Datenkanal 0 (stdin) umgeleitet und geht damit über die Netzwerkverbindung der Subshell an den Google-Server.

    cat
     

    Das cat-Kommando erwartet nun einfach einen Datenstrom von stdin in der Subshell, also in diesem Fall vom Webserver und gibt ihn an Datenkanal 1 (stdout) wieder aus. Da stdout nicht umgeleitet wurde, landen die Ausgaben der Subshell auf dem Terminalbildschirm. Prinzipiell könnte man die cat-Ausgabe innerhalb der Subshell mit cat > /tmp/googleindex.html auch in eine Datei umleiten und hätte diese dann auf seiner Festplatte. Das geht natürlich auch mit allen anderen Dateien (Bilder, MP3 etc) so.

    Mit diesem kleinen Scriptstück ist man also in der Lage, eine beliebige Datei aus dem Internet herunterzuladen und das in einem Textterminal und ohne Zusatzsoftware, einfach mit der Shell.

    bash-2.05b$ (echo -ne "GET http://www.google.de/index.html HTTP/1.0 \n\n"  >&0 ; cat ) <> /dev/tcp/www.google.de/80
    HTTP/1.0 200 OK
    Date: Sat, 02 Jul 2011 11:08:15 GMT
    Expires: -1
    Cache-Control: private, max-age=0
    Content-Type: text/html; charset=ISO-8859-1
    Set-Cookie: PREF=ID=24b7309b4b6ecca6:FF=0:TM=1309604895:LM=1309604895:S=6fvnmaBWxkTepoIj; expires=Mon, 01-Jul-2013 11:08:15 GMT; path=/; domain=.google.de
    Set-Cookie: NID=48=NTg8isAQaFdwVoId4kepj3AaEI_15OWYc8ZVS8pRltYDDSOMQDrwuCzCOvHtkI7f17eC0qSx1yKuGCzGjXtJO5ioY639H1ZDFUNiFMmlzDzmvieHifCPxbrfbMAjGoel; expires=Sun, 01-Jan-2012 11:08:15 GMT; path=/; domain=.google.de; HttpOnly
    Server: gws
    X-XSS-Protection: 1; mode=block
    
    <!doctype html><html><head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"><title>Google</title><script>window.google={kEI:"H_wOTqOLItSAswaMiJHSDQ",kEXPI:"28505,28936,30316,30465,31091,31116,31259",kCSI:{e:"28505,28936,30316,30465,31091,31116,31259",ei:"H_wOTqOLItSAswaMiJHSDQ",expi:"28505,28936,30316,30465,31091,31116,31259"},authuser:0,ml:function(){},kHL:"de",time:function(){return(new Date)
    .....
    bash-2.05b$
  4. Datenverkettung durch Pipes

    Die Datenkanäle mehrerer Kommandos können durch sogenannte Pipes miteinander verbunden werden. Eine Pipe wird in der bash durch den Operator ¦ erzeugt. Eine Pipe verbindet den Standardausgabekanal stdout eines Kommandos mit dem Standardeingabekanal stdin des folgenden Kommandos. Dadurch wird erreicht, das die Ausgaben des ersten Kommandos durch das folgende Kommando weiter verarbeitet werden können.
    Dazu ein Beispiel.
    Es soll eine Liste der IP-Adressen aller Netzwerkgeräte erzeugt werden, die im System aktiv sind. Auskunft über die Netzwerkschnittstellen soll das Kommando ifconfig geben. Also geben wir in unsere Konsole /sbin/ifconfig ein.

    bash-2.05b$ /sbin/ifconfig
    lo        Link encap:Local Loopback  
              inet addr:127.0.0.1  Mask:255.0.0.0
              UP LOOPBACK RUNNING  MTU:16436  Metric:1
              RX packets:319 errors:0 dropped:0 overruns:0 frame:0
              TX packets:319 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:0 
              RX bytes:589003 (575.1 Kb)  TX bytes:589003 (575.1 Kb)
    
    tap0      Link encap:Ethernet  HWaddr 00:FF:E0:EF:64:BF  
              inet addr:10.0.0.208  Bcast:10.0.0.255  Mask:255.255.255.0
              UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:12450 errors:0 dropped:0 overruns:0 frame:0
              TX packets:12066 errors:0 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:100 
              RX bytes:7569658 (7.2 Mb)  TX bytes:1506105 (1.4 Mb)
    
    wlan0     Link encap:Ethernet  HWaddr 00:E0:98:E2:95:98  
              inet addr:192.168.1.208  Bcast:192.168.1.255  Mask:255.255.255.0
              UP BROADCAST NOTRAILERS RUNNING MULTICAST  MTU:1500  Metric:1
              RX packets:20826 errors:3 dropped:0 overruns:0 frame:0
              TX packets:13921 errors:7 dropped:0 overruns:0 carrier:0
              collisions:0 txqueuelen:1000 
              RX bytes:5659759 (5.3 Mb)  TX bytes:2529460 (2.4 Mb)
              Interrupt:9
    bash-2.05b$

    Es erscheint eine mehr oder weniger lange Liste mit den konfigurierten Netzwerkgeräten, die eine Reihe von Angaben zu den Einstellungen enthält. Aus dieser Liste interessieren uns nur die IP-Adressen, die hinter "inet addr:" angegeben sind. Es müssen nun also alle Zeilen aus der Ausgabe von ifconfig herausgesucht werden, die den String "inet addr:" enthalten. Das erreichen wir mit dem Kommando grep, dem wir den Suchbegriff mitgeben. Das grep-Kommando erwartet an stdin den zu durchsuchenden Datenstrom, den es Zeilenweise abarbeitet. Dazu leiten wir jetzt die Ausgabe von /sbin/ifconfig nach stdin von grep um, indem wir eine Pipe legen. Damit landet die Ausgabe von /sbin/ifconfig nicht mehr auf unserem Bildschirm, sondern in der Eingabe von grep. Das wiederum wird den Datenstrom durchsuchen und alle Zeilen, die den text "inet addr:" enthalten an stdout ausgeben, was dann auf unserem Bildschirm zu sehen ist.

    bash-2.05b$ /sbin/ifconfig | grep "inet addr:"
              inet addr:127.0.0.1  Mask:255.0.0.0
              inet addr:10.0.0.208  Bcast:10.0.0.255  Mask:255.255.255.0
              inet addr:192.168.1.208  Bcast:192.168.1.255  Mask:255.255.255.0
    bash-2.05b$

    So, nun muss aus der Ausgabe noch die IP-Adresse selbst ausgeschnitten werden. Das besorgt das Kommando cut. Dieses Kommando teilt den Datenstrom an stdin in einzelne Teile (Felder) auf, wobei z.B. ein Trennzeichen mit -d definiert werden kann. Die einzelnen Felder werden durchnummeriert und können dadurch gezielt ausgegeben werden. Das Kommando cut arbeitet ebenfalls zeilenorientiert. Wir lassen den Datenstrom am Doppelpunkt aufteilen und damit befindet sich die gewünschte IP-Adresse im 2.Feld. Die cut-Befehlszeile lautet also cut -d: -f2. Da cut nun die Ausgabe von grep verarbeiten soll, hängen wir das Kommando mit einer weiteren Pipe an grep an.

    bash-2.05b$ /sbin/ifconfig | grep "inet addr:" | cut -d: -f2
    127.0.0.1  Mask
    10.0.0.208  Bcast
    192.168.1.208  Bcast
    bash-2.05b$

    Nun stören nur noch die Anhängsel und das erledigen wir mit einem weiteren cut. Trennzeichen ist das Leerzeichen und das erste Feld wird ausgegeben.

    bash-2.05b$ /sbin/ifconfig | grep "inet addr:" | cut -d: -f2 | cut -d' ' -f1
    127.0.0.1
    10.0.0.208
    192.168.1.208
    bash-2.05b$

    Fertig. Über die Eleganz dieser Lösung kann man sich streiten, aber es sollte lediglich die Funktionsweise einer Pipe demonstriert werden :-)
    Entscheidend ist, das durch die Pipes mehrere Kommandos miteinander zu einer Liste verkettet werden können, sodass jedes Kommando wie ein Filter die Ausgabe des vorhergehenden weiter bearbeitet, bis letztendlich die gewünschte Information herausgefiltert ist. Um diesen Mechanismus nutzen zu können ist es wichtig, das die Kommandos und auch selbst erstellte Scripte ihre Daten von stdin lesen können. Die volle Tragweite der Datenstromumleitung in eine Datei oder aus einer Datei heraus wird klar, wenn man sich bewußt mach, dass in Linux gilt:"Alles ist eine Datei!". So werden z.B. im Verzeichnis /dev Pseudo-Dateien erzeugt, mit denen der Kernel den Zugriff auf die Hardware regelt und im Verzeichnis /proc sehen wir die Schnittstelle zum Kernel selbst und hier sind auch lauter Dateien.

  5. Zeilenorientiertes Lesen von Standardin mit read

    Innerhalb eines Scriptes möchte man nun die Eingaben über stdin verarbeiten. Dazu ist es oft sinnvoll, die daten in einer Variable zu speichern und mit Hilfe von cat und der Kommandosubstitution der bash ist das auch möglich. In einem Script test könnte das wie folgt aussehen:
    #!/bin/bash
    echo "Bitte geben Sie Ihren Namen ein:"
    NAME=$( cat )
    echo "Guten Tag lieber ${NAME} !"

    Wenn wir dieses Script ausführbar machen und starten, dann erwartet es nach der ersten Ausgabe eine Eingabe an stdin (also momentan von der Tastatur), die wir mit Enter und <Strg>+<D> abschliessen. Es erscheint die gewünschte Ausgabe. Wir können die Eingabe auch durch eine Pipe tätigen, also z.B. mit echo "Walter" | ./test.
    Diese Methode des Lesens von stdin hat jedoch mehrere Nachteile.

    1. Sie ist nicht besonders bedienerfreundlich, denn jede Eingabe über die Tastatur ist mit Enter und <Strg>+<D> zu beenden. Das kann schnell nerven.
    2. Bei mehreren Eingaben auf diese Art und Weise gibt es Probleme mit der Pipe, da der Datenstrom bei der ersten Eingabe ja schon sein EOF absenden muss.
    3. Wenn das Script in einer Pipe liegt und das vorherige Kommando sehr große Datenmengen ausgibt, werden diese erst einmal komplett eingelesen und in der Variable zwischengespeichert, bevor das Script weiter ausgeführt wird. Das kann sehr schnell zu Speicherproblemen führen.
    4. Wenn der Eingabedatenstrom unendlich ist (zb aus einem fortlaufenden Logfile gelesen wird), wird das Kommando cat nie beendet und das Script hängt.

    Das Kommando read ist hier eine effiziente Alternative. Im Normalfall liest das Kommando read eine Eingabezeile von Standardin, die mit Enter (Zeilenschaltung <newline>) abgeschlossen wird. Die eingelesenen Daten werden in einer oder mehreren Variablen oder einem Array abgelegt. Die Eingabe wird im Normalfall mit <newline> beendet. Hier ein paar Beispiele:

    #!/bin/bash
    read TESTA
    echo "Die Variable TESTA enthält $TESTA"
    read -p "Bitte geben Sie Ihren Namen ein: " NAME
    echo "Die Variable NAME enthält $NAME"
    read -s -p "Bitte geben Sie Ihr Passwort ein: " PASSWORT
    echo
    echo "Die Variable PASSWORT enthält $PASSWORT"
    echo -n "Bitte geben Sie Ihren Vor- und Zunamen ein: "
    read VNAME ZNAME DUMMY
    echo "Der Vorname ist $VNAME, der Nachname ist $ZNAME, die restlichen Eingaben sind $DUMMY"
    echo "Welches sind Ihre Liebligsspeisen? "
    read -a ESSEN
    echo "Das Array ESSEN enthaelt folgende Elemente ${ESSEN[*]}"
    read -t 10 -p ">" WERT
    echo "Der exit-status der letzten read-Anweisung ist $?"
    echo "Die Variable WERT enthaelt $WERT"
    read -p "Alles klar?"
    echo "Die Variable REPLY enthaelt $REPLY"

    Die Scriptzeilen haben folgende Bedeutungen.
    Zuerst liest ein read die komplette Eingabe und speichert sie in der Variablen TESTA ab. Die Eingabe kann aus keinem, einem oder mehreren Worten bestehen, die Eingabe wird mit <newline> abgeschlossen. Anschliessend erfolgt die Ausgabe mit echo.
    Das näste read verwendet die Option -p um einen Erläuterungstext auf Standarderr auszugeben. Die Eingabe wird in der Variablen NAME gespeichert.
    In der dritten read-Anweisung wird mit der Option -s (silent-mode) erreicht, das die eingegebenen Zeichen nicht wieder im Terminalschirm angezeigt werden, das Passwort also verdeckt bleibt. Da auch das abschließende <newline> nicht auf dem Bildschirm erscheint, wird mit einem einfachen echo eine Zeilenschaltung erzeugt.
    Danach erfolgt die Benutzerführung nicht durch die Option -p, sondern durch eine vorherige Ausgabe des Erläuterungstextes durch echo. Der read-Anweisung werden die drei Variablen VNAME, ZNAME und DUMMY mitgegeben. read trennt die Eingabe anhand der Trennungszeichen (Leerzeichen, Tabulator je nach dem Inhalt der Shellvariable IFS) in einzelne Worte auf. Das erste eingegebene Wort wird in VNAME, das zweite Wort in ZNAME und der ganze, eventuell zuviel eingegebene Rest, in DUMMY abgespeichert.
    In der fünften read-Anweisung kommt nun ein Array zum Einsatz. Mit der Option -a wird read mitgeteilt, das es die Eingabe in Worte splitten soll und diese fortlaufend ab Index 0 (Null) in dem Array ESSEN abspeichern soll. Wenn das Array noch nicht existiert, so wird es durch diese Anweisung angelegt und entsprechend der Eingabe gefüllt. Sollte das Array bereits existiert haben, dann gehen alle vorher darin abgelegten Inhalte verloren.
    Die nächste read-Anweisung benutzt die Option -t um einen Timeout festzulegen. read wartet 10 Sekunden lang auf die Eingabe der Daten. Wird diese nicht innerhalb der Zeit vollständig abgeschlossen, so bricht read mit einem Fehler ab, die Variable WERT ist dann leer. Der exit-status 1 meldet diesen Fehler. Wurde die Eingabe rechtzeitig beendet, so wird diese in WERT abgespeichert und der exit-status ist 0 (Null).
    In der letzten read-Anweisung wurde keine Variable zum Abspeichern der Eingabe angegeben. In diesem Fall speichert read das Ergebnis in der Shellvariable REPLY ab.
    Die Terminalausgabe zu dem Script könnte so aussehen:

    bash-2.05b$ ./test
    Hallo, wer da?
    Die Variable TESTA enthält Hallo, wer da?
    Bitte geben Sie Ihren Namen ein: frank
    Die Variable NAME enthält frank
    Bitte geben Sie Ihr Passwort ein: 
    Die Variable PASSWORT enthält brotkorb
    Bitte geben Sie Ihren Vor- und Zunamen ein: Frank Schlaefendorf noch was
    Der Vorname ist Frank, der Nachname ist Schlaefendorf, die restlichen Eingaben sind noch was
    Welches sind Ihre Liebligsspeisen? 
    Brot Butter und Käse
    Das Array ESSEN enthaelt folgende Elemente Brot Butter und Käse
    Der exit-status der letzten read-Anweisung ist 1
    Die Variable WERT enthaelt 
    Alles klar?ja
    Die Variable REPLY enthaelt ja
    bash-2.05b$




Sollte jemand seine Rechte durch eine Veröffentlichung auf dieser oder einer anderen meiner Seiten verletzt sehen, bitte ich um sofortige Kontaktaufnahme. Ich werde die entsprechenden Inhalte umgehend entfernen. Somit sind sowohl ein anwaltlicher Rat als auch eine kostenpflichtige Abmahnung nicht erforderlich! Weiterhin weise ich darauf hin, dass der Inhalt verlinkter Seiten nicht in meiner redaktionellen Verantwortung liegt.
Vielen Dank

www.schlaefendorf.de 2012

www.linux-web.de