Ich habe ein Skript geschrieben, das während der Ausführung den Benutzer wechselt, und es mithilfe der Dateiumleitung zum Standard ausgeführt. So user-switch.sh
ist es ...
#!/bin/bash
whoami
sudo su -l root
whoami
Und wenn ich es ausführe, bash
erhalte ich das erwartete Verhalten.
$ 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 wird bash < user-switch.sh
eine andere Ausgabe ausgegeben als sh < user-switch.sh
?
Anmerkungen:
- passiert auf zwei verschiedenen Boxen mit Debian Jessie
Antwort1
Ein ähnliches Skript, ohne sudo
, aber mit ähnlichen Ergebnissen:
$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami
$ bash < script.sh
--whoami
$ dash < script.sh
itvirta
Bei bash
geht der Rest des Skripts als Eingabe an sed
, bei dash
interpretiert es die Shell.
Wird strace
auf diesen ausgeführt: dash
Liest einen Block des Skripts (hier acht KB, mehr als genug, um das gesamte Skript aufzunehmen) 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|...
Das bedeutet, dass sich der Dateihandle am Ende der Datei befindet und sed
keine Eingabe sieht. Der verbleibende Teil wird innerhalb 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 sucht dagegen zurück zum Ende des letzten Befehls:
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 aus einer Pipe kommt, wie hier:
$ cat script.sh | bash
Zurückspulen ist nicht möglich, da Pipes und Sockets nicht suchbar sind. In diesem Fall greift Bash auf das Lesen der Eingabe zurückjeweils ein Zeichenum zu viel Lesen zu vermeiden. (fd_to_buffered_stream()
Ininput.c
) Ein vollständiger Systemaufruf für jedes Byte ist grundsätzlich nicht sehr effektiv. In der Praxis glaube ich nicht, dass die Lesevorgänge einen großen Mehraufwand darstellen, verglichen beispielsweise mit der Tatsache, dass die meisten Dinge, die die Shell tut, das Starten 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 read
nur die erste neue Zeile gelesen wird, damit head
die nächste Zeile angezeigt wird. (Das funktioniert dash
auch mit .)
Mit anderen Worten: Bash unternimmt zusätzliche Anstrengungen, um das Lesen derselben Quelle für das Skript selbst und für daraus ausgeführte Befehle zu unterstützen. dash
tut dies nicht. Die in Debian verpackten zsh
, und ksh93
folgen Bash in dieser Hinsicht.
Antwort2
Die Shell liest das Skript von der Standardeingabe. Innerhalb des Skripts führen Sie einen Befehl aus, der auch die Standardeingabe lesen möchte. Welche Eingabe geht wohin?Man kann nicht zuverlässig sagen.
Shells arbeiten so, dass sie einen Abschnitt des Quellcodes lesen, ihn analysieren und, wenn sie einen vollständigen Befehl finden, diesen ausführen und dann mit dem Rest des Abschnitts und dem Rest der Datei fortfahren. Wenn der Abschnitt keinen vollständigen Befehl enthält (mit einem Abschlusszeichen am Ende – ich glaube, alle Shells lesen bis zum Ende einer Zeile), liest die Shell einen weiteren Abschnitt 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: Er hängt von der Blockgröße ab, die die Shell ausgewählt hat, und das kann nicht nur von der Shell und ihrer Version abhängen, sondern auch von der Maschinenkonfiguration, dem verfügbaren Speicher usw.
Bash sucht im Skript bis zum Ende des Quellcodes eines Befehls, 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 rückgängig gemacht werden.
Wenn Sie einem Befehl im Skript Eingaben übergeben möchten, müssen Sie dies explizit tun, normalerweise mit einemhier Dokument. (Für mehrzeilige Eingaben ist ein Here-Dokument normalerweise am praktischsten, aber jede andere Methode ist geeignet.) Der von Ihnen geschriebene Code funktioniert nur in einigen Shells und nur, wenn das Skript aus einer regulären Datei als Eingabe an die Shell übergeben wird. Wenn Sie erwartet haben, dass das zweite als whoami
Eingabe an übergeben würde sudo …
, denken Sie noch einmal darüber nach und bedenken Sie, dass das Skript meistens nicht an die Standardeingabe der Shell übergeben wird.
#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF
Beachten Sie, dass Sie in diesem Jahrzehnt verwenden können sudo -i root
. Laufen sudo su
ist ein Hack aus der Vergangenheit.