In der Shell-Skripting werden manchmal Vergleiche verwendet, bei denen jeder Wert mit “x” präfixiert ist. Hier sind einige Beispiele von GitHub:
if [ "x${JAVA}" = "x" ]; then
if [ "x${server_ip}" = "xlocalhost" ]; then
if test x$1 = 'x-help' ; then
Ich nenne das den “x-Hack”.
Für jede POSIX-konforme Shell ist der Wert des x-Hacks genau null: Dieser Vergleich funktioniert ohne das x zu 100%. Aber warum war das überhaupt ein Thema?
Online-Quellen wie diese Stackoverflow-Frage-Antwort sind etwas unklar und sagen, dass es eine Alternative zur Anführungszeichen ist (oof), dass es Probleme mit “einigen Versionen” bestimmter Shells gibt oder dass sie im Allgemeinen vor den mysteriösen Verhaltensweisen besonders alter Unix-Systeme warnen, ohne konkrete Beispiele zu liefern.
Um festzustellen, ob ShellCheck davor warnen sollte und falls ja, welche ausführliche Begründung dafür verwendet werden sollte, habe ich beschlossen, mit Hilfe der Archive der Unix Heritage Society in die Geschichte von Unix einzutauchen. Leider konnte ich nicht in die streng bewachte Welt von HP-UX und AIX eindringen, also seid gewarnt, Dinosaurierhirten.
Hier sind die Fälle, in denen es fehlschlagen kann.
Die linke Seite entspricht einem unären Operator
Die AT&T Unix v6-Shell aus dem Jahr 1973, wie sie in PWB/UNIX aus dem Jahr 1977 gefunden wurde, konnte keine Testbefehle ausführen, deren linke Seite einem unären Operator entspricht. Dies muss jedem aufgefallen sein, der versucht hat, Kommandozeilenparameter zu überprüfen:
% arg="-f"
% test "$arg" = "-f"
syntax error: -f
% test "x$arg" = "x-f"
(true)
Dies wurde in der AT&T Unix v7 Bourne-Shell-Builtin aus dem Jahr 1979 behoben. Allerdings waren test und [ auch als separate ausführbare Dateien verfügbar und schienen eine Variante des fehlerhaften Verhaltens beibehalten zu haben:
$ arg="-f"
$ [ "$arg" = "-f" ]
(false)
$ [ "x$arg" = "x-f" ]
(true)
Dies geschah, weil das Dienstprogramm einen einfachen rekursiven Abstiegsparser ohne Backtracking verwendete, der unäre Operatoren vor binären Operatoren bevorzugte und nachgestellte Argumente ignorierte.
Das “moderne” Verhalten der Bourne-Shell wurde vom Public Domain KornShell im Jahr 1988 kopiert und 1992 Teil von POSIX.2. GNU Bash 1.14 hat dasselbe für seinen Builtin [, und das GNU shellutils-Paket, das die externen Test/[-Binärdateien bereitstellte, folgte POSIX, sodass die frühen GNU/Linux-Distributionen wie SLS nicht betroffen waren, genauso wenig wie FreeBSD 1.0.
Der x-Hack ist effektiv, weil kein unärer Operator mit x beginnen kann.
Beide Seiten entsprechen dem String-Länge-Operator -l
Ein ähnliches Problem, das länger überlebt hat, bestand beim String-Länge-Operator -l. Im Gegensatz zu den normalen unären Prädikaten wurde dieser nur als Teil eines Operanden für binäre Prädikate analysiert:
var="helloworld"
[ -l "$var" -gt 8 ] && echo "String ist größer als 8 Zeichen"
Das hat es nicht in POSIX geschafft, weil es, wie die Begründung besagt, “in den meisten Implementierungen undokumentiert war, aus einigen Implementierungen entfernt wurde (einschließlich System V) und die Funktionalität vom Shell bereitgestellt wurde”, wobei auf [ ${#var} -gt 8 ] verwiesen wird.
In UNIX v7, wo = Vorrang hatte, war das kein Problem, aber Bash 1.14 von 1996 würde es gierig vorweg analysieren:
$ var="-l"
$ [ "$var" == "-l" ]
test: -l: binärer Operator erwartet
$ [ "x$var" == "x-l" ]
(true)
Es war auch ein Problem auf der rechten Seite, aber nur in verschachtelten Ausdrücken. Die -l-Überprüfung stellte sicher, dass es ein zweites Argument gab, daher benötigen Sie einen zusätzlichen Ausdruck oder Klammern, um dies auszulösen:
$ [ "$1" = "-l" -o 1 -eq 1 ]
Zu viele Argumente
$ [ "x$1" = "x-l" -o 1 -eq 1 ]
(true)
Dieser Operator wurde später im selben Jahr mit Bash 2.0 entfernt und das Problem behoben.
Die linke Seite ist !
Ein weiteres Problem in frühen Shells war, wenn die linke Seite der Verneinungsoperator ! war:
$ var="!"
$ [ "$var" = "!" ]
argument expected (UNIX v7, 1979)
=: unärer Operator erwartet (bash 1.14, 1996) (false) (pd-ksh88, 1988)
$ [ "x$var" = "x!" ]
(true)
Auch hier ist der x-Hack effektiv, indem er verhindert, dass das ! als Verneinungsoperator erkannt wird.
Ksh behandelte dies genauso wie [ ! “=” ] und ignorierte den Rest der Argumente. Dies ergab stillschweigend false, da = kein Null-String ist. Ksh ignoriert auch heute noch nachgestellte Argumente:
$ [ -e / random Wörter/ops hier ]
(true) (ksh93, 2021)
bash: [: zu viele Argumente (bash5, 2021)
Bash 2.0 und Ksh93 haben dieses Problem behoben, indem sie = im 3-Argument-Fall Priorität einräumen, in Übereinstimmung mit POSIX.
Die linke Seite ist “(“
Das ist bei weitem mein Favorit.
Die eingebaute UNIX v7 scheiterte, als die linke Seite eine linke Klammer war:
$ left="("
$ right="("
$ [ "$left" = "$right" ]
argument expected
$ [ "x$left" = "x$right" ]
(true)
Dies geschieht, weil ( Vorrang vor = hat und eine ungültige Klammergruppe wird.
Warum ist das mein Favorit? Schau dir Dash 0.5.4 an, bis 2009:
$ left="("
$ right=")"
$ [ "$left" = "$right" ]
[: 1: Schlussklammer erwartet
$ [ "x$left" = "x$right" ]
(true)
Das war ein aktiver Bug, als die StackOverflow-Frage gestellt wurde.
Aber warte, es gibt mehr!
Hier ist Zsh Ende 2015, direkt vor Version 5.3:
% left="("
% right=")"
% [ "$left" = "$right" ]
(true)
% [ "x$left" = "x$right" ]
(false)
Unglaublicherweise konnte der x-Hack bis 2015 verwendet werden, um bestimmte Fehler zu umgehen, sieben Jahre nachdem StackOverflow ihn als archaisches Relikt der Vergangenheit abgeschrieben hatte!
Die Fehler sind natürlich immer schwerer zu finden. Der Zsh tritt nur auf, wenn die linke Klammer mit der rechten Klammer verglichen wird, da der Parser sonst zurückverfolgt und es herausfindet.
Ein weiterer später Nachzügler war Solaris, dessen /bin/sh die Legacy-Bourne-Shell noch in Solaris 10 von 2009 war. Dies geschah jedoch zweifellos aus Gründen der Kompatibilität und nicht, weil sie glaubten, dass dies eine lebensfähige Shell sei. Eine “standardskonforme” Shell war schon lange vor Solaris 11 verfügbar und wurde erst 2011 standardmäßig auf ksh93 umgestellt.
In allen Fällen ist der x-Hack effektiv, weil er verhindert, dass die Operanden als Klammern erkannt werden.
Fazit
Der x-Hack war in mehreren Shells tatsächlich nützlich und effektiv gegen mehrere reale und praktische Probleme.
Der Wert war jedoch Mitte bis Ende der 1990er Jahre größtenteils verschwunden und die letzten verbleibenden Probleme wurden vor 2010 behoben – schockierend spät, aber vor über einem Jahrzehnt.
Der letzte hat es bis 2015 geschafft, aber nur im sehr spezifischen Fall des Vergleichs einer öffnenden Klammer mit einer geschlossenen Klammer in einer bestimmten Nicht-System-Shell.
Ich denke, es ist an der Zeit, diese Verfahrensweise zu verabschieden, und ShellCheck bietet jetzt standardmäßig einen Stilvorschlag an.
Epilog
Das Problem von [ “(” = “)” ] wurde 2008 in einer Form gemeldet, die sowohl Bash 3.2.48 als auch Dash 0.5.4 betraf. Das kann man heute noch in macOS bash sehen:
$ str="-e"
$ [ ( ! "$str" ) ]
[: 1: Schlussklammer erwartet
# Dash
$ [ "$str" == ")" ]
bash: [: `)’ erwartet, gefunden ]
# Bash
POSIX löst all diese Mehrdeutigkeiten für bis zu 4 Parameter und stellt sicher, dass die Bedingungen der Shells überall und zu jeder Zeit gleich funktionieren.
So hat der Dash-Maintainer Herbert Xu es in der Korrektur ausgedrückt:
/* * POSIX-Vorschriften: Derjenige, der das geschrieben hat, verdient den Nobelpreis für den Frieden. */