Pfadunabhängiges Shebangs


20

Ich habe ein Skript, das auf zwei Computern ausgeführt werden soll. Diese beiden Computer erhalten Kopien des Skripts aus demselben Git-Repository. Das Skript muss mit dem richtigen Interpreter (zB zsh) ausgeführt werden.

Leider leben beide env und zshan unterschiedlichen Orten auf den lokalen und entfernten Rechnern:

Remote-Maschine

$ which env
/bin/env

$ which zsh
/some/long/path/to/the/right/zsh

Lokale Maschine

$ which env
/usr/bin/env

$which zsh
/usr/local/bin/zsh

Wie kann ich den shebang so einstellen, dass beim Ausführen des Skripts wie /path/to/script.shimmer das Zshverfügbare in verwendet wird PATH?


8
Sind Sie sicher, dass Sie envnicht sowohl in / bin als auch in / usr / bin sind? Versuchen Sie es which -a envzu bestätigen.
Grawity

Antworten:


22

Sie können dies nicht direkt durch shebang lösen, da shebang rein statisch ist. Was Sie tun können, ist, einen »am wenigsten verbreiteten Multiplikator« (aus Sicht der Shell) im shebang zu haben und Ihr Skript mit der richtigen Shell erneut auszuführen, wenn diese LCM nicht zsh ist. Mit anderen Worten: zshLassen Sie Ihr Skript von einer Shell ausführen, die auf allen Systemen vorhanden ist. Prüfen Sie, ob eine einzige Funktion vorhanden ist. Wenn der Test falsch ist, führen Sie das Skript execmit aus zsh, wo der Test erfolgreich ist, und fahren Sie einfach fort.

Ein einzigartiges Merkmal zshist beispielsweise das Vorhandensein der $ZSH_VERSIONVariablen:

#!/bin/sh -

[ -z "$ZSH_VERSION" ] && exec zsh - "$0" ${1+"$@"}

# zsh-specific stuff following here
echo "$ZSH_VERSION"

In diesem einfachen Fall wird das Skript zuerst von ausgeführt /bin/sh(alle Unix-ähnlichen Systeme nach den 80er Jahren verstehen #!und haben /bin/shentweder Bourne oder POSIX, aber unsere Syntax ist mit beiden kompatibel). Wenn $ZSH_VERSIONist nicht gesetzt, wird das Skript execs‘selbst durch zsh. Wenn $ZSH_VERSIONgesetzt ist (bzw. das Skript bereits durchlaufen ist zsh), wird der Test einfach übersprungen. Voilà.

Dies scheitert nur, wenn überhaupt zshnichts drin ist $PATH.

Edit: Um sicherzustellen , dass Sie nur execein zshan den üblichen Stellen, könnten Sie so etwas wie verwenden

for sh in /bin/zsh \
          /usr/bin/zsh \
          /usr/local/bin/zsh; do
    [ -x "$sh" ] && exec "$sh" - "$0" ${1+"$@"}
done

Dies könnte Sie davor bewahren, versehentlich execetwas in Ihr zu stecken, $PATHwas nicht das ist, was zshSie erwarten.


Ich habe dies für Eleganz empfohlen, aber es gibt im Prinzip Sicherheits- / Kompatibilitätsprobleme, wenn das erste zshin dem $PATHnicht das ist, was Sie erwarten.
Ryan Reich

Versuchte es anzusprechen. Die Frage ist, ob man sich immer sicher sein kann, ob eine zshBinärdatei an den Standardstandorten wirklich eine ist zsh.
Andreas Wiese

Sie können die! Bang-Linie dynamisch verfolgen. Sie können sich auch fragen zsh, wo es mit ist zsh -c 'whence zsh'. Einfacher kann man eben command -v zsh. Sehen Sie meine Antwort für, wie man dynamisch den Pfad #!bang.
mikeserv

1
Das Aufrufen der zshBinärdatei von $PATH, um den Pfad der zshBinärdatei zu ermitteln, würde das Problem nicht ganz lösen. @RyanReich hat darauf hingewiesen, oder? :)
Andreas Wiese

Nicht, wenn Sie sich zshselbst exekutieren , nein, ich denke nicht. Aber wenn Sie die resultierende Zeichenfolge in Ihren Hash-Knall einbetten und dann Ihr eigenes Skript ausführen, wissen Sie zumindest, was Sie erhalten. Trotzdem wäre es für einen einfacheren Test als für das Schleifen.
mikeserv

7

Ich habe jahrelang etwas Ähnliches verwendet, um mit den verschiedenen Speicherorten von Bash auf Systemen umzugehen, für deren Ausführung ich meine Skripte benötigte.

Bash / Zsh / etc.

#!/bin/sh

# Determines which OS and then reruns this script with approp. shell interp.
LIN_BASH="/bin/sh";
SOL_BASH="/packages/utilities/bin/sun5/bash";

OS_TYPE=`uname -s`;

if [ $OS_TYPE = "SunOS" ]; then
  $SOL_BASH -c "`sed -n '/\#\#\# BEGIN/,$p' $0`" $0 $*;
elif [ $OS_TYPE = "Linux" ]; then
  $LIN_BASH -c "`sed -n '/\#\#\# BEGIN/,$p' $0`" $0 $*;
else
  echo "UNKNOWN OS_TYPE, $OS_TYPE";
  exit 1;
fi
exit 0;

### BEGIN

...script goes here...

Das Obige kann leicht für eine Vielzahl von Dolmetschern angepasst werden. Das wichtigste ist, dass dieses Skript zunächst als Bourne-Shell ausgeführt wird. Es ruft sich dann ein zweites Mal rekursiv auf, parst jedoch alles über dem Kommentar ### BEGINmit sed.

Perl

Hier ist ein ähnlicher Trick für Perl:

#!/bin/sh

LIN_PERL="/usr/bin/perl";
SOL_PERL="/packages/perl/bin/perl";

OS_TYPE=`uname -s`;

if [ $OS_TYPE = "SunOS" ]; then
  eval 'exec $SOL_PERL -x -S $0 ${1+"$@"}';
elif [ $OS_TYPE = "Linux" ]; then
  eval 'exec $LIN_PERL -x -S $0 ${1+"$@"}';
else
  echo "$OS_TYPE: UNSUPORRTED OS/PLATFORM";
  exit 0;
fi
exit 0;

#!perl

...perl script goes here...

Diese Methode nutzt Perls Fähigkeit, wenn eine Datei ausgeführt wird, und analysiert diese Datei, wobei alle Zeilen vor der Zeile übersprungen werden #! perl.


Eine Reihe von Problemen gibt es: fehlende Anführungszeichen, Verwendung von $*anstelle von "$@", unbrauchbare Verwendung von eval, nicht gemeldeter Exit-Status (Sie haben execden ersten nicht verwendet ), fehlende -/ --, Fehlermeldungen nicht auf stderr, 0 Exit-Status für Fehlerzustände Verwenden Sie / bin / sh für LIN_BASH, ein nutzloses Semikolon (kosmetisch), und verwenden Sie Großbuchstaben für Nicht-Umgebungsvariablen. uname -sist wie uname(Uname ist für Unix Name). Sie haben vergessen zu erwähnen, dass das Überspringen durch die -xOption zu ausgelöst wird perl.
Stéphane Chazelas

4

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.

DAS ANERKENNUNGS- / AUSFÜHRUNGSVERHALTEN DER MUSCHEL IST STANDARDS-DEFINIERT

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.

A <<HERE-DOCUMENTIST EINE INLINE-DATEI

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:

  1. Ich zuerst catder Inhalt , was auch immer Datei die Shell erstellt FILEin ./file, macht es ausführbar, dann ausführen.

  2. Der Kernel interpretiert die #!und Anrufe /usr/bin/shmit einem <read Dateideskriptor zugewiesen ./file.

  3. shOrdnet eine Zeichenfolge in den Speicher zu, die aus dem zusammengesetzten Befehl besteht, der um beginnt _fn()und um endet SCRIPT.

  4. 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.

  5. Die von printfund ausgegebenen Zeichenfolgen commandwerden an _fndie Standardausgabe >&1 (die an die aktuelle Shell umgeleitet wird) ARGV0oder ausgegeben $0.

  6. catverkettet seinen <&0 Standardeingabedateideskriptor - SCRIPT- über das >abgeschnittene ARGV0Argument der aktuellen Shell , oder $0.

  7. 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:

  1. 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.

  2. Eine Möglichkeit, den Pfad zu bestimmen. command -vist ziemlich gut dafür.

  3. 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 ...

  4. 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.


Die Ablehnung ist, weil 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.
JW013

@ jw013 Offensichtlich würde mein Ansatz zum Installieren oder Aktualisieren eines ausführbaren Skripts nicht funktionieren, wenn die Person, die versucht, das Skript zu installieren oder zu aktualisieren, keine Berechtigungen zum Installieren oder Aktualisieren des Skripts hat. Genau das macht diese Antwort besser als jede andere Antwort hier - sie kann bei Bedarf eine genaue #! bang-Zeile bereitstellen und benötigt nur spezielle Berechtigungen, um dies beim ersten Aufruf zu tun - während der Installation. Auch hier werde ich nicht einfach beim Wort nehmen, dass sich selbst ändernder Code als schlechte Praxis erweist - bitte sehen Sie man commandfür eine widersprüchliche Meinung.
mikeserv

Bitte sehen Sie man commandfür eine widersprüchliche Meinung - nicht finden. Können Sie mich zu dem Abschnitt / Absatz weiterleiten, über den Sie gesprochen haben?
JW013

@ jw013 - Mein Fehler, es ist in man sh- Suche nach 'Befehl -v'. Ich wusste, dass es auf einer der manSeiten war, die ich mir neulich angesehen habe .
mikeserv

Ich nehme an, dies ist das command -vBeispiel, von dem Sie gesprochen haben man sh. Das ist ein normal aussehendes Installationsskript und kein selbstmodifizierendes . Selbst unabhängige Installer enthalten nur Eingaben vor der Änderung und geben ihre Änderungen an anderer Stelle aus. Sie schreiben sich nicht so um, wie Sie es empfehlen.
JW013

1

Hier ist eine Möglichkeit, ein selbstmodifizierendes Skript zu erstellen, mit dem das Problem behoben werden kann. Dieser Code sollte Ihrem eigentlichen Skript vorangestellt werden.

#!/bin/sh
# unpatched

PATH=`PATH=/bin:/usr/bin:$PATH getconf PATH`
if [ "`awk 'NR==2 {print $2;exit;}' $0`" = unpatched ]; then
  [ -z "`PATH=\`getconf PATH\`:/usr/local/bin:/some/long/path/to/the/right:$PATH command -v zsh`" ] && { echo "zsh not found"; exit 1; }
  cp -- "$0" "$0.org" || exit 1
  mv -- "$0" "$0.old" || exit 1
  (
    echo "#!`PATH=\`getconf PATH\`:$PATH command -v zsh`" 
    sed -n '/^##/,$p' $0.old
  ) > $0 || exit
  chmod +x $0
  rm $0.old
  sync
  exit
fi
## Original script starts here

Einige Kommentare:

  • Es sollte einmal von jemandem ausgeführt werden, der die Berechtigung hat, Dateien in dem Verzeichnis zu erstellen und zu löschen, in dem sich das Skript befindet.

  • Es wird nur die alte Bourne-Shell-Syntax verwendet, da trotz der weit verbreiteten Meinung /bin/shnicht garantiert wird, dass es sich um eine POSIX-Shell handelt, auch nicht bei POSIX-kompatiblen Betriebssystemen.

  • Der Pfad wurde auf POSIX-konform gesetzt, gefolgt von einer Liste möglicher zsh-Speicherorte, um zu vermeiden, dass ein "falscher" zsh ausgewählt wird.

  • Wenn aus irgendeinem Grund ein selbstmodifizierendes Skript nicht erwünscht ist, wäre es trivial, zwei Skripte anstelle eines zu verteilen, wobei das erste dasjenige ist, das Sie patchen möchten, und das zweite, das ich für die Verarbeitung des ersteren leicht modifiziert habe.


Der /bin/shPunkt ist gut - aber brauchen Sie in diesem Fall überhaupt einen vormodifizierten #!? Und ist es nicht awkgenauso wahrscheinlich, dass es falsch ist, wie es zshist?
mikeserv

@mikeserv Antwort aktualisiert, um die POSIX awk anzurufen. Der vormodifizierte shebang soll verhindern, dass das Skript von einer nicht mit bourne kompatiblen Shell interpretiert wird, falls es Ihre Anmeldeshell sein sollte.
Juli

Macht Sinn. Ich habe es hochgestuft, weil es funktioniert, sich an das Buch hält und ein fundiertes Verständnis der möglichen Shell-Umgebung / des Datei-Handlings zeigt - insbesondere der von Ihnen verwendeten Backup-Dateien, was alles ist, was GNUs sed -isowieso tut. Ich persönlich denke, das $PATHProblem, das in den Kommentaren zu einer anderen Antwort erwähnt wurde und das Sie so sicher wie möglich ansprechen, lässt sich besser lösen, indem Sie einfach und explizit Abhängigkeiten definieren und / oder strenge und explizite Tests durchführen - getconfvielleicht jetzt gefälscht, aber die Chancen stehen nahe null, genau wie für zshundawk.
mikeserv

@mikeserv, Skript geändert, um das Risiko zu verringern, eine gefälschte getconf aufzurufen.
Juli

$(getconf PATH)ist nicht Bourne. cp $0 $0.oldist zsh-Syntax. Das Bourne Äquivalent wäre cp "$0" "$0.old"wenn man sich wünschencp -- "$0" "$0.old"
Stéphane Chazelas
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.