Druckversion | ||||||||||||||||||||||||||||||||||||||||||||||||||
1979 lieferte AT&T UNIX Version 7 mit der nach ihrem Erfinder Stephen Bourne benannten Shell bsh aus, die als erste Standard-Shell für Unix-Systeme gilt. Bsh basierte auf der Programmiersprache Algol 68 und sollte die Aufgaben eines Administrators automatisieren. Neue Errungenschaften der Bsh waren vor allem die History, Aliasing und die Fähigkeit der Prozesskontrolle. So wie die verschiedenen Unix-Derivate aus dem Boden schossen, entstanden auch eine Vielzahl von Kommandozeileninterpretern mit zum einen unterschiedlichen Merkmalen und zum anderen unterschiedlichem Erfolg. Parallel zur Bsh wurde in Berkeley von Bill Joy die C-Shell - der Name lässt die nahe Verwandtschaft zur Programmiersprache C schon erahnen - entwickelt, die bald zum Lieferumfang eines jeden BSD-Unix avancierte. Gerade die Mächtigkeit der Programmierung verhalf dieser Shell zu einer großen Verbreitung. Unter Linux kommt heute oft die erweiterte Tenex-C-Shell (tcsh) zum Einsatz. Vielleicht war ja der umstrittene Disput zwischen Bsh- und Csh-Verfechtern der Anlass
für David Korn, 1986 mit System V Release 4 (AT&T) seine Version einer Shell
auszuliefern. Die Ksh beinhaltet alle Fähigkeiten der Bsh, ergänzt um
einige Möglichkeiten der Csh.
Die wichtigste Aufgabe einer Shell im interaktiven Modus ist die Interpretation der Kommandozeile. Eine Shell nimmt die Eingabe des Nutzers entgegen, teilt diese an den Whitespaces (Leerzeichen, Tabulator, Zeilenumbruch) in so genannte Token auf, ersetzt Aliasse und Funktionen, organisiert die Umleitung von Ein- und Ausgaben, substituiert Variablen und Kommandos und nutzt vorhandene Metazeichen zur Dateinamensgenerierung. Anschließend sucht die Shell nach dem angegebenen Kommando und übergibt diesem die komplette Kommandozeile als Argumente. Während der Initialisierung ist die Shell für die Umgebung des Nutzers verantwortlich. Dazu liest eine Shell spezielle, ihr zugeordnete Konfigurationsdateien ein und setzt z.B. Aliasse oder die PATH-Variable des Nutzers usw. Ebenso bietet jede Shell die Möglichkeit der Shellprogrammierung, um bestimmte Abläufe automatisieren zu helfen. Mächtige Shells beinhalten hierzu Programmkonstrukte wie bedingte Ausführung, Schleifen, Variablenzuweisung und Funktionen.
Das Prinzip des Parsens der Kommandozeile ist allen in diesem Kapitel besprochenen Shells gleich. Auf die konkreten Realisierungen kommen wir in den Abschnitten der speziellen Shells zurück.
Es existieren 4 Arten von Kommandos:
Die Nummerierung entspricht dabei der Reihenfolge, nach der eine Shell nach einem Kommando sucht. Ein Alias ist einfach nur ein anderer Name für ein Kommando oder eine Kommandofolge. Würden wir einen Alias mit dem Namen eines vorhandenen Kommandos definieren, so wäre das originale Kommando nur noch durch Angabe seines vollständigen Zugriffspfades erreichbar:
Da ein Alias wiederum einen Alias beinhalten kann, wiederholen die Shells die Substitution solange, bis der letzte Alias aufgelöst ist. Funktionen gruppieren Kommandos als eine separate Routine. Builtins sind Kommandos, die direkt in der Shell implementiert sind. Einige dieser Kommandos müssen zwingend Bestandteil der Shell sein, da sie z.B. globale Variablen manipulieren (cd setzt u.a. PWD und OLDPWD). Andere Kommandos werden aus Effizienzgründen innerhalb der Shell realisiert (ls). Ausführbare Programme liegen irgendwo auf der Festplatte. Zum Start eines Programms erzeugt die Shell einen neuen Prozess, alle anderen Kommandos werden innerhalb des Prozesses der Shell selbst ausgeführt.
Wer die ersten Kapitel nicht gerade übersprungen hat, dem sollte der Begriff des Prozesses schon geläufig sein. Hier noch einmal eine Zusammenfassung der Charakteristiken eines Prozesses: Ein Prozess ist ein Programm, das sich in Ausführung befindet, d.h.
Geben wir nun auf der Kommandozeile etwas ein, ist es Aufgabe der Shell, das Kommando zu finden und, insofern es sich nicht um ein builtin-Kommando handelt, einen neuen Prozess zu erzeugen, welcher dann das Programm ausführt. Die Shell bedient sich dabei verschiedener Systemrufe, d.h. Aufrufe von Funktionen des Kernels. Die uns nachfolgend interessierenden Systemrufe sind:
Der Systemruf fork()Abbildung 1: fork() - Erzeugen eines neuen Prozesses Unter Unix ist fork() die einzige Möglichkeit, einen neuen Prozess zu
erzeugen (Threads - so genannte Leichtgewichtsprozesse - haben nicht viel mit dem
klassischen Prozesskonzept gemeinsam; für sie existieren eigene Systemrufe). Beide Prozesse führen zunächst ein und dasselbe Programm aus und teilen sich vorerst diesen Speicherbereich. Erst wenn der Kindprozess ein neues Programm laden sollte, wird ihm ein eigenes Codesegment zugestanden. Beispiel:
Wir übersetzen das Programm und starten es:
Erläuterung: Anhand der Ausgabe ist zu erkennen, dass der Kindprozess tatsächlich den ersten Befehl nach dem "fork()"-Aufruf ausführt, d.h. auch der Programmzeiger wurde vererbt. In der nachfolgenden Schleife gibt jeder Prozess seine PID aus. Die Aufrufe von "sleep()" und "fflush()" dienen nur der Demonstration, ohne diese würde vermutlich jeder Prozess innerhalb eines CPU-Zyklus sein Programm abarbeiten können und die Ausgaben wären nicht alternierend. Insofern der Elternprozess nicht explizit auf die Beendigung seiner Kinder wartet (siehe "wait") , laufen die Prozesse parallel ab. Der Systemruf exec()Abbildung 2: exec() - Laden eines neuen Programms Das Starten von Prozessen, die alle ein und dasselbe Programm ausführen, ist auf
die Dauer nicht sehr produktiv...
Beispiel: In einem C-Programm wird ein "exec()"-Aufruf wie folgt vollzogen:
Wiederum übersetzen wir das Programm und starten es:
Erläuterung: Das Programm demonstriert das Beschreiten alternativer Programmpfade. Für den Elternprozess ist "pid" verschieden von "0", so wird der erste Teil des "if"-Konstrukts betreten. Im Falle des Kindprozesses liefert "pid" "0", so dass mit dem "else"-Zweig fortgefahren wird. Nach einer Ausgabe wird das Kind das aktuelle Programm durch das Programm "date" ersetzen. Der Systemruf "exec()" existiert nicht selbst als C-Funktion, sondern ist in verschiedenen Varianten implementiert, eine davon ist "execl()". Der Kindprozess hat nun nichts mehr mit dem alten Programm zu tun, deshalb erscheint von ihm auch keine "Programmende"-Ausgabe. Der Systemruf exit()Ein Prozess beendet seine Existenz spätestens, wenn das ausgeführte Programm abgearbeitet ist. Innerhalb eines Programms kann dieses an beliebiger Stelle mit dem Systemruf exit() verlassen werden. Ein Prozess sendet vor seinem Ende noch ein Signal "SIGCHILD" an seinen Elternprozess und wartet, dass dieser das Signal behandelt. Gleichzeitig sollte jedes Programm einen Rückgabewert ("Status") liefern, der den Erfolg (Wert "0") oder einen möglichen Fehlercode (Wert "1-255") meldet. Der Status des letzten Kommandos kann in einer Shell abgefragt werden:
Der Systemruf wait()Eine Shell wird, während ein Kindprozess seine Arbeit erledigt, auf dessen Rückkehr warten. Dazu ruft die Shell (und die meisten Programme, die weitere Prozesse erzeugen, verfahren so) den Systemruf wait() auf und geht in den schlafenden Zustand, aus dem sie erst erwacht, wenn der Kindprozess terminiert.
Hat der Elternprozess das Signal des Kindes behandelt, gilt dieser Prozess als beendet, d.h. sein gesamter Speicherbereich wurde freigegeben und sein Eintrag aus der Prozesstabelle des Kernels entfernt. Beispiel:
Wir übersetzen das Programm und starten es:
Erläuterung: Nach der ersten Ausgabe durch den Elternprozess wartet dieser mittels "wait()", bis sein Kindprozess seine Arbeit beendet hat. Deshalb erscheinen die Ausgaben der PID des Elternprozesses erst nach denen des Kindes. Ein Elternprozess muss nicht auf die Terminierung seiner Kindprozesse warten. Sendet in diesem Fall ein Kind das Signal "SIGCHILD", dann befindet es sich aus Sicht des Systems im Zustand Zombie. Der Prozess ist zwar beendet, aber sein Signal wurde noch nicht behandelt und der Eintrag in der Prozesstabelle existiert weiterhin. Es soll auch vorkommen, dass die Eltern das Zeitliche vor ihren Nachfahren segnen (z.B. durch expliziten Abbruch durch den Nutzer oder durch einen Programmfehler). In einem solchen Fall spricht man vom Kind von einem Waisen. In dieser Situation übernimmt init - der Vorfahre aller Prozesse - die Rolle des Elternprozesses.
Es gibt eine Reihe von Möglichkeiten, wie Prozesse untereinander Nachrichten austauschen können. Gebräuchliche Mechanismen unter Unix-Systemen sind die Interprozesskommunikationen "IPC" nach SystemV und einige BSD-Entwicklungen. Dazu zählen:
Von den erwähnten Mechanismen stellen die Shells zur Kommunikation der in ihnen gestarteten Prozesse Pipes (benannt/unbenannt) und Signale zur Verfügung. PipesEine Pipe verbindet die Ausgabe eines Kommandos mit der Eingabe eines anderen Kommando. D.h. das erste Kommando schreibt seine Ausgaben anstatt auf die Standardausgabe in die Pipe, während das zweite Kommando aus dieser Pipe und nicht von der Standardeingabe liest. Beliebig viele Kommandos lassen sich durch Pipes miteinander verknüpfen. Die Shell kümmert sich dabei um die Synchronisation, so dass ein aus der Pipe lesendes Kommando tatsächlich erst zum Zuge kommt, nachdem ein schreibendes Kommando den Puffer der Pipe gefüllt hat. Nicht alle Kommandos akzeptieren ihre Eingaben aus einer Pipe. Kommandos, die es tun, bezeichnet man deshalb auch als Filter. Alle uns interessierenden Shells stellen als Symbol für die Pipe den Strich "|" zur Verfügung:
Eine durch das Zeichen "|" realisierte Pipe bezeichnet man als unbenannte Pipe.
Die Shell bedient sich des pipe()-Systemrufes und erhält vom Betriebssystem
einen Deskriptor auf die Pipe. Gleichzeitig stellt der Kernel einen Speicherbereich
bereit, den sich die durch die Pipe verbundenen Prozesse teilen. Sind alle Pipes der
Kommandozeile erzeugt, startet die Shell für jedes Kommando einen Kindprozess und
setzt deren Dateideskriptoren auf die entsprechenden Pipedeskriptoren.
Mit den Mechanismen der Ein- und Ausgabeumleitung kann ein Prozess in eine solche Datei schreiben, während ein anderer aus ihr liest. Mittels "Named Pipes" können auch Prozesse kommunizieren, die nicht unmittelbare Nachfahren der Shell sind. SignaleJedes Signal ist mit einer bestimmten Reaktion verbunden. Es liegt in der Verantwortung des Entwicklers, ob ein Programm auf ein Signal mit den "üblichen" Aktionen antwortet oder ob es dieses Signal sogar ignoriert. Das Signal "KILL" (Signalnummer 9) beendet einen Prozess, ohne dass dieses vom ausgeführten Programm abgefangen werden kann. Wichtige Signale und ihre Wirkung sind (die vollständige Liste der Signale ist in der Datei "/usr/include/signal.h" zu finden): SIGHUP (1) Terminal-Hangup, bei Dämonen verwendet, um ein erneutes Einlesen der Konfigurationsdateien zu erzwingen SIGINT (2) Tastatur-Interrupt SIGQUIT (3) Ende von der Tastatur SIGILL(4) Illegaler Befehl SIGAbrT (5) Abbruch-Signal von abort(3) SIGFPE (8) Fließkommafehler (z.B. Division durch Null) SIGKILL (9) Unbedingte Beendigung eines Prozesses SIGSEGV (11) Speicherzugrifffehler SIGPIPE (13) Schreiben in eine Pipe, ohne dass ein Prozess daraus liest SIGTERM (15) Prozess soll sich beenden (default von kill) SIGCHLD (17) Ende eines Kindprozesses SIGCONT (18) Gestoppter Prozess wird fortgesetzt SIGSTOP (19) Der Prozess wird gestoppt SIGTSTP (20) Ausgabe wurde angehalten SIGUSR1 (30) Nutzerdefiniertes Signal Die Shells beinhalten das Kommando kill, um Signale "von außen" an Prozesse versenden zu können. Der Aufruf lautet:
Die "Signalnummer" kann dabei als numerischer Wert oder durch den symbolischen Namen ("TERM", "HUP, "KILL", ...) angegeben werden.
Das Versenden einiger Signale unterstützen die Shells durch Tastenkombinationen. Während die Csh nur das [Ctrl]-[C] akzeptiert, existieren unter der Bsh, Ksh und Tcsh mindestens folgende Shortcuts: [Ctrl]+[C] SIGINT - Ausführung des aktiven Prozesses abbrechen [Ctrl]+[Z] SIGSTOP, stoppt die Ausführung des aktiven Prozesses
Nach einem erfolgreichen Anmelden ins System steht dem Nutzer i.d.R. eine komplette Arbeitsumgebung zur Verfügung. Unter anderem sind wichtige Shellvariablen (PATH, USER, DISPLAY, ...) vorbelegt, einige Aliasse sind vorhanden, bestimmte Programme wurden bereits gestartet... Die Startup-Dateien der Bash und KshFür diese initiale Umgebung sind verschiedene Konfigurations-Dateien der Shells verantwortlich, die teils bei jedem Start einer Shell, teils nur beim Start einer Login-Shell abgearbeitet werden. Die Bash und die Ksh führen während des ersten Starts die Kommandos der Datei /etc/profile aus. Ein Ausschnitt dieser, in dem einige Shellvariablen vorbelegt und Aliasse gesetzt werden, sei kurz aufgelistet:
Zum jetzigen Zeitpunkt sollte den Leser nur die Tatsache interessieren, dass die wichtigen Variablen beim Start einer Shell vorbelegt werden. Die Erläuterung der verwendeten Syntax bleibt den Abschnitten zur Programmierung der Shells vorbehalten. Im Falle der Bash folgt die Abarbeitung einer Reihe weiterer Dateien, sofern diese im Heimatverzeichnis des jeweiligen Nutzers existieren. Die Dateien sind: ~/.bash_profile, ~/.bash_login und ~/.profile. Alle drei Dateien können vom Besitzer, also dem Nutzer selbst, bearbeitet werden und ermöglichen eine individuelle Einrichtung der Umgebung. Die Ksh betrachtet neben der schon erwähnten /etc/profile nur die Datei ~/.profile. Wird die Bash nicht als Loginshell gestartet (z.B. als Subshell zum Ausführen eines Skripts), dann bearbeitet sie nur die Datei ~/.bashrc. Die Ksh liest keine Konfigurationsdatei ein. Die Startup-Dateien der TcshDie Tcsh verwendet eine grundlegend andere Programmiersprache. Demzufolge benötigt sie auch andere Konfigurationsdateien. Die Dateien /etc/csh.cshrc und /etc/csh.login übernehmen dabei die Rolle der /etc/profile von Bash und Ksh. Die Ausschnitte aus diesen, die im wesentlichen dieselben Initialisierungen veranlassen, die weiter oben im Beispiel zur /etc/profile dargestellt wurden, seien hier angeführt:
Weitere Dateien, die während des Starts einer (T)csh betrachtet werden, liegen im Heimatverzeichnis und sind: ~/.tcshrc oder, falls erstere Datei nicht existiert, ~/.cshrc, ~/.history, ~/.login und ~/.cshdirs. Die Reihenfolge der Auswertung der Dateien hängt von der konkreten Implementierung ab. VererbungVererbung im Sinne der Prozessentstehung wurde im Punkt Shell und Prozesse besprochen. Für den Nutzer ist insbesondere der Geltungsbereich von Variablen von Bedeutung, die alle Shells verwenden, um das Verhalten der in ihnen gestarteten Programme zu steuern. Die Shells kennen lokale und globale Variablen. Eine lokale Shellvariable ist dabei nur innerhalb der Shell ihrer Definition sichtbar und wird nicht an die in der Shell gestarteten Programme weiter gereicht. Globale Variablen hingegen sind ab der Shell ihrer Einführung sichtbar, also auch in allen aus dieser Shell gestarteten Programmen. Zur Definition von Variablen existiert in allen Shells das builtin-Kommando set:
In der Bash und Ksh kann auf das Kommando set verzichtet werden, da beide Shells anhand des Gleichheitszeichens eine Zuweisung erkennen. Allerdings dürfen im Unterschied zur (T)csh bei beiden keine Leerzeichen vor und nach dem Gleichheitszeichen stehen. Dass eine solche Variable tatsächlich nur in der aktuellen Shell sichtbar ist, lässt sich leicht überprüfen:
Im Beispiel wurde eine Subshell (die bash) gestartet, in dieser ist die Variable "local_x" nicht bekannt (leere Zeile). Soll eine Variable nun auch in den anderen Shells sichtbar sein, muss sie exportiert werden:
Wir testen die Existenz der Variable in einer Subshell:
Alle Ein- und Ausgaben werden vom Kernel über den Mechanismus der File-Deskriptoren behandelt. So ein Deskriptor ist eine kleine nichtnegative Zahl (unsigned Integer), die einen Index auf eine vom Kernel verwaltete Tabelle ist. Jeder offene E/A-Kanal und jede offene Datei (named Pipe, Socket) wird nur über einen solchen Eintrag angesprochen. Jeder Prozess erbt nun seine eigene Deskriptortabelle von seinem Vorfahren. Üblicherweise sind die drei ersten Einträge der Tabelle (0, 1 und 2) mit dem Terminal verbunden und werden mit Standardeingabe (0), Standardausgabe (1) und Standardfehlerausgabe (2) bezeichnet. Öffnet ein Prozess nun eine Datei (oder eine named Pipe oder einen Socket), so wird in der Tabelle nach dem nächsten freien Index gesucht. Und dieser wird der Deskriptor für die neue Datei. Die Größe der Deskriptor-Tabelle ist beschränkt, so dass nur eine bestimmte Anzahl Dateien gleichzeitig geöffnet werden können (Vergleiche Limits unter Linux). Man spricht von E/A-Umleitung, sobald ein Dateideskriptor nicht mit einem der Standardkanäle 0, 1 oder 2 verbunden ist. Shells realisieren die Umleitung, indem zunächst der offene Deskriptor geschlossen wird und die anschließend geöffnete Datei diesen Deskriptor zugewiesen bekommt. Standardein- und Standardausgabe lassen sich in allen drei Shells analog manipulieren:
Die Shells lösen solche Umleitungen von links nach rechts auf. Im Beispiel wird also zunächst der Deskriptor der Standardeingabe geschlossen (symbolisiert durch "<"), anschließend wird die Datei "infile" geöffnet. Sie erhält den ersten freien Index zugewiesen und dieser ist nun die "0". Im nächsten Schritt wird die Standardausgabe geschlossen (Symbol ">"). Die nun zu öffnende Datei "outfile" bekommt den Deskriptor "1" zugewiesen. Das Kommando "cat" bezieht also seine Eingaben aus "infile" und schreibt das Ergebnis nach "outfile". Die Umleitung der Standardfehlerausgabe wird bei der (T)csh etwas abweichend vom Vorgehen bei Bash und Ksh gehandhabt. Der Grund ist, dass man bei Bash und Ksh das Umleitungssymbol mit einem konkreten Deskriptor assoziieren kann (z.B. bezeichnet "2>" die Standardfehlerausgabe), die (T)csh aber keine solche Bezeichnung für die Standardfehlerausgabe kennt. Das obige Beispiel hätte man in Bash und Ksh auch folgendermaßen ausdrücken können:
Und analog zu diesem Schema lässt sich gezielt die Standardfehlerausgabe umleiten:
Die (T)csh kennt, wie auch Bash und Ksh, das Symbol >&, womit gleichzeitig Standard- und Standardfehlerausgabe in eine Datei umgeleitet werden können. Der Umweg, den man zur Umleitung der Fehler in der (T)csh beschreiten muss, geht über die Verwendung einer Subshell, in der nun die normalen Ausgaben abgefangen werden, und das, was übrig bleibt (Fehler), wird außerhalb der Subshell behandelt.
Sie als Leser finden das verwirrend? Dann wollen wir Ihnen einen Vorgeschmack geben, welche Feinheiten der Ein- und Ausgabeumleitung die kommenden Kapitel für Sie bereit halten. Hier einige Beispiele für die Bash und Ksh:
Immer wieder wird man auf Probleme treffen, bei denen man sich fragt, warum es kein Programm gibt, das dieses löst? Entweder weil es zu komplex ist, ein solches zu schreiben und noch niemand solch starken Bedarf verspürte, dass er sich selbst dem Brocken widmete. Oder weil es so einfach ist, dass es sich quasi jeder selbst zusammenbasteln könnte. Den umfangreichen Problematiken wird man nur schwerlich oder gar nicht mit Mitteln der Shellprogrammierung beikommen können, aber gerade wer reichlich auf der Kommandozeile hantiert, wird Problemstellungen begegnen, die immer und immer wiederkehren. Warum jedes Mal mühsam die Gedanken ordnen, wenn es einzig der Erinnerung eines Programmnamens bedarf, welches das Schema automatisiert? In den nächsten Kapiteln werden Sie genügend Gelegenheit haben, der einem Neuling verwirrenden Syntax der Shellskripte Herr zu werden. Sie werden die unterschiedlichen Möglichkeiten, die die Shells bieten, bewerten und sich letztlich auf die Anwendung nur einer Shellprogrammiersprache festlegen. Welche das sein wird, ist Geschmackssache und wir hoffen mit der Reihenfolge unserer Darlegung (bash, tcsh, ksh), keine vorschnellen Wertungen beim Leser hervorzurufen. Jede der Sprachen hat Stärken und Schwächen... Zu diesem Zeitpunkt möchten wir nur die Realisierung eines kleinen Programms mit den drei Shellsprachen gegenüber stellen, ohne irgend welche Wertungen einfließen zu lassen. Voran gestellt sei die Idee, die zum Schreiben des Skripts animierte. Die Aufgabe. Die vorliegende Linuxfibel entsteht auf Basis von html-Dateien. Wir wirken zu dritt an der Realisierung, teils testen und schreiben wir in der Firma, teils daheim. Um die Dateien zu übertragen, sammeln wir sie in einem Archiv. Nun geschieht es immer wieder, dass ein schon "fertiger" Abschnitt oder eine Grafik doch wieder verworfen wird, so löschen wir nicht gleich die alte Datei, sondern nennen sie um und arbeiten auf einer Kopie weiter. Erzeugten wir nun ein neues Archiv, enthielt es die alten und die neuen Dateien und war damit viel größer, als eigentlich notwendig. In dem Wirrwarr von ca. 300 Dateien den Überblick zu wahren, war vergebene Müh', also kam die Idee zu einem Skript, das Folgendes realisieren sollte:
Die Realisierung in der Bash und der KshBash und Ksh unterscheiden sich im Sinne der Programmierung nur unwesentlich, in unserem Beispiel überhaupt nicht. Um das Skript von der Korn Shell ausführen zu lassen, müssen Sie nur die erste Zeile von "#!/bin/sh" auf "#!/bin/ksh" ändern.
Die Realisierung in der TcshEine gänzlich andere, an die Programmiersprache C angelehnte Syntax verwendet die Tcsh. Beachten Sie bitte den Hinweis im Skript, dass bestimmte Zeilen in der Programmdatei zusammengefügt werden müssen. Die Tcsh kennt kein Zeilenfortsetzungszeichen, wie die Bash und Ksh mit dem Backslash.
Das Skript ausführenFalls Sie eines der Skripte in einer Datei gespeichert haben, so müssen Sie dieses noch mit den Ausführungsrechten versehen:
Sollten Sie dieses vergessen, wird eine Shell mit der Ausschrift "command not found" Ihr Anliegen abweisen. |
||||||||||||||||||||||||||||||||||||||||||||||||||
Korrekturen, Hinweise? |