HINWEIS: @ jw013 erhebt in den folgenden Kommentaren den folgenden nicht unterstützten Einwand:
Die Ablehnung ist, dass selbstmodifizierender Code im Allgemeinen als schlechte Praxis angesehen wird. Früher war es eine clevere Methode, bedingte Verzweigungen zu reduzieren und die Leistung zu verbessern. Heute überwiegen die Sicherheitsrisiken die Vorteile. Ihr Ansatz würde nicht funktionieren, wenn der Benutzer, der das Skript ausgeführt hat, keine Schreibrechte für das Skript hätte.
Ich antwortete auf seine Sicherheit Einwände mit dem Hinweis darauf , dass keine speziellen Berechtigungen werden nur einmal benötigt pro Installation / Update Aktion , um zu installieren / aktualisieren die selbst installierende Skript - was ich persönlich würde ziemlich sicher nennen. Ich wies ihn auch auf einen man sh
Hinweis darauf hin, ähnliche Ziele mit ähnlichen Mitteln zu erreichen. Ich habe mich zu diesem Zeitpunkt nicht darum gekümmert , darauf hinzuweisen, dass Sicherheitslücken oder sonstige allgemein nicht empfohlene Praktiken , die in meiner Antwort vertreten sein könnten oder nicht, eher in der Frage selbst als in meiner Antwort darauf begründet waren:
Wie kann ich den shebang so einrichten, dass beim Ausführen des Skripts als /path/to/script.sh immer das in PATH verfügbare Zsh verwendet wird?
Nicht zufrieden, fuhr @ jw013 fort, Einwände zu erheben, indem er sein noch nicht unterstütztes Argument mit mindestens ein paar falschen Aussagen vorbrachte :
Sie verwenden eine einzelne Datei, nicht zwei Dateien. Das [ man sh
referenzierte]
Paket hat eine Datei, die eine andere Datei modifiziert. Sie haben eine Datei, die sich selbst ändert. Es gibt einen deutlichen Unterschied zwischen diesen beiden Fällen. Eine Datei, die Eingaben entgegennimmt und Ausgaben erzeugt, ist in Ordnung. Eine ausführbare Datei, die sich während der Ausführung von selbst ändert, ist im Allgemeinen eine schlechte Idee. Das Beispiel, auf das Sie verwiesen haben, macht das nicht.
An erster Stelle:
DER EINZIGE AUSFÜHRBARE CODE IN JEDEM AUSFÜHRBAREN SHELL-SCRIPT IST #!
DAS SELBST
(obwohl selbst #!
ist offiziell nicht spezifiziert )
{ cat >|./file
chmod +x ./file
./file
} <<-\FILE
#!/usr/bin/sh
{ ${l=lsof -p} $$
echo "$l \$$" | sh
} | grep \
"COMMAND\|^..*sh\| [0-9]*[wru] "
#END
FILE
##OUTPUT
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
file 8900 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash
file 8900 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33)
file 8900 mikeserv 0r REG 0,35 108 15496912 /tmp/zshUTTARQ (deleted)
file 8900 mikeserv 1u CHR 136,2 0t0 5 /dev/pts/2
file 8900 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2
file 8900 mikeserv 255r REG 0,33 108 2134129 /home/mikeserv/file
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sh 8906 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash
sh 8906 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33)
sh 8906 mikeserv 0r FIFO 0,8 0t0 15500515 pipe
sh 8906 mikeserv 1w FIFO 0,8 0t0 15500514 pipe
sh 8906 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2
{ sed -i \
'1c#!/home/mikeserv/file' ./file
./file
sh -c './file ; echo'
grep '#!' ./file
}
##OUTPUT
zsh: too many levels of symbolic links: ./file
sh: ./file: /home/mikeserv/file: bad interpreter: Too many levels of symbolic links
#!/home/mikeserv/file
Ein Shellskript ist nur eine Textdatei. Damit es überhaupt eine Wirkung hat, muss es von einer anderen ausführbaren Datei gelesen und die Anweisungen von dieser anderen ausführbaren Datei interpretiert werden, bevor die andere ausführbare Datei schließlich die Interpretation des Befehls ausführt Shell-Skript. Es ist nicht möglich, dass die Ausführung einer Shell-Skriptdatei weniger als zwei Dateien umfasst. Es gibt eine mögliche Ausnahme im zsh
eigenen Compiler, aber damit habe ich wenig Erfahrung und es wird hier in keiner Weise dargestellt.
Der Hashbang eines Shell-Skripts muss auf den vorgesehenen Interpreter verweisen oder als irrelevant verworfen werden.
Die Shell verfügt über zwei grundlegende Modi zum Analysieren und Interpretieren ihrer Eingabe: Entweder definiert ihre aktuelle Eingabe ein <<here_document
oder sie definiert ein { ( command |&&|| list ) ; } &
- mit anderen Worten, die Shell interpretiert ein Token entweder als Begrenzer für einen Befehl, den sie ausführen soll, sobald sie ihn gelesen hat in oder als Anweisungen zum Erstellen einer Datei und Zuordnen zu einem Dateideskriptor für einen anderen Befehl. Das ist es.
Bei der Interpretation von Befehlen zur Ausführung der Shell werden Token für eine Reihe reservierter Wörter begrenzt. Wenn die Schale eine Öffnung stößt Token es muss weiterhin in einer Befehlsliste lesen , bis entweder die Liste durch eine Schließ Token wie eine neue Zeile begrenzt wird - falls anwendbar - oder das Schließen wie Token })
für die ({
vor der Ausführung.
Die Shell unterscheidet zwischen einem einfachen Befehl und einem zusammengesetzten Befehl. Der zusammengesetzte Befehl ist der Befehlssatz, der vor der Ausführung eingelesen werden muss, aber die Shell führt $expansion
keinen ihrer einfachen Befehle aus, bis sie jeden einzeln ausführt.
Im folgenden Beispiel begrenzen die ;semicolon
reservierten Wörter einzelne einfache Befehle, wohingegen das nicht maskierte \newline
Zeichen zwischen den beiden zusammengesetzten Befehlen liegt:
{ cat >|./file
chmod +x ./file
./file
} <<-\FILE
#!/usr/bin/sh
echo "simple command ${sc=1}" ;\
: > $0 ;\
echo "simple command $((sc+2))" ;\
sh -c "./file && echo hooray"
sh -c "./file && echo hooray"
#END
FILE
##OUTPUT
simple command 1
simple command 3
hooray
Das ist eine Vereinfachung der Richtlinien. Es wird viel komplizierter, wenn man Shell-Builtins, Subshells, die aktuelle Umgebung usw. betrachtet, aber für meine Zwecke hier ist es genug.
Und sprechen Einbauten und Befehlslisten, eine function() { declaration ; }
ist lediglich ein Mittel , um eine Zuordnung Verbindung Befehl zu einem einfachen Befehl. Die Shell darf $expansions
die Deklarationsanweisung selbst nicht ausführen, <<redirections>
sondern muss stattdessen die Definition als einzelne, wörtliche Zeichenfolge speichern und als spezielle Shell ausführen, die beim Aufruf integriert ist.
Daher wird eine in einem ausführbaren Shell-Skript deklarierte Shell-Funktion im Speicher der interpretierenden Shell in ihrer Literal-String-Form gespeichert - nicht erweitert, um hier angehängte Dokumente als Eingabe einzuschließen - und jedes Mal, wenn sie als Shell-Builded aufgerufen wird, unabhängig von ihrer Quelldatei ausgeführt. in so lange, wie die aktuelle Umgebung der Shell dauert.
Die Umleitungsoperatoren <<
und <<-
beide ermöglichen die Umleitung von Zeilen in einer Shell-Eingabedatei, die als Here-Document bezeichnet wird, zur Eingabe eines Befehls.
Das Here-Dokument wird als ein einzelnes Wort behandelt, das nach dem nächsten beginnt \newline
und so lange fortgesetzt wird, bis eine Zeile nur noch das Trennzeichen und ein \newline
ohne [:blank:]
s dazwischen enthält. Dann startet das nächste Here-Dokument , falls es eines gibt. Das Format ist wie folgt:
[n]<<word
here-document
delimiter
... wobei das optionale n
die Dateideskriptornummer darstellt. Wenn die Nummer weggelassen wird, bezieht sich das Dokument auf die Standardeingabe (Dateideskriptor 0).
for shell in dash zsh bash sh ; do sudo $shell -c '
{ readlink /proc/self/fd/3
cat <&3
} 3<<-FILE
$0
FILE
' ; done
#OUTPUT
pipe:[16582351]
dash
/tmp/zshqs0lKX (deleted)
zsh
/tmp/sh-thd-955082504 (deleted)
bash
/tmp/sh-thd-955082612 (deleted)
sh
Siehst du? Für jede Shell über der Shell wird eine Datei erstellt und einem Dateideskriptor zugeordnet. In zsh, (ba)sh
der Shell wird eine reguläre Datei erstellt /tmp
, ausgegeben, einem Deskriptor zugeordnet und dann die /tmp
Datei gelöscht , sodass nur die Kopie des Deskriptors des Kernels übrig bleibt. dash
Vermeidet all diesen Unsinn und legt die Ausgabeverarbeitung einfach in einer anonymen |pipe
Datei ab, die auf das Umleitungsziel abzielt <<
.
Das macht dash
's:
cmd <<HEREDOC
$(cmd)
HEREDOC
funktionell äquivalent zu bash
's:
cmd <(cmd)
dash
Die Implementierung von while ist zumindest POSIX-fähig.
Das macht MEHRERE FILES
Also in der Antwort unten, wenn ich es tue:
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_fn() { printf '#!' ; command -v zsh ; cat
} <<SCRIPT >$0
[SCRIPT BODY]
SCRIPT
_fn ; exec $0
FILE
Folgendes passiert:
Ich zuerst cat
der Inhalt , was auch immer Datei die Shell erstellt FILE
in ./file
, macht es ausführbar, dann ausführen.
Der Kernel interpretiert die #!
und Anrufe /usr/bin/sh
mit einem <read
Dateideskriptor zugewiesen ./file
.
sh
Ordnet eine Zeichenfolge in den Speicher zu, die aus dem zusammengesetzten Befehl besteht, der um beginnt _fn()
und um endet SCRIPT
.
Wenn _fn
aufgerufen wird, sh
muss zunächst dann auf einen Deskriptor in die Datei map interpret definiert <<SCRIPT...SCRIPT
vor Aufruf _fn
als spezielle integrierte Dienstprogramm , da SCRIPT
ist _fn
‚s<input.
Die von printf
und ausgegebenen Zeichenfolgen command
werden an _fn
die Standardausgabe >&1
(die an die aktuelle Shell umgeleitet wird) ARGV0
oder ausgegeben $0
.
cat
verkettet seinen <&0
Standardeingabedateideskriptor - SCRIPT
- über das >
abgeschnittene ARGV0
Argument der aktuellen Shell , oder $0
.
Vervollständigen Sie den bereits eingelesenen aktuellen zusammengesetzten Befehl mit sh exec
dem ausführbaren und neu geschriebenen $0
Argument.
Von der Zeit , ./file
bis die darin enthaltenen Anweisungen aufgerufen wird , festzulegen , dass es sollte exec
wieder d, sh
liest er in einer einzigen Verbindung Befehl zu einem Zeitpunkt , als es sie ausführt, während ./file
sich gar nichts tut , außer glücklich seine neue Inhalte. Die Dateien, die tatsächlich in Arbeit sind, sind/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
DANKE NACH ALLEN
Wenn @ jw013 Folgendes angibt:
Eine Datei, die Eingaben entgegennimmt und Ausgaben erzeugt, ist in Ordnung ...
... inmitten seiner fehlerhaften Kritik an dieser Antwort billigt er tatsächlich unabsichtlich die einzige Methode, die hier angewendet wird, was im Grunde genommen nur zu Folgendem führt:
cat <new_file >old_file
ANTWORTEN
Alle Antworten hier sind gut, aber keine von ihnen ist völlig richtig. Jeder scheint zu behaupten, dass Sie nicht dynamisch und dauerhaft auf Ihrem Weg sein können #!bang
. Hier ist eine Demonstration der Einrichtung eines wegunabhängigen Schebangs:
DEMO
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_rewrite_me() { printf '#!' ; command -v zsh
${out+cat} ; ${out+:} . /dev/fd/0 >&2
} <<\SCRIPT >|${out-/dev/null}
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
SCRIPT
_rewrite_me ; out=$0 _rewrite_me ; exec $0
FILE
AUSGABE
$0 : ./file
lines : 13
!bang : #!/usr/bin/sh
shell : /usr/bin/sh
1 > #!/usr/bin/sh
2 > _rewrite_me() { printf '#!' ; command -v zsh
...
12 > SCRIPT
13 > _rewrite_me ; out=$0 _rewrite_me ; exec $0
$0 : /home/mikeserv/file
lines : 8
!bang : #!/usr/bin/zsh
shell : /usr/bin/zsh
1 > #!/usr/bin/zsh
2 > printf "
...
7 > sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
8 > sed -e 'N;s/\n/ >\t/' -e 4a\\...
Siehst du? Wir lassen das Skript einfach selbst überschreiben. Und es passiert immer nur einmal nach einer git
Synchronisierung. Von diesem Punkt an hat es den richtigen Pfad in der #! Knalllinie.
Jetzt ist fast alles da oben nur noch Flusen. Um dies sicher zu machen, benötigen Sie:
Eine oben definierte und unten aufgerufene Funktion, die das Schreiben übernimmt. Auf diese Weise speichern wir alles, was wir brauchen, im Speicher und stellen sicher, dass die gesamte Datei eingelesen wird, bevor wir anfangen, darüber zu schreiben.
Eine Möglichkeit, den Pfad zu bestimmen. command -v
ist ziemlich gut dafür.
Heredocs helfen wirklich, weil es sich um aktuelle Dateien handelt. Sie werden Ihr Skript in der Zwischenzeit speichern. Sie können auch Zeichenfolgen verwenden, aber ...
Sie müssen sicherstellen, dass die Shell den Befehl einliest, der Ihr Skript in derselben Befehlsliste überschreibt wie den Befehl, der es ausführt.
Aussehen:
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_rewrite_me() { printf '#!' ; command -v zsh
${out+cat} ; ${out+:} . /dev/fd/0 >&2
} <<\SCRIPT >|${out-/dev/null}
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
SCRIPT
_rewrite_me ; out=$0 _rewrite_me
exec $0
FILE
Beachten Sie, dass ich den exec
Befehl nur eine Zeile nach unten verschoben habe . Jetzt:
#OUTPUT
$0 : ./file
lines : 14
!bang : #!/usr/bin/sh
shell : /usr/bin/sh
1 > #!/usr/bin/sh
2 > _rewrite_me() { printf '#!' ; command -v zsh
...
13 > _rewrite_me ; out=$0 _rewrite_me
14 > exec $0
Ich bekomme die zweite Hälfte der Ausgabe nicht, weil das Skript den nächsten Befehl nicht einlesen kann. Trotzdem, weil der einzige fehlende Befehl der letzte war:
cat ./file
#!/usr/bin/zsh
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
Das Drehbuch ist so durchgekommen, wie es hätte sein sollen - hauptsächlich, weil alles im Heredoc enthalten war -, aber wenn Sie es nicht richtig planen, können Sie Ihren Dateistream abschneiden.
env
nicht sowohl in / bin als auch in / usr / bin sind? Versuchen Sie eswhich -a env
zu bestätigen.