Aufzucht und Hege von Shell-Scripts: Wie man quotet
Wann sollte man quoten, wann nicht?
Ein guter Grundsatz für das Quoten lautet: Besser zuviel als zuwenig. Allerdings muss man ein wenig aufpassen, WIE man quotet. Prinzipiell schützt Quoting vor Fehlern durch Leerzeichen, Tabulatoren und anderen Gemeinheiten in Variablen. Es gibt allerdings auch Fälle, wo Quoting unangebracht oder einfach überflüssig ist. Ich will hier nicht alle Quoting-Regeln erklären, das findet Ihr in Tutorials, dem Bash-Manual oder in Büchern. Mir geht es um ein paar spezielle Fälle, bei denen man kreativ mit dem Quoting arbeiten kann, sowie um Regeln, wie man Quotings vor sich selbst schützt.
Es gibt Fälle, in denen ein Quoting überflüssig ist - wie bei Variablen, von denen man weiß, dass sie gesetzt und numerisch sind, z. B.:
- Der Returncode des letzten Befehls $?.
- Die Ausgabe eines wc
- Die Anzahl der Argumente in einem Script $#
- usw. usf.
In anderen Fällen kann es nützlich sein, wenn man das Quoting bewusst weglässt. Das folgende Beispiel nutzt das, um ein Shell-Array zu füllen:
line="field1 field2 field3 field4"
declare -a fld_arr
fld_arr=($line) # hier nicht quoten, um an einzelne Felder zu kommen
for (( i=0; i < ${#fld_arr[*]}; i++ )); do
echo $i ${fld_arr[$i]}
done
Das Quoting-Zeichen ' (einfaches Hochkomma) sorgt dafür, dass im umschlossenen Text überhaupt kein Sonderzeichen mehr durch die Shell ausgewertet wird. Das wird dann zum Problem, wenn im Text eben dieses Zeichen auftaucht, es kann nämlich nicht entwertet werden, weil alle anderen verfügbaren Quoting-Zeichen wirkungslos bleiben. Ich nutze im Folgenden angepasste Versionen meiner Beiträge in einem Forum-Thread als Beispiel:
# Hochkomma im Text
jan@jack:~/tmp> echo '123'(456\('
bash: syntax error near unexpected token `('
# Versuch, das eingeschlossene Hochkomma zu entwerten
jan@jack:~/tmp> echo '123\'(456\('
bash: syntax error near unexpected token `('
# So geht es: Das eingeschlossene Hochkomma wird ausserhalb der '' geparkt
# und vor der Shell mit Anfuehrungszeichen versteckt
jan@jack:~/tmp> echo '123'"'"'(456\('
123'(456\(
Wie kann man diese Methode anwenden, um mit variablen, unbekannten Inhalten umgehen zu können? Als Beispiel dient ein Verzeichnis, in dem sich ein paar ganz schlimme Dateinamen herumtreiben - MP3-Dateien z. B. haben nach meiner Erfahrung oft solche wüsten Namen, manche Verwaltungsprogramme schrecken ja vor nichts zurück ;-)
jan@jack:~/tmp/muell_namen> ls 123'(456\(b l a) 123'(456\(bla) 123 "buh" 456 jan@jack:~/tmp/muell_namen> find . -type f -printf "ls |%p|\n" | > sed "s/'/'\"'\"'/g;s/|/'/g" | sh ./123'(456\(bla) ./123'(456\(b l a) ./123 "buh" 456
Wenn man die Dateinamen einfach per ls an eine Schleife verfüttert und dann mit den Variablen
weiterarbeiten will (z. B. um die Dateien umzubenennen), dann hat man mit den oben genannten Problemen zu tun. Als
Ausweg dient hier das Tarnen der umschließenden Hochkommata durch ein anderes Zeichen. Die
-printf-Option ist leider nur im GNU-find verfügbar. Im Beispiel wird jeder Dateiname in
Pipe-Zeichen eingeschlossen (dieses Zeichen darf natürlich nicht in Dateinamen auftreten).
Anschließend geht der Dateiname durch eine sed-Wäsche. Dabei wird zuerst jedes auftretende
Hochkomma durch die rettende Zeichenfolge '"'"' ersetzt, anschließend jedes Fluchtzeichen
| durch ein Hochkomma - jetzt kann man mit den Dateinamen arbeiten.
Manchmal muss man auch andere Quoting-Zeichen verstecken - zum Beispiel bei geschachtelten Kommandosubstitutionen: wie kriegt man ineinander gesteckte Kommandos zum Laufen (also in einem Kommando die Ausgabe eines anderen Kommandos, das die Ausgabe eines dritten Kommandos nutzt, nutzen ;-)? Das folgende Kommando gibt den nächsten Arbeitstag in der Form "Montag, 21. Juli" aus. Dazu nutzt es ausgiebig den date-Befehl aus (funktioniert aber nur mit der GNU-Version).
date +"%A, %d. %B" -d "today +`if test \`date +%w\` -gt 4; then \
expr 8 - \`date +%w\`; else echo 1; fi` days"
Im Folgenden eine Step-by-Step-Erklärung der einzelnen Bestandteile des Kommandomonsters:
- Ziel ist es, den date in folgender Form aufzurufen:
date +"%A, %d. %B" -d "today +X days"
wobei X abhängig ist vom aktuellen Wochentag. Am Freitag muss ich 3 Tage addieren, um auf den kommenden Montag zu kommen, am Samstag 2 Tage und an allen anderen Tagen ist der nächste Tag auch der nächste Arbeitstag. - Den aktuellen Wochentag ermittle ich mit date +%w, dass mir Werte von 0 (Sonntag) bis 6 (Samstag) ausgibt. Damit kann ich eine Fallunterscheidung (Wochentag > 4) machen.
- Zum Zweck der Unterscheidung nutze ich die Ausgabe einer If-Then-Else-Kontrollstruktur:
if test `date +%w` -gt 4; then ...; else echo 1; fi
Innerhalb der Zweige gebe ich die ermittelten Tage aus, die Ausgabe der Kontrollstruktur selbst wird als Argument für die -d-Option eingesetzt. Damit muss ich aber alle Backslashes, die innerhalb der Kontrollstruktur auftreten, entwerten, da sie sonst das Ende der Kommandosubstitution bedeuten würden:
`if test \`date +%w\` -gt 4; then ...; else echo 1; fi` - Für den Freitag und Samstag berechne ich die Anzahl der zu addierenden Tage. Um auf die richtigen
Werte zu kommen, muss ich den Wochentag von 8 subtrahieren. Das kann man so machen:
expr 8 - `date +%w`
Diese Berechnung steht aber wieder innerhalb der Kontrollstruktur, also:
`if test \`date +%w\` -gt 4; then expr 8 - \`date +%w\`; else echo 1; fi`
So, und jetzt will ich das in eine Variable schreiben. Dafür stülpe ich eine neue "`...`"-Schale um den ganzen Befehl. Was muss ich jetzt alles entwerten, damit alle Zeichen in ihrer gewünschten Bedeutung auch in der Subshell ankommen, in der sie gebraucht werden? Das ist eigentlich ganz einfach: Vor der nun hinzukommenden Shell müssen alle Zeichen verborgen werden, die für sie eine Sonderbedeutung haben. Das sind in unserem Fall: Anführungzeichen, Backslash und Backtick. Vor jedes dieser Zeichen setzen wir einfach einen Entwerter, nämlich den Backslash - fertig:
NEXT_WDAY="`date +\"%A, %d. %B\" -d \"today +\`if test \\\`date +%w\\\` -gt 4; then \\
expr 8 - \\\`date +%w\\\`; else echo 1; fi\` days\"`"

