#! / bin / sh vs #! / bin / bash für maximale Portabilität

36

Normalerweise arbeite ich mit Ubuntu LTS-Servern, von denen ich verstehe, dass sie mit Symlink verbunden /bin/shsind /bin/dash. Viele andere Distributionen, obwohl Symlink /bin/shzu /bin/bash.

Daraus ergibt sich, dass ein Skript, das #!/bin/shoben verwendet wird, möglicherweise nicht auf allen Servern auf die gleiche Weise ausgeführt wird.

Gibt es eine empfohlene Vorgehensweise für die Shell, die für Skripte verwendet werden soll, wenn eine maximale Portabilität dieser Skripte zwischen Servern gewünscht wird?

Cherouvim
quelle
Es gibt leichte Unterschiede zwischen den verschiedenen Schalen. Wenn die Portabilität für Sie am wichtigsten ist, verwenden Sie #!/bin/shnichts anderes als die von der Original-Shell bereitgestellte.
Thorbjørn Ravn Andersen

Antworten:

65

Es gibt ungefähr vier Portabilitätsstufen für Shell-Skripte (in Bezug auf die Shebang-Linie):

  1. Am portabelsten: Verwenden Sie einen #!/bin/shShebang und nur die im POSIX-Standard angegebene grundlegende Shell-Syntax . Dies sollte auf so ziemlich jedem POSIX / Unix / Linux-System funktionieren. (Nun, mit Ausnahme von Solaris 10 und früheren Versionen, die die echte Bourne-Legacy-Shell besaßen, die vor POSIX so nicht kompatibel war, wie /bin/sh.)

  2. Am zweithäufigsten tragbar: Verwenden Sie eine #!/bin/bash(oder #!/usr/bin/env bash) Shebang-Linie und halten Sie sich an die Funktionen von Bash V3. Dies funktioniert auf jedem System, auf dem bash installiert ist (am erwarteten Speicherort).

  3. Am dritthäufigsten tragbar: Verwenden Sie eine #!/bin/bash(oder #!/usr/bin/env bash) Shebang-Linie und verwenden Sie bash v4-Funktionen. Dies schlägt auf allen Systemen fehl, auf denen bash v3 installiert ist (z. B. macOS, das es aus Lizenzgründen verwenden muss).

  4. Am wenigsten portabel: Verwenden Sie einen #!/bin/shShebang und Bash-Erweiterungen für die POSIX-Shell-Syntax. Dies schlägt auf jedem System fehl, das etwas anderes als Bash für / bin / sh enthält (z. B. neuere Ubuntu-Versionen). Mach das niemals; es ist nicht nur ein Kompatibilitätsproblem, es ist einfach falsch. Leider ist es ein Fehler, den viele Leute machen.

Meine Empfehlung: Verwenden Sie die konservativste der ersten drei, die alle Shell-Funktionen bietet, die Sie für das Skript benötigen. Verwenden Sie für maximale Portabilität Option 1, aber meiner Erfahrung nach sind einige Bash-Funktionen (wie Arrays) hilfreich genug, um mit # 2 fortzufahren.

Das Schlimmste, was Sie tun können, ist # 4, wenn Sie den falschen Shebang verwenden. Wenn Sie sich nicht sicher sind, welche Funktionen POSIX-Basisfunktionen und welche Bash-Erweiterungen sind, halten Sie sich entweder an einen Bash-Schebang (Option 2) oder testen Sie das Skript gründlich mit einer sehr einfachen Shell (wie dash auf Ihren Ubuntu LTS-Servern). Das Ubuntu-Wiki hat eine gute Liste von Bashismen, auf die man achten sollte .

In der Unix- und Linux-Frage "Was bedeutet es, sh-kompatibel zu sein?" Finden Sie einige sehr gute Informationen zur Geschichte und zu den Unterschieden zwischen Shells. und die Stackoverflow-Frage "Unterschied zwischen sh und bash" .

Beachten Sie auch, dass die Shell nicht das einzige ist, das sich zwischen verschiedenen Systemen unterscheidet. Wenn Sie an Linux gewöhnt sind, sind Sie an die GNU-Befehle gewöhnt, die viele nicht standardmäßige Erweiterungen haben, die Sie auf anderen Unix-Systemen (z. B. bsd, macOS) möglicherweise nicht finden. Leider gibt es hier keine einfache Regel. Sie müssen lediglich den Variationsbereich für die von Ihnen verwendeten Befehle kennen.

Einer der übelsten Befehle in Bezug auf die Portabilität ist eine der grundlegendsten: echo. Jedes Mal, wenn Sie sie mit Optionen (z. B. echo -noder echo -e) oder mit Escapezeichen (Backslashes) in der zu druckenden Zeichenfolge verwenden, werden verschiedene Versionen unterschiedliche Aktionen ausführen. Verwenden Sie printfstattdessen, wenn Sie eine Zeichenfolge ohne Zeilenvorschub oder mit Escapezeichen in der Zeichenfolge drucken möchten (und erfahren Sie, wie dies funktioniert - es ist komplizierter als es echoist). Der psBefehl ist auch ein Durcheinander .

Ein weiterer allgemeiner Punkt, auf den Sie achten sollten, ist die jüngste / GNUish-Erweiterung der Befehlsoptionssyntax: Altes (Standard-) Befehlsformat: Auf den Befehl folgen Optionen (mit einem Bindestrich, und jede Option ist ein einzelner Buchstabe), gefolgt von Befehlsargumente. Neuere (und häufig nicht portierbare) Varianten enthalten lange Optionen (normalerweise eingeführt mit --), mit denen Optionen nach Argumenten folgen und --Optionen von Argumenten trennen können.

Gordon Davisson
quelle
12
Die vierte Option ist einfach eine falsche Idee. Bitte nicht benutzen.
Pabouk
3
@pabouk Ich stimme vollkommen zu, daher habe ich meine Antwort bearbeitet, um dies klarer zu machen.
Gordon Davisson
1
Ihre erste Aussage ist leicht irreführend. Der POSIX-Standard spezifiziert nichts darüber, dass das Äußere, das er verwendet, zu unspezifischem Verhalten führt. Darüber hinaus gibt POSIX nicht an, wo sich die Posix-Shell befinden muss, sondern nur ihren Namen ( sh), sodass /bin/shnicht garantiert wird, dass es sich um den richtigen Pfad handelt. Am portabelsten ist es dann, gar keinen shebang anzugeben oder den shebang an das verwendete Betriebssystem anzupassen.
jlliagre
2
Ich bin vor kurzem mit einem Skript von mir auf Platz 4 gefallen und konnte einfach nicht herausfinden, warum es nicht funktionierte. Immerhin funktionierten dieselben Befehle einwandfrei und taten genau das, was ich von ihnen wollte, als ich sie direkt in der Shell ausprobierte. Sobald ich geändert , #!/bin/shum #!/bin/bashperfekt aber funktionierte das Skript. (Zu meiner Verteidigung hatte sich dieses Drehbuch im Laufe der Zeit von einem, das wirklich nur Sh-Ismen brauchte, zu einem entwickelt, das sich auf bash-artiges Verhalten stützte.)
CVn
2
@ MichaelKjörling Die (echte) Bourne-Shell wird fast nie mit Linux-Distributionen gebündelt und ist sowieso nicht POSIX-kompatibel. Die POSIX-Standardshell wurde aus kshVerhalten erstellt, nicht aus Bourne. Was die meisten Linux-Distributionen nach dem FHS tun, /bin/shist eine symbolische Verknüpfung mit der Shell, die sie auswählen, um normalerweise bashoder in der Regel POSIX-Kompatibilität bereitzustellen dash.
jlliagre
5

In dem ./configureSkript, das die TXR-Sprache für die Erstellung vorbereitet, habe ich den folgenden Prolog geschrieben, um die Portabilität zu verbessern. Das Skript bootet sich selbst dann, wenn #!/bin/shes sich um eine nicht POSIX-konforme alte Bourne-Shell handelt. (Ich erstelle jede Version auf einer Solaris 10-VM).

#!/bin/sh

# use your own variable name instead of txr_shell;
# adjust to taste: search for your favorite shells

if test x$txr_shell = x ; then
  for shell in /bin/bash /usr/bin/bash /usr/xpg4/bin/sh ; do
    if test -x $shell ; then
       txr_shell=$shell
       break
    fi
  done
  if test x$txr_shell = x ; then
    echo "No known POSIX shell found: falling back on /bin/sh, which may not work"
    txr_shell=/bin/sh
  fi
  export txr_shell
  exec $txr_shell $0 ${@+"$@"}
fi

# rest of the script here, executing in upgraded shell

Die Idee dabei ist, dass wir eine bessere Shell als die finden, unter der wir laufen, und das Skript mit dieser Shell erneut ausführen. Die txr_shellUmgebungsvariable ist so festgelegt, dass das erneut ausgeführte Skript weiß, dass es sich um die erneut ausgeführte rekursive Instanz handelt.

(In meinem Skript wird die txr_shellVariable auch nachträglich für genau zwei Zwecke verwendet: Erstens wird sie als Teil einer Informationsnachricht in der Ausgabe des Skripts gedruckt. Zweitens wird sie als SHELLVariable in der installiert Makefile, damit diese verwendet makewird Shell auch zum Ausführen von Rezepten.)

Auf einem System mit /bin/shBindestrich können Sie sehen, dass die obige Logik /bin/bashdas Skript damit findet und erneut ausführt.

Auf einer Solaris 10-Box wird die /usr/xpg4/bin/shaktiviert, wenn kein Bash gefunden wird.

Der Prolog ist in einem konservativen Shell-Dialekt geschrieben, der testfür die Prüfung der Existenz von Dateien verwendet wird, und der ${@+"$@"}Trick, Argumente zu erweitern, die sich auf einige zerbrochene alte Shells beziehen (was einfach der Fall wäre, "$@"wenn wir uns in einer POSIX-konformen Shell befinden).

Kaz
quelle
Man würde den xHackery nicht brauchen, wenn das richtige Zitieren verwendet würde, da die Situationen, die diese Redewendung erforderten , jetzt veraltete Testaufrufe mit mehreren Tests verbinden -aoder diese -okombinieren.
Charles Duffy
@ CharlesDuffy In der Tat; Das test x$whatever, was ich dort tue, sieht aus wie eine Zwiebel im Lack. Wenn wir der kaputten alten Shell nicht vertrauen können, ist der letzte ${@+"$@"}Versuch sinnlos.
Kaz
2

Alle Variationen der Bourne-Shell-Sprache sind im Vergleich zu modernen Skriptsprachen wie Perl, Python, Ruby, node.js und sogar (wohl) Tcl objektiv schrecklich. Wenn Sie etwas kompliziertes tun müssen, werden Sie auf lange Sicht glücklicher sein, wenn Sie eines der oben genannten anstelle eines Shell-Skripts verwenden.

Der einzige Vorteil, den die Shell-Sprache gegenüber diesen neueren Sprachen noch hat, ist, dass etwas , das sich selbst nennt /bin/sh, auf allem, das vorgibt, Unix zu sein, garantiert existiert. Dies ist jedoch möglicherweise nicht einmal POSIX-konform. viele der Legacy - proprietäre Unix - Varianten froren die Sprache umgesetzt durch /bin/shund die Dienstprogramme in dem Standardpfad vor den Änderungen durch Unix95 gefordert (ja, Unix95, vor zwanzig Jahren und Zählen). Möglicherweise gibt es eine Reihe von Unix95- oder sogar POSIX.1-2001-Tools, wenn Sie Glück haben, die sich in einem Verzeichnis befinden, das sich nicht auf dem Standardpfad befindet (z. B. /usr/xpg4/bin), deren Existenz jedoch nicht garantiert ist.

Es ist jedoch wahrscheinlicher , dass die Grundlagen von Perl in einer willkürlich ausgewählten Unix-Installation vorhanden sind, als dies bei Bash der Fall ist. (Mit "den Grundlagen von Perl" meine ich, dass /usr/bin/perles eine , möglicherweise ziemlich alte, Version von Perl 5 gibt, und wenn Sie Glück haben, sind auch die Module verfügbar, die mit dieser Version des Interpreters geliefert wurden.)

Deshalb:

Wenn Sie etwas schreiben, das überall dort funktionieren muss, wo es sich um Unix handelt (z. B. ein "configure" -Skript), müssen Sie es verwenden #! /bin/sh, und Sie müssen keinerlei Erweiterungen verwenden. Heutzutage würde ich unter diesen Umständen eine POSIX.1-2001-kompatible Shell schreiben, aber ich wäre bereit, POSIXisms auszubessern, wenn jemand um Unterstützung für rostiges Eisen bittet.

Wenn Sie jedoch nicht überall etwas schreiben, das funktionieren muss, sollten Sie in dem Moment, in dem Sie versucht sind, Bashismus überhaupt zu verwenden, das Ganze stoppen und stattdessen in einer besseren Skriptsprache umschreiben. Dein zukünftiges Ich wird es dir danken.

(Wann ist es angebracht, Bash-Erweiterungen zu verwenden? Bei der ersten Bestellung: Nie. Bei der zweiten Bestellung: Nur zur Erweiterung der interaktiven Bash-Umgebung - z.

zwol
quelle
Neulich musste ich ein Drehbuch schreiben, das so etwas wie machte find ... -print0 |while IFS="" read -rd "" file; do ...; done |tar c --null -T -. Ohne bashisms ( read -d "") und GNU-Erweiterungen ( find -print0, tar --null) würde es nicht richtig funktionieren . Die Neuimplementierung dieser Zeile in Perl wäre viel länger und umständlicher. Dies ist die Art von Situation, in der die Verwendung von Bashismen und anderen Nicht-POSIX-Erweiterungen das Richtige ist.
Michau
@michau Je nachdem, was in diesen Ellipsen steht, ist es möglicherweise nicht mehr als Einzeiler zu qualifizieren, aber ich denke, Sie nehmen das "Umschreiben der gesamten Sache in einer besseren Skriptsprache" nicht so wörtlich, wie ich es gemeint habe. Mein Weg sieht perl -MFile::Find -e 'our @tarcmd = ("tar", "c"); find(sub { return unless ...; ...; push @tarcmd, $File::Find::name }, "."); exec @tarcmdungefähr so aus. Beachten Sie, dass Sie nicht nur keine Bashismen, sondern auch keine GNU-Tar-Erweiterungen benötigen. (Wenn die Befehlszeilenlänge ein Problem ist oder Sie möchten, dass Scan und Teer parallel ausgeführt werden, müssen Sie dies tun tar --null -T.)
zwol
Die Sache ist, findüber 50.000 Dateinamen in meinem Szenario zurückgegeben, so dass Ihr Skript wahrscheinlich das Argument Längenlimit erreicht. Eine korrekte Implementierung, die keine Nicht-POSIX-Erweiterungen verwendet, müsste die tar-Ausgabe inkrementell erstellen oder möglicherweise Archive :: Tar :: Stream im Perl-Code verwenden. Natürlich kann es gemacht werden, aber in diesem Fall ist das Bash-Skript viel schneller zu schreiben und hat weniger Boilerplate und daher weniger Platz für Bugs.
Michau
BTW, konnte ich Perl statt Bash in einer schnellen und präzisen Art und Weise benutzen find ... -print0 |perl -0ne '... print ...' |tar c --null -T -. Aber die Fragen sind: 1) Ich benutze find -print0und tar --nulltrotzdem, was ist der Sinn des Vermeidens des Bashismus read -d "", der genau die gleiche Sache ist (eine GNU-Erweiterung für den Umgang mit Null-Separatoren, die im Gegensatz zu Perl eines Tages zu einer POSIX-Sache werden kann )? 2) Ist Perl wirklich weiter verbreitet als Bash? Ich habe kürzlich Docker-Images verwendet, und viele von ihnen haben Bash, aber kein Perl. Es ist wahrscheinlicher, dass dort neue Skripte ausgeführt werden als auf alten Unices.
Michau