*** BSH PROGRAMMING CONSIDERED HARMFUL *** Seit einiger Zeit hat die Newsgroup de.comp.os.unix.shell mit Postings eines gewissen Helmut Schellong zu tun, in denen er sein Programm »bsh« ungebeten anpreist. Da sich herausstellte, daß dieses Programm nach Ansicht vieler Teilnehmer fragwürdige, aber laut Autor gewollte Eigenheiten hat, habe ich sie hier zusammengestellt. ------------------------------------------------------------------------------ 1. »Bsh« hat eine proprietäre Syntax Seit dem Ende der siebziger Jahre ist die Bourne-Shell die dominierende Unix-Shell. Sie bietet eine für einen Kommandoprozessor sehr ausgefeilte und mächtige Sprache. Anfang der neunziger Jahre wurde sie zusammen mit mehreren Erweiterungen der Korn-Shell in POSIX.2 standardisiert; SUSv2 als der Unix-Marke zugrundeliegende Spezifikation hat diese Definition übernommen. Nahezu alle Shell-Skripte auf Unix-Systemen sind in dieser derart historisch gewachsenen Sprache geschrieben; Erweiterungen sind in etablierten Shells wie der Korn-Shell oder der Bourne-Again-Shell stets so gehalten, daß Kompatibilität und Konformität davon nicht berührt werden. Daher ist es ohne großen Aufwand möglich, Skripte so zu programmieren, daß sie mit allen Shells dieser Gruppe lauffähig sind. Der Autor der »bsh« behauptet, seine Shell verfüge über eine ähnliche Syntax. Bei oberflächlicher Betrachtung mag man das hinnehmen, es werden z.B. dieselben Namen für Kontrollkonstrukte (for - do - done, if - then - fi) und Befehle (break, expr) verwendet. Sieht man jedoch genauer hin, erschließen sich gravierende Unterschiede, zumal man unter einer Syntax eben nicht nur die Bezeichner, sondern auch deren kontextuale Einbindung zu verstehen hat. So haben viele Kommandos zwar denselben Namen, aber andere Optionen oder sonstige Änderungen. »expr« z.B. wird unter Unix vornehmlich für simple arithmetische Operationen genutzt. Das expr-Kommando der »bsh« kennt sie nicht, stattdessen aber eine Reihe von Erweiterungen im Umfeld regulärer Ausdrücke. Hier weist das Unix-expr lediglich den Operator »:« auf, der einen String mit einem regulären Ausdruck vergleicht. Letzterer ist auf den Anfang des Strings fixiert, auch das ist bei »bsh« nicht der Fall. Damit akzeptiert ein gleichnamiges Kommando nur einen einzigen gleichartigen Operator, und das mit einer Semantik, die verschieden genug ist, um zu anderen Ergebnissen zu führen, aber doch ähnlich genug, um Verwechslungen zu begünstigen. Im Widerspruch zur allgemeinen Praxis auf Unix-Systemen wird von »bsh« per Voreinstellung nicht der Backslash »\«, sondern das Prozentzeichen »%« zum Quoten einzelner Zeichen benutzt. Das heißt, daß zunächst fast jeder String einschließlich regulärer Ausdrücke auf diese Zeichen untersucht und ggf. geändert werden muß; ferner wird »%« auf Unix-Systemen an so vielen Stellen verwendet, daß es zu Problemen mit dem echo-Kommando der »bsh« kommen kann, das dieses Zeichen zum Einleiten von Escapesequenzen nutzt. Die Kontrollkonstrukte der »bsh« weisen gegenüber denen der Unix-Shell funktionale Beschränkungen auf. Insbesondere ist es nicht möglich, die Ausgabe einer Schleife while get-some-input do process-one-line done | postprocess oder einer Kommandogruppe gemeinsam unzulenken: { echo foo; echo bar; } > output-file Schleifen können nicht wie gewohnt asynchron ausgeführt werden, das »&«-Zeichen bleibt in diesem Fall schlicht wirkungslos: while some-condition do background-processing done & Operationen, für die die Shell gemeinhin eingesetzt wird, lassen sich also mit »bsh« nicht oder nur über Umwege durchführen. Aber selbst wenn die Funktionalität im Prinzip vorhanden ist, ergeben sich Probleme, denn gängige Konstrukte der Unix-Shell werden von »bsh« nicht akzeptiert. So dürfen Funktionsdefinitionen nicht allein mit »;« enden: foo() { bar; }; foo Das Dollarzeichen darf nicht unmaskiert am Ende eines Wortes stehen: grep foo$ bar Manche Konstrukte werden mit anderer Semantik belegt. Besteht ein Kommando in der Unix-Shell lediglich aus einer Umleitung, so wird die Umleitung temporär vollzogen; die Dateideskriptoren der aufrufenden Shell werden dadurch nicht beeinflußt. Mit dem Kommando > foo wird also eine Datei dieses Namens erzeugt bzw. der Inhalt einer existierenden Datei gelöscht. Bei »bsh« wird hingegen die Ausgabe der gesamten Shell umgelenkt, dies entspräche exec > foo in der Unix-Shell. Die Operatoren »&&« und »||« werden anders evaluiert, die Unix-Shell führt Und bzw. Oder alternativ aus, true || echo foo && echo bar so daß »bar« ausgegeben wird. Die »bsh« bricht die Ausführung der gesamten Liste hingegen bereits nach dem ersten »||« ab. Wenn eine Einflußnahme auf Variablen, Arbeitsverzeichnis, offene Dateien usw. der ausführenden Shell nicht erwünscht ist, kann der Programmierer einen Skriptabschnitt in einer bis auf die per »trap« gesetzten Handler exakten Kopie der Shell (Subshell) ausführen, indem er ihn in Klammern ( ... ) setzt. Auch »bsh« bietet ein solches Konstrukt, jedoch sind Syntax wie Semantik verschieden. So ist es nicht möglich, innerhalb von Subshell-Listen konsistent zu quoten: ( egrep 'foo|bar$' ) müßte mit »bsh« ( egrep foo%|bar%%%$ ) oder ( egrep 'foo|bar%$' ) geschrieben werden, um die Auswertung als Pipezeichen bzw. Variable zu verhindern. Die Subshell der »bsh« ist keine exakte Kopie ihres Vorgängers, denn der Einsatz ihrer Shell-Funktionen ist darin nicht möglich: foo() { bar; } ( foo ) schlägt also fehl. Die Erkennung von Schlüsselworten ist in der Unix-Shell unterbunden, wenn sie mit Quotingzeichen versehen sind oder der entsprechende String aus einer Variablensubstitution entstanden ist. Nicht so in »bsh«: # "if" true; then echo foo; fi foo # foo=if; $foo true; then echo bar; fi bar In der Unix-Shell läßt sich das, falls gewünscht, mit »eval« erreichen; das Verhalten der »bsh« kann hingegen dazu führen, daß eine Stelle in einem Skript abhängig von externen Einflüssen als Syntaxfehler gewertet wird. Der Programmierer kann sich also nicht darauf verlassen, daß ein Skript im tatsächlichen Einsatz auch nur syntaktisch korrekt ist. Die Verwendung der »bsh« wird von arbiträren Limits eingeschränkt, so gibt es eine feste Obergrenze für Länge und Anzahl von Variablen wie Funktionen. Selbst bei internen Kommandos sind sowohl die Zahl als auch der gesamte Zeichenumfang der Argumente begrenzt. Das steht im Kontrast zur Unix-Shell, die seit ihren ersten Versionen vor über zwanzig Jahren so geschickt programmiert ist, daß der vom System bereitgestellte Arbeitsspeicher flexibel genutzt werden kann. Die regulären Ausdrücke in den Kommandos der »bsh« sind ebenfalls proprietär. So gibt es »%A« für Buchstaben, »%U« für Großbuchstaben usw. POSIX.2 schreibt dafür nicht nur eine andere Syntax ([:alpha:], [:upper:]) vor, sondern insbesondere auch, daß Umlaute und ähnliche sprachgebundene Zeichen abhängig von der Locale-Einstellung des Systems einbezogen werden; in »bsh« fehlt diese Eigenschaft. Folglich ist es unmöglich, mit »bsh« ein nicht vollkommen triviales Unix-Shellskript auszuführen, ohne in größerem Rahmen Anpassungen vorzunehmen. Da ein »bsh«-Skript auf den ersten Blick jedoch nahezu identisch wirkt, besteht die große Gefahr, Konstrukte zu verwechseln bzw. die subtilen Änderungen zu übersehen. Wer sowohl Unix-Shells als auch »bsh« einsetzt, wird ständig durch falsche Freunde irritiert; »bsh« wird insbesondere die, die in ihren Erfahrungen auf Unix-Systemen noch nicht völlig gefestigt sind, zu ständigen Fehlern verleiten. ------------------------------------------------------------------------------ 2. Der Parser der »bsh« ist unzuverlässig Die ohnehin vom Standard abweichende Syntax der »bsh« wird vom Parser des Programmes nicht immer sauber behandelt. Das zeigt sich bereits an den häufig irreführenden Fehlermeldungen, wie # if { then; }; fi bsh: Syntaxfehler: 'vermisse ;} ' # echo $/ bsh: Parametername zu lang: '' Der Umgang mit der Syntax wird dadurch noch zusätzlich erschwert. Fehlerhafte Konstrukte werden mitunter ohne jede Fehlermeldung oder Warnung ausgeführt: { echo bar && } while true && do break | done while true; echo | do; false && done Somit ist »bsh« offenbar nicht in der Lage, korrekte und falsche Syntax einwandfrei zu trennen. Ein Fehler des Programmierers kann also leicht dazu führen, daß ungültige Programmabschnitte zur Ausführung gelangen. ------------------------------------------------------------------------------ 3. »Bsh« bietet Pipes, die keine sind Eines der wichtigsten Mittel der Shellprogrammierung ist die Pipe, ein Kommunikationskanal, der sich an beiden Enden als Datei präsentiert. Was in das eine Ende der Pipe geschrieben wird, wird am anderen Ende gelesen, wobei das Lesen und u.U. auch das Schreiben blockiert wird, bis die entsprechende Aktion auf der anderen Seite stattfindet. Eine Pipe wird laut POSIX-Standards mit dem Systemaufruf pipe() erstellt, sie kann in der Shell mittels »write_cmd | read_cmd« zum Einsatz kommen. Das syntaktische Konstrukt heißt »Pipeline«. Die »bsh« belegt dieselbe Syntax mit einer anderen Semantik. Statt der Pipe werden reguläre Dateien verwendet, deren Inhalt von einem Programm an das nächste weitergegeben wird; das entspricht in etwa write_cmd > /tmp/$$; read_cmd < /tmp/$$ in der Unix-Shell. Da Zugriffe auf eine solche Datei nicht blockieren, muß »bsh« mit dem Starten jeden Gliedes der Pipeline so lange warten, bis der vorhergehende Prozeß beendet ist. Das führt zunächst dazu, daß der Vorgang durch den Verlust der Parallelität langsamer abläuft; ferner müssen die Daten, die durch eine echte Pipe transparent laufen, hier im Dateisystem abgelegt werden, was leicht zu Platzproblemen führen kann. Anders als bei einer echten Pipe muß zudem jeder Prozeß von selbst terminieren, damit es nicht zu einer Blockadesituation kommt. Das heißt, daß eine Befehlsfolge wie tail -f /var/log/messages | egrep 'warning|error|fatal|panic' zum Überwachen einer Logdatei mit der »bsh« nicht verwendbar ist. Viele Unix-Utilities prüfen, ob ihre Ein- oder Ausgabe mit einer Pipe verbunden ist und passen sich daran an. Das ist erwünscht, da sich eine Pipe in Details anders verhält als eine reguläre Datei. So ist es nicht möglich, die Position des nächsten Zugriffs zu ändern; Daten werden beim Schreiben ans Ende angehängt und stets von vorne gelesen. Zudem sind Schreibzugriffe in eine Pipe bis zu einer gewissen Größe »atomic«, anders als bei Zugriffen auf reguläre Dateien wird vom System also garantiert, daß eine bestimmte Datenmenge in einem Zug geschrieben wird. Das ist z.B. relevant, wenn mehrere Prozesse zeilenweise in eine Pipe schreiben, weil so gesichert werden kann, daß eine einzelne Zeile immer aus einer einzigen Quelle stammt. Wenn eine Pipe aus irgendwelchen Gründen nicht in Frage kommt, kann man in der Unix-Shell von Hand reguläre Dateien anlegen; es liegt im Ermessen des Programmierers, welches Verfahren er wählt. Insofern sind die Pseudopipes in der »bsh« ein minderwertiges Verfahren, das ausschließlich Funktionalität raubt und keinerlei Vorteile bringt. ------------------------------------------------------------------------------ 4. »Bsh« ist keine freie Software Obwohl es eine kostenlose Version der »bsh« gibt, dient sie letztlich als Anreiz zum Kauf einer »Vollversion«, wie man an einer Reihe von künstlichen Limitierungen erkennen kann. Außerdem ist der Quellcode zur »bsh«, anders als bei mittlerweile allen etablierten Unix-Shells, nicht verfügbar. Daher ist es unmöglich, selbst Bugs zu fixen oder Features hinzuzufügen. Ferner wird verhindert, daß ein Nutzer »bsh« auf ein System portiert, für das das Programm vom Hersteller nicht zur Verfügung gestellt wird. Insbesondere aber kann man sich von der Sicherheit der Software nicht ohne Quelleneinblick überzeugen. Wer »bsh« ernsthaft einsetzen wollte, wäre damit hochgradig vom Autor der Software abhängig. ------------------------------------------------------------------------------ 5. Der Verwendungsbereich der »bsh« ist stark eingeschränkt Aufgrund der proprietären Syntax ist ein »bsh«-Skript letztlich nur lauffähig, wenn eine »bsh« zur Verfügung steht. Dies ist aber selten der Fall, den »bsh« wird von keinem Hersteller eines Unix-artigen Systems mitgeliefert. Sie ist zudem nur für wenige Systemumgebungen überhaupt verfügbar, Versionen für Unix-Systeme auf RISC-Rechnern fehlen fast völlig. Eine weitere Einschränkung ist, daß Fehlermeldungen und Dokumentation den Einsatz effektiv auf den deutschen Sprachraum festlegen. Wer Skripte für »bsh« schreibt, muß sich also darüber im klaren sein, daß deren Verbreitung über das persönliche Umfeld hinaus praktisch nicht herzustellen ist. ------------------------------------------------------------------------------ Fazit Die proprietären Eigenheiten der »bsh« sind im Vergleich zur Unix-Shell nicht nur funktional minderwertig; seine Fehler und die ständige Gefahr, die Konstrukte mit gängiger Syntax zu verwechseln, machen dieses Programm zu einem Risiko für das ganze System. Dank der weiten und überwiegend freien Verfügbarkeit der etablierten Unix-Shells ebenso wie flexibler und sicherer Skriptsprachen wie Perl ist es unnötig, sich auf dieses Abenteuer einzulassen. Von einer Verwendung der »bsh« ist deshalb dringend abzuraten. Abschließend sei gesagt, daß diese Aspekte in de.comp.os.unix.shell in gebotenem Umfange diskutiert worden sind. Detailinformationen lassen sich also mühelos über einholen. Viele Teilnehmer der Newsgroup haben ihren Unmut darüber geäußert, daß »bsh« entgegen ihrer nicht vorhandenen Verbreitung auf Unix-Systemen immer wieder Gegenstand von Diskussionen wird; es empfiehlt sich also, die Netiquette bei diesem Thema besonders sorgfältig zu beachten und sich nur dann über »bsh« zu äußern, wenn es etwas wirklich Neues mitzuteilen gibt. ------------------------------------------------------------------------------ In diesen Text flossen Beobachtungen von Jürgen Ilse, Jürgen P. Meier und Matthias Andree ein. Das Konzept lehnt sich an »Csh Programming Considered Harmful« von Tom Christiansen an. Jürgen Ilse stellt unter eine aktuelle HTML-Fassung dieses Dokumentes zur Verfügung. Gunnar Ritter Version 1.4 01/08/24