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 shHinweis 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 shreferenzierte]
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 zsheigenen 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_documentoder 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 $expansionkeinen 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 \newlineZeichen 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 $expansionsdie 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 \newlineund so lange fortgesetzt wird, bis eine Zeile nur noch das Trennzeichen und ein \newlineohne [: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 ndie 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)shder Shell wird eine reguläre Datei erstellt /tmp, ausgegeben, einem Deskriptor zugeordnet und dann die /tmpDatei gelöscht , sodass nur die Kopie des Deskriptors des Kernels übrig bleibt. dashVermeidet all diesen Unsinn und legt die Ausgabeverarbeitung einfach in einer anonymen |pipeDatei ab, die auf das Umleitungsziel abzielt <<.
Das macht dash's:
cmd <<HEREDOC
$(cmd)
HEREDOC
funktionell äquivalent zu bash's:
cmd <(cmd)
dashDie 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 catder Inhalt , was auch immer Datei die Shell erstellt FILEin ./file, macht es ausführbar, dann ausführen.
Der Kernel interpretiert die #!und Anrufe /usr/bin/shmit einem <read Dateideskriptor zugewiesen ./file.
shOrdnet eine Zeichenfolge in den Speicher zu, die aus dem zusammengesetzten Befehl besteht, der um beginnt _fn()und um endet SCRIPT.
Wenn _fnaufgerufen wird, shmuss zunächst dann auf einen Deskriptor in die Datei map interpret definiert <<SCRIPT...SCRIPT vor Aufruf _fnals spezielle integrierte Dienstprogramm , da SCRIPTist _fn‚s<input.
Die von printfund ausgegebenen Zeichenfolgen commandwerden an _fndie Standardausgabe >&1 (die an die aktuelle Shell umgeleitet wird) ARGV0oder ausgegeben $0.
catverkettet seinen <&0 Standardeingabedateideskriptor - SCRIPT- über das >abgeschnittene ARGV0Argument der aktuellen Shell , oder $0.
Vervollständigen Sie den bereits eingelesenen aktuellen zusammengesetzten Befehl mit sh execdem ausführbaren und neu geschriebenen $0Argument.
Von der Zeit , ./filebis die darin enthaltenen Anweisungen aufgerufen wird , festzulegen , dass es sollte execwieder d, shliest er in einer einzigen Verbindung Befehl zu einem Zeitpunkt , als es sie ausführt, während ./filesich 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 gitSynchronisierung. 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 -vist 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 execBefehl 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.
envnicht sowohl in / bin als auch in / usr / bin sind? Versuchen Sie eswhich -a envzu bestätigen.