Schnell geschrieben sind Shellskripte oft eine Quelle mangelnder Robustheit, gefährlicher Sicherheitslücken oder bremsender Performance. Das muss nicht sein: Das Linux-Magazin demonstriert und erklärt die Stolperfallen. Diesmal: Wieso sich Schleifen und Leerzeichen nicht mögen.
Eigentlich hatten es die Entwickler von Sun gut gemeint und wollten nur helfen: In seinem großen Java-Werkzeugkasten liefert der Anbieter den Wrapper »HtmlConverter« mit, der Anwender unterstützen soll, die leidigen Pfade automatisch zu finden und Environment-Variablen zu setzen. Solche Aufgaben erledigen Shellskripte gerne und meist zuverlässig. Ist die Umgebung erkundet und sind die Variablen gesetzt, braucht der Wrapper nur noch das ursprüngliche Programm aufzurufen (siehe Listing 1).
|
Listing 1: Probleme beim |
|---|
01 $ ls -l
02 -rw-rw-r-- 1 pub lo 980 09-08-20 00:18 index.html
03 -rw-rw-r-- 1 pub lo 623 09-08-20 00:19 pub login.html
04 $ HtmlConverter -simulate index.html
05 Zu konvertierende Dateien:
06 /tmp/box/index.html
07 [...]
08 Konvertierung ok.
09 $ HtmlConverter -simulate "pub login.html"
10 Es sind keine zu verarbeitende Dateien vorhanden.
11 $ tail -2 /usr/bin/HtmlConverter
12 # Execute the converter
13 ${BINDIR}/java -jar ${LIBDIR}/htmlconverter.jar $*
|
Doch komisch: Analysiert das Programm, das eigentlich Applet-Code in HTML-Dateien konvertieren soll, die erste Datei »index.html« noch korrekt, so verweigert es beim zweiten Aufruf mit dem Dateinamen »pub login.html« seinen Dienst, wie Zeilen 9 und 10 anschaulich demonstrieren. Ein Blick auf die letzten Zeilen des Programmcode bestätigt: Einmal mehr kommt ein Shellskript nicht mit allen erlaubten Pfadnamen zurecht, offenkundig hat es Probleme mit Dateien, deren Namen Leerzeichen enthalten.
Umstrittene Umlaute
Im Prinzip sind als Dateiname unter den normalen Linux-Dateisystemen alle Zeichen außer dem Slash und dem Zeichen mit dem Wert 0 erlaubt, also durchaus auch Leerzeichen, Umlaute und ausländische Schriftzeichen, Dollarzeichen, Ampersands und führende Bindestriche. Ob das in jedem Fall nützlich ist, steht freilich auf einem anderen Blatt. Wer etwa per Skript alle Textdateien ausgeben möchte, greift oft auf ein Skript mit einer For-Schleife wie in Listing 2 zurück.
|
Listing 2: Einfache |
|---|
01 #!/bin/sh 02 for i in *.txt; do 03 echo $i ... 04 done |
Das klappt dann auch mit allen Sonderzeichen, wie Abbildung 1 zeigt. Einzig das Muster mit den drei führenden Punkten greift das Globbing-Muster »*.txt« nicht auf. Wer auch Punkt-Dateien aufnehmen möchte, muss daher das Muster ».*.txt« noch hinzufügen. Achtung: Das klappt nur mit einer Endung, denn auf ».*« passen auch die beiden Verzeichnisse ».« und »..«. Dotfiles mit mindestens zwei Buchstaben erwischt am besten das Shell-Pattern ».??*«.

Abbildung 1: Dateinamen erlauben auch Ungewohntes wie Blanks, führende Bindestriche oder ausländische Glyphen. Nicht alle Skripte gehen damit richtig um.
Skripte recyceln Daten
Wer genau hinschaut, stellt fest, dass in der Ausgabe die führenden Leerzeichen der Datei verschwunden sind, deren Name nur aus ebensolchen sowie der Endung besteht. Besser wäre beim Echo-Befehl alle Argumente in doppelte Anführungszeichen einzuschließen. Die werten nämlich Shell-Variablen noch aus, behalten aber alle Leerzeichen bei.
Nun lässt sich die Liste der Dateien nicht immer direkt durch so ein Muster, hier »*.txt« in Zeile 2, erzeugen, sondern ist das Ergebnis eines Befehls oder einer Verkettung von Befehlen. Da liegt es nahe, den Backquote-Operator »`Kommando`« der Bourne-Shell zu verwenden. Das lässt sich etwa in der Bash etwas lesbarer gestalten, wenn der Entwickler den Posix-Operator »$(Kommando)« verwendet. Sonst ist die Funktionsweise gleich.
Blanks verwirren Schleifen
Vorsicht ist allerdings geboten, wenn Entwickler das Ergebnis einer solchen Befehlssubstitution einer Variable zuweisen oder direkt in die Schleife einsetzen, wie dies in Listing 3 geschieht. Dass der Aufruf von »ls« in Zeile 2 das Extra-Argument »–« benötigt, liegt dabei nur indirekt an der Shell: Die Bash expandiert zuerst das Glob-Pattern und übergibt dessen Ergebnis als Parameter an den Unterprozess »ls«. Da aber einer der Namen mit einem Minuszeichen beginnt, fasst das Unix-Programm ihn als Option auf. Der doppelte Bindestrich signalisiert, dass nach ihm kein Parameter mehr als Option zu interpretieren ist.
Das Ergebnis stellt dennoch nicht zufrieden, da die For-Schleife die Leerzeichen in den Dateinamen als Trenner auffasst. Abbildung 2 verdeutlich das Desaster. Das liegt daran, dass der Unterprozess »ls« seine Daten über die Standardausgabe zum aufrufenden Shellskript überträgt. Dort ist nicht mehr vermerkt, wo ein einzelnes Listenelement endet.
|
Listing 3: Substituionen |
|---|
01 #!/bin/bash 02 for i in $(ls -- *.txt); do 03 echo "$i ..." 04 done |

Abbildung 2: Leerzeichen in Kommandosubstitionen führen ohne Vorsichtsmaßnahmen leicht zu Datenmüll. Ohne richtiges Quoting wirken sie als Trenner.
Daher ist es in diesem Fall besser, das Ergebnis des letzten Befehls zeilenweise zu verarbeiten. Die While-Schliefe aus Listing 4 setzt dieses Prinzip um. Hier steht das Kommando einzeln, über Pipes ließen sich auch einfach weitere Bearbeitungsstufen einfügen, etwa durch »grep« oder »sed«. Anschließend leitet das Programm seine Ausgabe in die While-Schleife von Zeile 3 um. Sie läuft solange wie die Bash noch eine Input vorfindet und zeilenweise in der Variable »i« speichert, die sich später mit »$i« auslesen lässt.
|
Listing 4: Zeilenweise |
|---|
01 #!/bin/sh 02 ls -- *.txt | 03 while read i; do 04 echo "$i ..." 05 done |
Arglistige Argumentlisten
Eine ähnliche Problematik tritt auf, wenn ein Shellskript vom Aufrufer eine a priori unbekannte Anzahl von Parametern erhält. Die ersten neun Argumente ruft ein Programmierer durch »$1« bis »$9« ab, mit dem Shell-Befehl »shift« lässt sich das erste Argument entfernen, alle anderen rutschen dann nach. Alle Argumente auf einmal legt die Bash in der Variable »$*« ab, allerdings erneut ohne nach Argumentgrenzen zu trennen. Wer Listing 5 mit dem Aufruf »demo.sh apfel “bittere birne” coole kiwi« startet, erhält
...apfel... ...bittere... ...birne... ...coole... ...kiwi...
als Ausgabe. Um dieses Problem zu lösen, gibt es die ähnlich arbeitende Variable »$@«. Im normalen Kontext arbeitet sie genau wie »$*«. Schließt sie der Bash-Hacker jedoch in doppelte Anführungszeichen ein und verwendet sie in einem Listenumfeld wie der For-Schleife, so überträgt die Shell die Elemente einzeln. Listing 6 zeigt das korrekte Skript.
|
Listing 5: Parameter |
|---|
01 #!/bin/sh 02 for i in $*; do 03 echo "...$i..." 04 done |
|
Listing 6: Variante versteht |
|---|
01 #!/bin/sh 02 for i in "$@"; do 03 echo "...$i..." 04 done |
Leer, kein und nichts
Die Variable »$@« eignet sich auch dazu, ganze Kommandozeilen an weitere Befehle zu übergeben, etwa durch »cat “$@”«, das auf diese Weise auch die Dateien mit Leerzeichen im Namen ausgibt. Sollte das Programm aber gar keine Parameter erhalten, entsteht gleich das nächste Problem: Die Variable expandiert dann zum leeren String »””«. Einem Kommando wie »cat« übergeben, versucht dieses erfolglos eine Datei mit leerem Namen zu öffnen [1].
Aus diesem Grund verwenden Bash-Profis bei solchen Gelegenheiten das Konstrukt »${1+”$@”}«. Es bedeutet, dass die Shell zunächst »$1« überprüft (die geschweiften Klammern dienen zur Klarheit und dürfen immer um die Ausdrücke nach dem Dollarzeichen stehen). Der Plus-Modifier findet so heraus, ob diese Variable überhaupt definiert ist und einen Wert besitzt. Gibt es also mindestens ein Argument, so verwendet die Shell den Ausdruck »$@«. Andernfalls expandiert sie zu gar nichts, nicht einmal einem leeren Dateinamen [2].
Leerschritte und Sonderzeichen sind also kein Teufelswerk, robuste Skripte kommen damit klar. In Schleifenkontexten zwingt gerade das Space Entwickler dazu, genau nachzudenken, ob die Liste das Zeichen als Trenner verwendet. Wer zeilenweise vorgeht, agiert auf der sicheren Seite. Argumente übergeben Entwickler am besten mit »$@« und dem richtigen Quoting.
|
Infos |
|---|
|
[1] Tutorium für die Bourne Shell:[http://www.grymoire.com/Unix/Sh.html] [2] Bourne Again Shell:[http://gnu.org/software/bash/] |






