Wie unterscheidet sich die Umleitung von Bash-Dateien auf Standard in von Shell (`sh`) unter Linux?

9

Ich schrieb ein Skript , das Benutzer schaltet beim Laufen, und ausgeführt es Dateiumleitung zu Standard zu verwenden. So user-switch.shist ...

#!/bin/bash

whoami
sudo su -l root
whoami

Und bashwenn ich es mit laufe, bekomme ich das Verhalten, das ich erwarte

$ bash < user-switch.sh
vagrant
root

Wenn ich das Skript jedoch mit ausführe sh, erhalte ich eine andere Ausgabe

$ sh < user-switch.sh 
vagrant
vagrant

Warum gibt es bash < user-switch.sheine andere Ausgabe als sh < user-switch.sh?

Anmerkungen:

  • passiert auf zwei verschiedenen Boxen, auf denen Debian Jessie läuft
popedotninja
quelle
1
Keine Antwort, aber in Bezug auf sudo su: unix.stackexchange.com/questions/218169/…
Kusalananda
1
ist / bin / sh = Dash auf Debian? Ich kann nicht auf RHEL repro, wo / bin / sh = bash
Jeff Schaller
1
/bin/shauf (meine Kopie von) Debian ist Dash, ja. Ich wusste bis jetzt nicht einmal, was das war. Ich bin bis jetzt bei Bash geblieben.
Popedotninja
1
Es wird auch nicht garantiert, dass das Bash- Verhalten durch eine dokumentierte Semantik funktioniert.
Charles Duffy
Jemand hat mich heute darauf hingewiesen, dass das Skript, das ich ausgeführt habe, gegen die Semantik verstößt, wie Bash verwendet werden soll. Es gab ein paar gute Antworten auf diese Frage, aber es ist erwähnenswert, dass man den Benutzer wahrscheinlich nicht während des Skripts wechseln sollte. Ich habe es ursprünglich user-switch.shin einem Jenkins-Job versucht und das Skript ausgeführt. Das Ausführen des gleichen Skripts außerhalb von Jenkins führte zu unterschiedlichen Ergebnissen, und ich griff auf die Dateiumleitung zurück, um das Verhalten zu erhalten, das ich in Jenkins gesehen habe.
Popedotninja

Antworten:

12

Ein ähnliches Skript ohne sudo, aber ähnliche Ergebnisse:

$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

Mit bashdem Rest des Skripts geht als Eingabe sed, mit dashihm, der Schale interpretiert.

Laufen straceauf diesen: dashLiest einen Block des Skripts (hier acht KB, mehr als genug, um das gesamte Skript zu speichern) und erzeugt dann sed:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 8192) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Dies bedeutet, dass sich das Dateihandle am Ende der Datei befindet und sedkeine Eingabe angezeigt wird . Der verbleibende Teil wird darin gepuffert dash. (Wenn das Skript länger als die Blockgröße von 8 kB wäre, würde der verbleibende Teil von gelesen sed.)

Bash hingegen sucht bis zum Ende des letzten Befehls zurück:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 36) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
...
lseek(0, -7, SEEK_CUR)                  = 29
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Wenn die Eingabe von einer Pipe stammt, wie hier:

$ cat script.sh | bash

Das Zurückspulen ist nicht möglich, da Rohre und Muffen nicht suchbar sind. In diesem Fall fällt Bash zurück , um die Eingabe zu lesen ein Zeichen zu einem Zeitpunkt zu vermeiden overreading. ( fd_to_buffered_stream()ininput.c ) Ein vollständiger Systemaufruf für jedes Byte ist im Prinzip nicht sehr effektiv. In der Praxis denke ich nicht, dass das Lesen ein großer Aufwand sein wird, z. B. im Vergleich zu der Tatsache, dass die meisten Dinge, die die Shell tut, das Laichen ganz neuer Prozesse beinhalten.

Eine ähnliche Situation ist folgende:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'

Die Subshell muss sicherstellen, dass readnur die erste neue headZeile gelesen wird, damit die nächste Zeile angezeigt wird . (Dies funktioniert auch mit dash.)


Mit anderen Worten, Bash unternimmt zusätzliche Anstrengungen, um das Lesen derselben Quelle für das Skript selbst und für von ihm ausgeführte Befehle zu unterstützen. dashnicht. Die zshund ksh93in Debian verpackten gehen mit Bash dazu.

ilkkachu
quelle
1
Ehrlich gesagt bin ich ein bisschen überrascht, dass das funktioniert.
Ilkkachu
danke für die tolle antwort! Ich werde beide Befehle stracespäter ausführen und die Ausgaben vergleichen. Ich hatte diesen Ansatz nicht gewählt.
Popedotninja
2
Ja, meine Reaktion auf die Frage zu lesen war überrascht , dass es tut Arbeit mit bash - ich hatte es abgeheftet als etwas , das definitiv nicht funktionieren würde. Ich denke ich hatte nur halb recht :)
hobbs
12

Die Shell liest das Skript von der Standardeingabe. Innerhalb des Skripts führen Sie einen Befehl aus, der auch die Standardeingabe lesen möchte. Welcher Eingang wird wohin gehen? Sie können nicht zuverlässig sagen .

Shells funktionieren so, dass sie einen Teil des Quellcodes lesen, analysieren und einen vollständigen Befehl ausführen, den Befehl ausführen und dann mit dem Rest des Teils und dem Rest der Datei fortfahren. Wenn der Block keinen vollständigen Befehl enthält (mit einem abschließenden Zeichen am Ende - ich denke, alle Shells lesen bis zum Ende einer Zeile), liest die Shell einen weiteren Block und so weiter.

Wenn ein Befehl im Skript versucht, aus demselben Dateideskriptor zu lesen, aus dem die Shell das Skript liest, findet der Befehl alles, was nach dem letzten gelesenen Block kommt. Dieser Speicherort ist unvorhersehbar: Dies hängt von der ausgewählten Blockgröße der Shell ab. Dies kann nicht nur von der Shell und ihrer Version abhängen, sondern auch von der Maschinenkonfiguration, dem verfügbaren Speicher usw.

Bash versucht, das Ende des Quellcodes eines Befehls im Skript zu erreichen, bevor der Befehl ausgeführt wird. Darauf können Sie sich nicht verlassen, nicht nur, weil andere Shells dies nicht tun, sondern auch, weil dies nur funktioniert, wenn die Shell aus einer regulären Datei liest. Wenn die Shell aus einer Pipe liest (z. B. ssh remote-host.example.com <local-script-file.sh), werden gelesene Daten gelesen und können nicht ungelesen werden.

Wenn Sie Eingaben an einen Befehl im Skript übergeben möchten, müssen Sie dies explizit tun, normalerweise mit einem Here-Dokument . (Ein Dokument hier ist normalerweise am bequemsten für mehrzeilige Eingaben, aber jede Methode reicht aus.) Der von Ihnen geschriebene Code funktioniert nur in wenigen Shells, nur wenn das Skript als Eingabe aus einer regulären Datei an die Shell übergeben wird. Wenn Sie erwartet haben, dass die Sekunde whoamials Eingabe an übergeben wird sudo …, denken Sie noch einmal darüber nach, dass das Skript die meiste Zeit nicht an die Standardeingabe der Shell übergeben wird.

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF

Beachten Sie, dass Sie dieses Jahrzehnt verwenden können sudo -i root. Laufen sudo suist ein Hack aus der Vergangenheit.

Gilles 'SO - hör auf böse zu sein'
quelle