- Auf Unix-ähnlichen Systemen kann es eine ausführbare Datei
/bin/[geben, deren Name aus einem einzigen Symbol besteht; auch Syntax, die wie ein Shell-Bedingungsausdruck aussieht, basiert tatsächlich auf dem Ausführen von Befehlen und deren Exit-Codes testwertet Ausdrücke aus und gibt 0 zurück, wenn sie wahr sind, und 1, wenn sie falsch sind; wird es als[aufgerufen, prüft es zusätzlich, ob das letzte Argument]ist- Viele Shells stellen
testund[auch als Built-in-Befehle bereit, daher können sich Fehlermeldungen oder Verhalten der externen Programme/bin/testund der Shell-internen Implementierung unterscheiden - Die Bash-Erweiterung
[[ist kein externer Befehl, sondern eingebaute Syntax; sie wendet daher andere Regeln als[an, etwa indem sielong*wie im Beispiel nicht per Glob erweitert, sondern als literalen String vergleicht - Für portable Skripte ist
[die richtige Wahl; wenn es Bash-only ist, ist es besser, konsequent[[zu verwenden — man sollte die Unterschiede bei den Erweiterungsregeln beider Varianten aber kennen
Was hinter /bin/[ und /bin/test steckt
- Auf Unix-Systemen kann es eine ausführbare Datei
/bin/[geben, deren Name ein einzelnes Symbol ist- Der Beispielbefehl
ls /bin/?zeigt/bin/[an
- Der Beispielbefehl
/bin/[und/bin/testkönnen auf dasselbe Binary verweisen- Im Beispiel erscheinen die beiden Pfade als Dateien mit derselben inode und Größe
- Allerdings müssen sie nicht auf jedem System Hardlinks sein
testist ein Programm, das in der Shell Ausdrücke auswertet- String-Vergleiche
- Zahlenvergleiche
- Prüfung von Dateibedingungen
- Ist das Ergebnis der Auswertung wahr, gibt es den Exit-Code 0 zurück, ist es falsch, 1
Warum [ wie ein Befehl funktioniert
test a = bsieht kaum wie ein Bedingungsausdruck aus, aber schreibt man dieselbe Logik als[ a = b ], wirkt sie vertrauter[sieht wie separate Syntax aus, ist tatsächlich aber ein Befehlsaufrufif [ a = b ]; then ... fiführt den Befehl[aus und prüft den Exit-Code- Wird das Programm als
[aufgerufen, prüft es, ob das letzte Argument die schließende eckige Klammer]ist
- Eine
if-Anweisung interpretiert den Bedingungsausdruck nicht direkt, sondern verzweigt anhand des Exit-Codes des übergebenen Befehlstest a = a; echo $?ergibt0test a = b; echo $?ergibt1[ a = a ]; echo $?ergibt0[ a = b ]; echo $?ergibt1
- Im selben Sinne lassen sich auch
trueundfalseals Hilfs-Binaries betrachten, die Exit-Codes zurückgeben
Unterschiede zwischen externem Binary und Shell-Built-in
- Weil
testund[in Shell-Skripten häufig verwendet werden, implementieren die meisten Shells sie auch als Built-in-Befehle - Selbst bei gleicher Eingabe kann sich die Ausgabe eines externen Binarys von der eines Shell-Built-ins unterscheiden
/bin/test a bergibttest: a: unexpected operatortest a bergibtdash: 2: test: a: unexpected operator
- Solche Unterschiede können nicht nur bei
testund[, sondern auch bei scheinbar einfachen Befehlen wieechoauftreten - Da sich die Built-in-Implementierungen je nach Shell unterscheiden, kann sich das Verhalten eines Skripts je nach ausführender Shell ändern
Die separaten Regeln der Bash-Erweiterung [[
[[ist eine Bash-Erweiterung und kann die Verwendung von[ersetzen- Der größte Unterschied ist, dass
[[immer eingebaute Syntax ist- Anders als
[, das als externes Binary ausgeführt werden kann, erlaubt[[Bash, die Sprachregeln innerhalb des Ausdrucks zu ändern
- Anders als
- Im Glob-Beispiel verhalten sich
[und[[unterschiedlich- Nach
touch long-namegibt[ long* = long-name ] && echo matchmatchaus - Auf die Argumente des Befehls
[werden die normalen Shell-Erweiterungsregeln angewendet, sodasslong*zulong-nameim Verzeichnis erweitert wird [[ long* = long-name ]] && echo matchgibt nichts aus[[behandeltlong*als literalen String und vergleicht ihn unverändert mitlong-name, was fehlschlägt
- Nach
- In Bash-only-Skripten lassen sich mit
[[auch Funktionen wie Regex-Matching=~verwenden
Was man in Skripten wählen sollte
- Für portable Shell-Skripte sollte man
[verwenden testkann ebenfalls verwendet werden, ist aber nicht die übliche Wahl- Wenn ein Skript ausschließlich für Bash gedacht ist, ist es besser, konsequent
[[zu verwenden - Die Shell selbst hat ebenfalls Ausdrucksoperatoren wie
!,&&und||- Diese Operatoren arbeiten anhand des Exit-Status von Befehlen
grep ^hello$ ... && grep ^bye$ ...führt dazu, dass der gesamte Exit-Code0ist, wenn beide Befehle erfolgreich sind- Scheitert das erste
grep, wird auch der Befehl hinter&&nicht erfolgreich ausgeführt, und der gesamte Exit-Code wird1
- Daher lassen sich
test-/[-Ausdrücke und logische Operatoren der Shell in einer Bedingung kombinieren- Beispiel:
[ a = b ] || grep -q ^hello$ /usr/share/dict/words
- Beispiel:
- POSIX verlangt nicht, dass
/bin/[und/bin/testHardlinks sind- Unter NetBSD waren sie Hardlinks
- macOS Catalina stellt separate Kopien desselben Binarys bereit
- Debian testing stellt unterschiedliche Binaries bereit
- Die POSIX-Spezifikation verlangt nicht, dass die beiden Dateien Links sind
1 Kommentare
Hacker-News-Kommentare
Ich bin der Autor des Originals. Danke fürs Teilen, und ich freue mich, dass es es bis auf die Frontpage geschafft hat. Im Titel sollte wahrscheinlich (2020) stehen, und „test“ bezeichnet tatsächlich einen Befehl, daher wäre es besser, es nicht großzuschreiben.
Es gibt auch einen verwandten Beitrag, den ich 2021 geschrieben habe; er behandelt sogar den
[[-Operator von Bash und dürfte in diesem Kontext interessant sein: https://jmmv.dev/2021/08/useless-use-of-gnu.html[[ist streng genommen kein eingebauter Befehl, sondern im Kern eher ein Syntaxelement. Intern wird vermutlich ein kaum direkt zugänglicher Builtin verwendet, aber interessant ist, dass auch]]ein reserviertes Wort ist, obwohl es gar nicht an einer Stelle stehen kann, an der ein reserviertes Wort sinnvoll wäre.In einigen Nicht-Bash-Shells braucht man das Schlüsselwort
function, um bestimmte Arten von Funktionen zu deklarieren. Bei$(shell)inmakekann der Performance-Unterschied messbar sein, wenn viele Targets gebaut werden. Wenn allerdings nichts passiert, ist es dennoch ein Nachteil; normalerweise ist es daher richtig,includezu verwenden, wenn man eine Regenerierung auslösen will. Dass GNU POSIX ignoriert, ist völlig vertretbar, denn POSIX ist für die Lösung der meisten realen Probleme nicht besonders nützlich..ist hilfreich, und Optionen nachträglich an den gerade eingegebenen Befehl anzuhängen, ist wirklich bequem. Bei Befehlen, die so etwas nicht unterstützen, ärgere ich mich immer.In Skripten ist es meist sinnvoll, sich an POSIX
shzu halten. Zumindest sollte man wissen, ob man Bash-spezifische Syntax verwendet.--ignore-caseundset -o pipefaildie Portabilität von Skripten verringern. Das stimmt an sich.Er erklärt aber nicht, warum Linux-Nutzer sich groß um Portabilität kümmern sollten. OpenBSD und FreeBSD sind zwar quicklebendig, haben aber so wenige Nutzer, dass sie mir kein besonders großer Grund zur Sorge zu sein scheinen. Man kann der Fairness halber sagen, dass man solche Betriebssysteme berücksichtigen sollte, aber wo hört dieser Maßstab auf? Muss man auch Obskures wie
vxWorksberücksichtigen? BusyBox und Alpine sind interessanter, aber die Änderungen sind so groß, dass ohnehin fast immer ein separates Porting nötig ist. Gibt es einen anderen überzeugenden Grund, sich um Nicht-GNU-Ökosysteme zu kümmern?if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; finicht einfach ganz normale, alltägliche Shell-Nutzung?make $(shell …) expansionausgegeben, sollte aber eigentlichmake $(shell ...) expansionsein.Im Text steht es korrekt mit drei Punkten, nicht mit einem Auslassungszeichen, also stimmt auch
mldrselbst nicht. Vermutlich wirken hier zwei voneinander unabhängige Bugs gleichzeitig.Wenn man den letzten Punkt noch einen Schritt weiter treibt, kann man den
if-Block selbst ebenfalls entfernen. Ausif [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fiwird[ a = b ] && echo "Oops!" || echo "Expected; phew!".Ich weiß nicht, wie oft man das so machen sollte, aber es ist gelegentlich nützlich, wenn man bedingt Debug-Ausgaben auf Standardfehler schreiben will, etwa
[ "$debug" ] && echo "what's going on" >&2. Weilif-Blöcke normale Befehle prüfen, ist auch so etwas möglich wieif grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi. Was ich noch nicht untersucht habe, ist, ob man[ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ]schreiben sollte oder den eingebauten logischen UND-Operator vontestverwenden sollte, also[ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]. Wenn Performance keine Rolle spielt, scheinen beide aus ähnlichen Gründen vertretbar.[ a = b ] && echo "Oops!" || echo "Expected; phew!"sollte man nicht als allgemeine Regel übernehmen. Bash wird diese Zeile vermutlich als([ a = b ] && echo "Oops!") || echo "Expected; phew!"interpretieren.Wenn also die Befehlsfolge nach
&&fehlschlägt, wird der Code nach||trotzdem ausgeführt. Wenn zum Beispiel>/dev/full echo "strings match"wegen eines Schreibfehlers fehlschlägt, wird"strings don't match"ausgegeben, obwohl die Strings gleich waren. Das ist nicht dieselbe Semantik wie bei einemif-Block.set -everwendet, das man verwenden sollte, dann funktioniertif [ a = b ]; then echo "Oops!"; fiwie erwartet, aber[ a = b ] && echo "Oops!"beendet sich mit einem Fehler, wenn der Ausdruckanicht gleichbist.-a,-osowie die Operatoren(und)als obsoleszent markiert. Details stehen unter „Application Usage“ auf https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t....-aoder zwei Tests mit&&verwendet: Wenn man die arithmetische Auswertung von Bash nutzen kann, muss man nicht extraexprausführen:[ $((1+1)) -eq 2 ]Vor ein paar Jahren habe ich angefangen,
[nicht mehr zu verwenden.testverstärkt den Punkt, dass das keine Syntax ist, sondern einfach ein Befehl wie jeder andere. Undman testist deutlich angenehmer, als sich durchman bashzu wühlen.man test, sondern auch eine fürman [.In Bash gibt es als schnellen Spickzettel
help test. Der Befehl[ist sehr alt und war bereits 1979 in Version 7 Unix enthalten.Einverstanden.
[und das Bash-spezifische[[sorgen tatsächlich für viel Verwirrung darüber, was eigentlich passiert, sodass man sich nur schwer sicher sein kannAllerdings hatte die Tatsache, dass
[[garantiert ein Builtin ist, sicher ihren Zweck in Zeiten, in denen die Performance von Shell-Skripten relevant war – und das ist gar nicht so lange hertestverwenden. Ich schreibe nicht oft Bash-Skripte und stolpere deshalb ständig überif-Anweisungen, besonders über die Leerzeichenregeln. Nachdem ich den Grund gesehen habe, ist es völlig naheliegend geworden, und mittestwird klarer, dass man einfach nur Argumente übergibtDie größte Falle bei
[undtestist das Verhalten mit genau einem Argument. Zum Beispiel könnte man[ -n $FOO ]schreiben, um zu prüfen, ob eine Variable nicht leer istWenn
FOOaber nicht gesetzt ist, wird es nicht zu einem leeren String erweitert, sondern zu gar nichts, sodass daraus[ -n ]wird. POSIX verlangt, dass die Ein-Argument-Form von[erfolgreich ist, wenn dieses Argument – hier"-n"– nicht leer ist. Daher meldet es fälschlich, dass$FOOnicht leer sei. Variablen müssen unbedingt in Anführungszeichen gesetzt werdentestselbst, sondern in der Shell an sichDas erwähnte Verhalten ergibt Sinn.
[ "$FOO" ]ist nämlich die Form, die prüft, ob der Inhalt – egal welcher, sogar"-n"– nicht leer ist$FOOLeerzeichen enthält, wird es zu mehreren Argumenten erweitert. Einfach immer Variablen in Anführungszeichen setzen[ x"$FOO" != x"" ][ -n "${FOO?}" ]verwenden, damit das Skript sofort abbricht, wenn$FOOnull oder nicht gesetzt istchubot hat ein interessantes Dokument geschrieben, das sich mit den subtileren Aspekten von
test/[/[[befasst. Auch die anderen Beiträge in diesem Blog erklären ziemlich interessant die seltsamen Seiten der Shell¹ https://www.oilshell.org/blog/2017/08/31.html
² https://www.oilshell.org/blog/2016/11/18.html
Ich wusste überhaupt nicht, dass
[ein Programm ist, und finde es etwas lustig, dass es prüft, ob das letzte Argument eine schließende eckige Klammer istImmerhin erklärt das, warum auf beiden Seiten der eckigen Klammern Leerzeichen nötig sind
[[ist Bash-spezifisch. Wenn man weiß, dass man nur Bash verwenden wird, kann man es benutzen. Der Artikel behandelt die Details guthttps://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
[[verwendenEs gibt es auch in zsh und ksh, und ich bin mir ziemlich sicher, dass es eigentlich in ksh von 1988 oder früher begonnen hat
testund[sind in POSIX spezifiziert und existieren üblicherweise als echte Binaries. Sie können allerdings von Shell-Builtins überdeckt werden[[dagegen ist nicht in POSIX spezifiziert und existiert normalerweise nur als Shell-BuiltinSich nur am kleinsten gemeinsamen Shell-Nenner zu orientieren, wirkt wie eine völlig archaische Sorge
Ich habe nicht ganz verstanden, warum die letzte
if-Anweisung verwirrend sein soll. Wenn es daran liegt, dass man beim ersten Lernen von Shell-Skripten normalerweise annimmt,[sei Teil der Bash-Skriptsprache und nicht einfach ein anderes Programm, dann verstehe ich es jetzt. Falls nicht, wäre es gut zu erklären, warum das überraschend ist[ein Binary ist, verstehe ich nicht so recht, warum das verwirrend sein soll. Es sieht nach ganz normalem Bash ausIch habe starke Vorlieben bei Shells, die mit dem Großteil der Welt nicht besonders gut zusammenpassen
Meiner Meinung nach sollte man
[niemals verwenden, sondern nurtest.[verleitet zu dem Irrglauben, der Mechanismus sei Teil der Sprachsyntax, ist in Wirklichkeit aber nur ein weiteres „Programm“. Zu „Programm“ zähle ich hier auch Builtins und Funktionen.if/||/&&betrachten den Exit-Status, und ein Programm kann nach der Expansion den Exit-Status von nichts anderem sehen, außer der magischen Variable$?, die dann einfach ein String ist.casebetrachtet Strings, arbeitet aber nicht anhand des Exit-Status und setzt auch nicht als Teil der Ausführung voncase ... esaceinen Exit-Status. Ein „Programm“ setzt den Exit-Status. Außerdem sollten[/testnur verwendet werden, um Dateisystemstrukturen auszuwerten, etwa beitest -f /dev/null. Für String-Auswertungen sollte man meiner Ansicht nachcaseverwenden. Natürlich jucken mich daher die meisten Skripte beim Lesen, und andere finden meine Skripte seltsamWenn ich Shell schreibe, bevorzuge ich die Form, bei der nach
ifein Programm in einer eigenen Zeile steht und es dann mitthenweitergeht. Das soll betonen, dass nachif„der Exit-Status des letzten Befehls vorthen“ betrachtet wird. In einer Struktur wieif; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fietwa betrachtetifam Ende nur den Exit-Status vonecho, daher wird der then-Zweig immer ausgeführttestsind mein Beweis, dass ich zustimme. Es ist ein einsamer Weg ... ich gebe dem Google Shell Style Guide die SchuldIch habe mir die Vorliebe für
testangewöhnt, als ich Skripte schrieb, die sowohl untershals auch unterbashlaufen mussten, aber ich bin dabei geblieben, weil es semantisch sinnvoller ist, als das Zeichen[wie einen Befehl zu behandeln. Dass]kein eigenes Binary ist, sondern ein Argument von[, ist ebenfalls seltsam. Ich verstehe den technischen Grund, aber es fühlt sich wie ein Hack antestverwenden. Dutzende!Ich habe es so gelernt, dass man
[[nur verwendet, wenn man Regex-Matching machen will. Beispiel:if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fiAnsonsten nutze ich einfach
"test"oder"[". Ich bin inzwischen bei 85.000 Zeilen Bash. Das heißt nicht, dass Bash großartig wäre, aber es erfüllt noch immer bei vielen Dingen meine AnforderungenexprBasic Regular Expressions matchen und auch Capture Groups zurückgeben1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...
[ "$foo" = bar ] && echo Yesreicht aus.Für Substring-Matching reichen
[und*-Globs meistens völlig aus. Etwa so:[ "$bar" = extra* ] && echo '$bar began with extra'. Bashs Regex-Dialekt ist primitiv, daher lohnt es sich kaum, ihn mit Mühe zu verwenden. Für komplexe Dinge sollte man andere Tools wiegrep,awkoderperlverwenden. Wenn man sich darauf versteift, alles in Bash zu erledigen, selbst komplexe Aufgaben, die mehr Wiederverwendbarkeit, Modularität und eingebaute Typen erfordern, nimmt der Nutzen rapide ab.