Script-Programmierung

Hallo Welt


Die meisten Lehrbücher zu einer Programmiersprache beginnen mit dem unvermeidlichen "Hallo Welt"-Programm. Also los.
Das "Hallo Welt"-Programm könnte nun folgendermaßen aussehen:

#!/bin/bash
echo Hallo Welt

Wir speichern diese Datei in unserem Homeverzeichnis unter dem Namen test1 ab und geben mit dem Befehl chmod 700 test1 das Lese-, Schreib- und Ausfühungsrecht für uns als Besitzer der Datei frei. Alle anderen User erhalten keine Rechte. Nun rufen wird die Datei auf. Wenn wir uns in der Konsole in unserem Homeverzeichnis befinden, geschieht das durch die Eingabe von ./test1 und Enter.

bash-2.05b$ ./test
Hallo Welt
bash-2.05b$

Alles in Ordnung. Nun könnte man dem Irrtum unterliegen, das unser Script gestartet wird und der echo-Befehl in diesem Script nichts anderes tut, als den Text hinter dem Befehlswort an den Bildschirm auszugeben.
Weit gefehlt, genau das tut echo nicht, es sieht im Endergebnis nur so aus. Um das genau zu verstehen, ist eine Betrachtung der Grundprinzipien notwendig.
Genau genommen haben Bildschirm und Tastatur überhaupt nichts mit dem Linuxsystem zu tun. Zu Zeiten der Großrechner (und eigentlich auch heute) wäre es eine Verschwendung von Rechenzeit und Geldmitteln gewesen, wenn an einer Maschine nur eine Tastatur und ein Bildschirm angeschlossen wäre, an der zu einer bestimmten Zeit nur eine Person arbeitet. Deshalb wurde die Benutzerschnittstelle auf Terminals ausgelagert, die nur aus einer Tastatur und einem Bildschirm bestanden und mit dem eigentlichen Rechner über eine serielle Schnittstelle verbunden waren. Von diesen Terminals konnten sehr viele an eine Unixmaschine angeschlossen werden und das ist unter Linux genauso. Diese Terminals sind recht dumm und ihre Aufgabe besteht eigentlich nur darin, Eingaben von der Tastatur entgegenzunehmen und über sie serielle Schnittstelle an den Rechner zu senden, also einen Datenstrom zu erzeugen, den der Rechner empfängt. Der Datenstrom der vom Rechner kommt, wird auf dem Terminal dargestellt, dass konnte ein Bildschirm sein, oder auch ein Drucker. Ein solches Terminal kann also zB. auch eine elektrische Schreibmaschine oder ein Fernschreiber sein. Auch heute können Sie einen Linuxrechner über die serielle COM-Schnittstelle mit einem Terminal verbinden. Prinzipiell ist das auch über andere Schnittstellen möglich, wie zB. die parallele Schnittstelle, Modems, ISDN-Adapter, Ethernetadapter etc. Ein Terminal muss man nicht unbedingt kaufen, da es auch Terminalemulationen für PC gibt, wie das Hyper-Terminal, Telnet-Clienten usw, die Softwareemulationen sind.
Wenn nun also viele Personen an unzähligen Terminals gleichzeitig arbeiten, dann ist klar, dass eine Ausgabe auf "dem Bildschirm" vollkommen unsinnig ist. Davon könnte es locker mehrere hundert an einem Rechner geben und wenn unser echo nun seine Ausgabe auf alle Bildschirme schreibt, entsteht gelinde gesagt ein heilloses Durcheinander der Ausgaben aller Benutzer. Es muss also klar geregelt werden, welches Programm von welchem Terminal seine Eingaben erhalten muss und an welches Terminal die Ausgaben zu senden sind, nur so ist ein sinnvolles Arbeiten an einem Unixsystem möglich.
Man könnte nun sagen "Ich arbeite allein an meinem Rechner, habe nur eine Tastatur und einen Bildschirm und brauche das alles nicht!"
Auch das ist zu kurz gedacht. Wenn man das Linuxsystem etwas genauer kennt, dann ist einem bewusst, das gleichzeitig viele Prozesse laufen, die von unterschiedlichen Usern (meist virtuellen) gestartet wurden. Sie haben auch die Möglichkeit selbst an mehreren Terminals angemeldet zu sein um dort unterschiedliche Aufgabe zu erledigen. Sollten Sie immer noch der Meinung sein, dass das nutzlos und unnötig kompliziert ist, dann sollten Sie diese Seite verlassen und auf Ihrem Rechner DOS installieren, das dürfte dann das System Ihrer Wahl sein.
Für die anderen geht es nun weiter.
Da ein PC im Normalfall über eine Tastatur und einen Bildschirm verfügt, sind in die Linuxdistributionen eine Reihe von Terminalemulationen bereits integriert, sodass man nicht extra ein externes Terminal anschliessen muss, sondern die Benutzerschnittstelle des PC gleich als Terminal nutzen kann. Von diesen Terminalemulationen werden beim Systemstart durch den init-Daemon oft mindestens sechs Stück automatisch mitgestartet. Das sind die sogenannten Textkonsolen. Wenn eine grafische Benutzeroberfläche installiert ist (X-Server mit einem Windowmanager wie zB. kde oder xfce oder ähnliche), dann erreichen Sie die Textkonsolen im Allgemeinen über die Tastenkombination <Strg><Alt><F1> bis <Strg><Alt><F6> und zwischen den Textkonsolen können Sie mit <Alt><F1> bis <Alt><F6> hin und herwechseln. Diese Textkonsolen emulieren nun sechs Textterminals, wirken also so, als hätten Sie an Ihren Linuxrechner sechs Hardwareterminals gleichzeitig angeschlossen. Mit der Tastenkombination <Alt><F7> kommen Sie dann wieder in die grafische Oberfläche, die auch nur eine Terminalemulation ist!!!! An ihrem Linuxrechner ist dann gewissermaßen noch ein grafisches X-Terminal angeschlossen. Innerhalb der graphischen Bedienoberfläche haben Sie ebenfalls mehrere Terminalemulationen zur Verfügung, wie zB. xterm oder terminal usw usf, von denen Sie auch gleichzeitig mehrere starten können.
Halten wir also fest, es ist möglich und auch sinnvoll, über viele Terminals auf das System zuzugreifen. Diese Terminals können von einem oder mehreren Benutzern gleichzeitig verwendet werden und das Linuxsystem muss sicherstellen, dass diese sich nicht gegenseitig stören. Der Betrieb muss also so organisiert werden, dass ein Benutzer an einem Terminal (egal ob ein reales Hardwareterminal oder eine Terminalemulation, egal ob Textterminal oder Graphikterminal) immer das Gefühl hat, dass er allein den Rechner steuert. Eingaben von seiner Tastatur müssen sein Programm, dass er an seinem Terminal gestartet hat, erreichen und die Ausgaben seines Programms müssen auch auf seinem Terminal landen.
Das gilt deshalb auch für die grafische Oberfläche, denn die ist genaugenommen auch nur eine Terminalemulation, die "zufällig" auf der selben Maschine läuft, genau wie die virtuellen Textkonsolen. Genau wie die Textterminals kann die grafische Oberfläche ebenfalls aus der Ferne über eine Schnittstelle gestartet werden. Der Zugriff des Benutzers ist unter Linux eigentlich immer ein Remotezugriff, da jedoch gleichzeitig verschiedene Terminalemulationen mit gestartet werden, wird die Bentzerschnittstelle des PC gleich als Terminal genutzt. Es bleibt in jedem Fall ein Remotezugriff, egal ob Textkonsole oder grafische Oberfläche. Benutzerschnittstelle und Linuxsystem sind ganz klar voneinander getrennt und dass sich Tastatur, Bildschirm, Maus etc an dem selben Rechner befinden ist nur ein Sonderfall!!
Das ist ein wesentlicher Unterschied zu Windows. Unter Unix sind Benutzerschnittstellen (Tastatus, Bildschirm etc) externe Komponenten, die als Terminal mit dem System Kontakt aufnehmen. Ein Remote-Zugriff auf den Unixrechner ist deshalb von jeher schon immer möglich und ein wesentlicher Bestandteil des Systems. Windows ist aus DOS entstanden, dass als Einzelplatzsystem konzipiert war und die Remotefähigkeiten mussten erst nachträglich eingebaut werden.

Nun zurück zum "Hallo Welt"-Script. Aus der Sicht des Linuxsystems ist es nun so, dass Sie als Benutzer über ein Terminal Kontakt aufgenommen haben. Das könnte so abgelaufen sein, dass Sie die erste Textkonsole genutzt haben. Dort lauert ein getty-Prozess, bis jemand eine Eingabe macht.
Welcome to Linux 2.6.21.5 (tty1)

ramses login:
Sie müssen Ihren Usernamen+<Enter> eingeben. Danach werden Sie nach Ihrem Password gefragt, dass Sie ebenfalls eingeben. Wundern Sie sich nicht, dass bei der Passworteingabe nichts auf dem Bildschirm erscheint. Das ist gewollt, damit niemand das Passwort mitlesen kann und auch die Anzahl der Zeichen nicht erkennt. Im Hintergrund startet ein login-Programm, dass Ihre Angaben überprüft und wenn alles korrekt war, dann erhalten Sie eine Ausgabe wie zB.
Welcome to Linux 2.6.21.5 (tty1)

ramses login:frank
Password:
Linux 2.6.21.5.
Last login: Sun Dec 23 07:57:16 +0100 2007 on tty1.
No mail.

frank@ramses:~$
Sie befinden sich jetzt in einer interaktiven Shell. Das Linuxsystem hat erkannt, dass Sie sich von einem Textterminal tty1 aus angemeldet haben und das Sie der User frank (in meinem Beispiel) sind. Die Bezeichnung tty1 ist der Name einer Gerätedatei über die Ihr Terminal mit dem Rechner verbunden ist. Damit kommen wir zu einem weiteren wichtigen Grundprinzip des Linuxsystems:
"Alles ist eine Datei!"
Das bedeutet, dass der Linuxkernel alle Hardwarekomponenten als Datei abbildet. Diese Gerätedateien findet man zB. im Verzeichnis /dev (Devices). Dort findet man sehr viele Verzeichniseinträge, die der Kernel als Schnittstelle zwischen System und Hardware zur Verfügung stellt. Auch der Kernel selbst stellt seine Arbeit als Dateien zur Verfügung, die man unter /proc und /sys findet.
Das Terminal ist jetzt über die Gerätedatei /dev/tty1 erreichbar. Daten der Tastatur können jetzt aus der Gerätedatei /dev/tty1 gelesen werden und alles was in die Gerätedatei /dev/tty1 geschrieben wird, landet auf dem Bildschirm unseres Terminals. Der Zugriff auf das Terminal ist also mit Schreib- und Lesezugriffen in eine Datei vergleichbar. Das Terminal ist nun mit der interaktiven Shell (in meinem Fall eine bash) verbunden. Eingaben von der Tastatur werden von der Shell entgegengenommen indem sie aus dieser Gerätedatei gelesen werden und die Ausgaben werden wieder in die Gerätedatei zurückgeschrieben. Die Shell stellt uns nun drei Datenkanäle zur Verfügung, über die wir mit dem System kommunizieren. Der Datenkanal von der Gerätedatei (/dev/tty1, also unserer Terminaltastatur) ist stdin (Standardinputkanal Numer 0) und vom System zum Terminalbildschirm ist stdout (Standardoutputkanal Nummer 1). Die Shell stellt noch einen weiteren Ausgabekanal stderr (Standarderrorkanal Nummer 2) zur Verfügung, auf dem Fehlermeldungen ausgegeben werden. Dieser Kanal zeigt im Normalfall ebenfalls auf unseren Terminalbildschirm. Logischerweise sind stdin, stdout und stderr wieder Dateien und zwar /dev/stdin, /dev/stdout und /dev/stderr. Die Arbeit des Systems muss man sich nun so vorstellen:
Ein gestarteter Prozess (Programm, Konsolenkommando, Script etc) liest seine Daten immer von stdin (/dev/stdin) und gibt seine Ausgaben immer an stdout (/dev/stdout) aus. Fehlermeldungen werden nach stderr (/dev/stderr) geschrieben. Wo diese Daten herkommen und wo sie hinmüssen, ist dem Prozess vollkommen egal, denn wohin stdin, stdout und stderr zeigen, regelt die Shell. Sie hat diese drei (oder mehr) Datenkanäle mit den richtigen Gerätedateien zu verbinden.
Für unser einfaches "Hallo Welt"-Script bedeutet das nun folgendes
Bevor es zur eigentlichen Ausführung des Kommandos echo kommt, leistet die Shell eine ganze Menge Vorarbeit. Die Shell scannt die Zeile aus dem Script. Das Ende des Kommandos wird entweder am Zeilenendezeichen <newline> oder einem Semikolon erkannt. Das erste Wort (echo) wird als Kommandoname erkannt, der Rest sind Optionen und Parameter, die auch Sonderzeichen der Shell enthalten können. Zuerst werden also die Datenströme festgelegt. Danach führt die Shell verschiedene Expansionen durch, die die Parameter und Optionen noch vor der Ausführung des Kommandos verändern. Das Word-splitting trennt anhand verschiedener Trennzeichen (z.B. dem Leerzeichen) die Parameter voneinander, da diese einzeln an das Kommando als Parameter übergeben werden. Zum Schluss werden noch die Quotings aus den Teilen des Kommandos entfernt. Die Abarbeitung unserer Zeile sieht nun so aus:

  1. In der Zeile sind keine Umleitungen definiert, deshalb legt die bash standardmäßig die 3 Datenkanäle an. Der Standard-Eingabekanal(stdin) wird durch die shell mit der Gerätedatei unseres Terminals (Konsole) verbunden, z.B. /dev/tty1, wenn wir auf der ersten Textkonsole eingeloggt sind. Über diesen Datenkanal besteht damit eine Verbindung zur Tastatur. Der Standard-Ausgabekanal(stdout) und der Standard-Fehlerkanal(stderr) werden als Ausgabekanäle ebenfalls mit der Konsole verbunden.
  2. Da unsere einfache Programmzeile keine Sonderzeichen und Quotings enthält, erkennt die Shell nun an den Leerzeichen zwischen den drei Worten, dass es sich um das Kommando echo, gefolgt von den beiden Parametern Hallo und Welt handelt.
  3. Die Shell sucht nun in ihren Standardpfaden nach dem Kommando im System, echo ist ein internes Kommando der Shell. Wenn es gefunden wird und für uns ausführbar ist, startet die Shell das Kommando und übergibt die beiden Parameter.
  4. echo selbst übernimmt nun die Parameter und erkennt, das keine Optionen übergeben wurden. Damit gibt echo die einzelnen Parameter, durch ein Leerzeichen getrennt, an den Standardausgabekanal aus. Da dieser von der Shell mit unserem Terminal verbunden wurde, landet die Ausgabe auf dem Bildschirm.

Das Kommando liest die einzelnen Parameter also nicht selbst ein und es hat auch keinen direkten Zugang zu Tastatur und Bildschirm. Dafür ist die Shell zuständig.
Um das zu verdeutlichen, werden wir das Script nun ein wenig abändern. Dazu fügen wir zwischen Hallo und Welt einige weitere Leerzeichen ein, speichern ab und starten das Script neu.
#!/bin/bash
echo Hallo        Welt

bash-2.05b$ ./test
Hallo Welt
bash-2.05b$
Das Ergebnis zeigt keinen Unterschied. Das ist auch logisch, da echo ja nichts von diesen Leerzeichen weiss, sie dienen der Shell lediglich als Trennzeichen. Damit übergibt die Shell an echo wieder nur die beiden Parameter Hallo und Welt, die von echo mit einem Leerzeichen dazwischen, wieder nach Standardout zurückgegeben werden. Wenn ich nun möchte, dass die Leerzeichen mit angezeigt werden, muss ich diese vor der Shell verstecken, dazu dient das Quoting. Durch das Quoting kann ich der Shell mitteilen, dass es sich bei den Leerzeichen (oder anderen Sonderzeichen) um Teile des Parameters handelt und nicht um spezielle Anweisungen an die Shell. In unserem Fall also, dass die Leerzeichen in den Text gehören und nicht als Trennzeichen zwischen Parametern gemeint sind. Das Quoting hebt die Bedeutung von Sonderzeichen in der shell auf. Das Script könnte nun so aussehen:
#!/bin/bash
echo "Hallo        Welt"
oder so
#!/bin/bash
echo 'Hallo        Welt'
oder so
#!/bin/bash
echo Hallo\ \ \ \ \ Welt
Das Quoting mit den doppelten Anführungszeichen hebt die Bedeutung fast aller Sonderzeichen der Shell auf, dadurch erkennt die Shell nur einen einzigen Parameter hinter echo. Das Quoting mit einfachen Anführungszeichen hebt die Bedeutung aller Sonderzeichen außer ' auf. Im dritten Beispiel erreicht man den selben Effekt mit dem Quoting durch den backslash. Da dieser nur das jeweils folgende Zeichen versteckt, muss vor jedem Leerzeichen ein backslash eingefügt werden. Diese Quotungszeichen werden durch die Shell vor der Übergabe an das Kommando entfernt.
Und wenn ich nun die Anführungszeichen mit ausgeben möchte, was dann?
Da das Anführungszeichen ebenfalls ein Sonderzeichen ist, muss ich es also quoten(verstecken), damit es nicht selbst als Quotungszeichen erkannt wird. Das sieht im Script dann z.B. so aus.
#!/bin/bash
echo "\"Hallo       Welt\""
Der backslash hebt die Sonderbedeutung der beiden inneren Anführungszeichen auf, sie werden von der shell nicht als Quotungszeichen erkannt und werden im Parameter mit übergeben. Alle Quotungszeichen, die äußeren Anführungszeichen und der backslash werden wieder vor der Parameterübergabe entfernt.
Und wenn ich nun einen backslash mit ausgeben will, wie hebe ich seine Sonderbedeutung auf? Na klar, durch einen backslash, also
#!/bin/bash
echo Hallo\\Welt
Um genau verstehen zu können, wie und warum ein Script funktioniert oder auch nicht, sollte man seinen Kommandointerpreter wie z.B. die bash sehr gut kennen und die man-Page ausgiebig studieren.