blog · git · desktop · images · contact & privacy · gopher


Bash: „brace expansion“ vs. „command substitution“

Vor einem Monat bin ich über ein etwas merkwürdiges Verhalten in der Bash gestolpert. Zugehöriger Thread ist dieser hier. Es geht darum, dass die im Titel genannten Funktionen etwas mit der Intuition kollideren. Dort im Thread ging es um einen regulären Ausdruck, der innerhalb einer "command substitution" verwendet wird -- und dabei das Ergebnis völlig verfälscht. Jedoch nur in der Bash. Die zsh macht es anders (richtig?).

Ich habe bis heute noch keine befriedigende Erklärung hierfür gefunden.

Fangen wir hiermit an:

$ echo {1..3}
1 2 3

Das ist brace expansion, quasi soetwas wie ein in die Bash eingebautes seq (von der Grundidee her).

Möchte ich nun diese Expansion verhindern, kann ich zum Beispiel doppelte Anführungszeichen drumherum setzen:

$ echo "{1..3}"
{1..3}

So weit, so gut. Nun geben wir etwas command substitution hinzu:

$ echo $(echo "{1..3}")
{1..3}

Die Ausgabe ist dieselbe, jedoch wird für das innere "echo" eine Subshell gestartet. Das kann man schön nachvollziehen, wenn man "set -x" aktiviert:

$ set -x
$ echo $(echo "{1..3}")
++ echo '{1..3}'
+ echo '{1..3}'
{1..3}

Nun ist es so, dass in dieser Form zwar die Ausgabe des inneren "echo" abgefangen wird. Auf diese Ausgabe wird nachträglich jedoch noch einmal word splitting angewendet. Das äußert sich zum Beispiel darin, dass im folgenden Beispiel die Leerzeichen verlorengehen:

$ echo $(echo "foo        {1..3}          bar")
foo {1..3} bar

Der normale Weg ist nun, noch einmal doppelte Anführungszeichen um "$(...)" herum zu setzen. Doch was passiert nun?

$ echo "$(echo "foo        {1..3}          bar")"
foo        1          bar foo        2          bar foo        3          bar

Die Leerzeichen bleiben erhalten, doch die Ausgabe ist nicht, was wir erwartet hatten. "Eigentlich" sollte

foo        {1..3}          bar

erscheinen.

Gehen wir einen Schritt zurück, lassen der Übersichtlichkeit zuliebe die Leerzeichen außen vor und betrachten einfach nur folgendes:

$ echo "$(echo "{1..3}")"
1 2 3

Hier sieht es nun so aus, als funktionierten die inneren Anführungszeichen zur Verhinderung der "brace expansion" nicht mehr. Wirklich? Mit aktiviertem "set -x" zeigt sich etwas sehr seltsames:

$ set -x
$ echo "$(echo "{1..3}")"
++ echo 1
++ echo 2
++ echo 3
+ echo 1 2 3
1 2 3

In der Subshell wird nicht einfach nur eine "brace expansion" durchgeführt. Stattdessen werden sogar drei Subshells gestartet. In der Tat ist obiger Aufruf äquivalent zu diesem:

$ set -x
$ echo $(echo 1) $(echo 2) $(echo 3)
++ echo 1
++ echo 2
++ echo 3
+ echo 1 2 3
1 2 3

Ist das nicht wundersam? Sollten nicht die äußeren Anführungszeichen um "$(...)" herum unabhängig von den inneren Anführungszeichen um "{1..3}" herum sein? Man betrachte zum Vergleich folgenden Aufruf:

$ set -x
$ echo "$(echo "*")"
++ echo '*'
+ echo '*'
*

Hier ist es so, dass die Anführungszeichen um den Stern herum diesen daran hindern, Globbing auszuführen. Hingegen die Anführungszeichen um "$(...)" bewirken, dass nachträglich, nachdem die Ausgabe der Subshell aufgefangen wurde, kein Globbing mehr ausgeführt wird. (Im Zweifel probiere man hier die verschiedenen Varianten einmal durch.)

Ich dachte auch immer, dass alles innerhalb von "$(...)" so behandelt wird, als hätte man es direkt eingegeben. Das ist mein eigentliches Problem an der ganzen Sache, denn offenbar ist das nicht so.

Ich habe jedoch nun schon in vielen verschiedenen Versionen (Bash 4 unter Arch, Bash 3 unter msys auf Windows, laut Thread die Bash in verschiedenen Ubuntu-Versionen) das "falsche" Verhalten beobachten können. Das macht mich stutzig. Ist das wirklich ein Bug? Oder ist das doch Absicht? Ist da noch keiner vorher drüber gestolpert? Die Bash verhält sich offenbar schon seit vielen Jahren so, deswegen traue ich mich kaum, das auf der Bash-Mailingliste anzusprechen. ;)

Irgendwo ist ein Denkfehler versteckt.

"Lösbar" ist das natürlich, indem man innen einfache Anführungszeichen setzt:

$ set -x
$ echo "$(echo '{1..3}')"
++ echo '{1..3}'
+ echo '{1..3}'
{1..3}

Eine Erklärung für das ursprüngliche Verhalten fehlt mir jedoch.

Ich könnte jetzt noch einige Beispiele zur Verdeutlichung bringen, weshalb ich dieses Verhalten für falsch halte. Das würde aber den Rahmen etwas sprengen und zu viel Verwirrung stiften, denke ich. Ein Beispiel muss jedoch noch sein.

Nehmen wir mal an, dass "brace expansion" einfach Vorrang vor allem anderen hat. Starten wir dann bei diesem Beispiel:

$ echo "abc"{1..3}'$'
abc1$ abc2$ abc3$

"abc" ist die Präambel der "brace expansion", danach kommt die Sequenz und dann "$" als Postskript. Die Ausgabe entspricht dann meiner Erwartung.

Hat nun "brace expansion" absolute Priorität, wieso geht dann das hier nicht?

$ echo "$(echo "{1..3}')'

Die Bash wird sich hier beschweren, dass ein gequoteter Bereich nicht abgeschlossen ist. Doch wieso? Wäre hier nicht wieder "$(echo" die Präambel und ")" das Postskript, sodass der komplette Befehl am Ende zu "echo $(echo 1) $(echo 2) $(echo 3)" zusammengebaut wird?