Wählen Sie den Interpreter nach dem Start des Skripts aus, z. B. wenn / else in Hashbang enthalten ist

16

Gibt es eine Möglichkeit, den Interpreter, der ein Skript ausführt, dynamisch auszuwählen? Ich habe ein Skript, das auf zwei verschiedenen Systemen ausgeführt wird, und der Interpreter, den ich verwenden möchte, befindet sich an verschiedenen Orten auf den beiden Systemen. Am Ende muss ich die Hashbang-Linie jedes Mal ändern, wenn ich umschalte. Ich möchte etwas tun, das dem logisch entspricht (mir ist klar, dass dieses genaue Konstrukt unmöglich ist):

if running on system A:
    #!/path/to/python/on/systemA
elif running on system B:
    #!/path/on/systemB

#Rest of script goes here

Oder noch besser wäre dies, damit es versucht, den ersten Interpreter zu verwenden, und wenn es den zweiten nicht findet:

try:
    #!/path/to/python/on/systemA
except: 
    #!path/on/systemB

#Rest of script goes here

Offensichtlich kann ich stattdessen führen Sie es als /path/to/python/on/systemA myscript.py oder /path/on/systemB myscript.py je nachdem , wo ich bin, aber ich habe tatsächlich einen Wrapper - Skript , das gestartet wird myscript.py, so würde Ich mag den Pfad zum Python - Interpreter spezifizieren programmatisch eher als von Hand.

dkv
quelle
3
Den Rest des Skripts als Datei an den Interpreter weiterzugeben, ohne dass es zu Problemen kommt, und die ifBedingung zu verwenden, ist für Sie keine Option? like,if something; then /bin/sh restofscript.sh elif...
mazs
Es ist eine Option, die ich auch in Betracht gezogen habe, aber etwas chaotischer, als ich es gerne hätte. Da Logik in der Hashbang-Zeile unmöglich ist, denke ich, dass ich diesen Weg in der Tat gehen werde.
dkv
Ich mag die Vielzahl der Antworten, die diese Frage generiert hat.
Oskar Skog

Antworten:

27

Nein, das geht nicht. Die beiden Zeichen müssen #!unbedingt die ersten beiden Zeichen in der Datei sein (wie würden Sie angeben, was die if-Anweisung überhaupt interpretiert hat?). Dies ist die "magische Zahl", die die exec()Funktionsfamilie erkennt, wenn sie feststellt, ob es sich bei einer auszuführenden Datei um ein Skript (das einen Interpreter benötigt) oder eine Binärdatei handelt (was nicht der Fall ist).

Das Format der Shebang-Linie ist ziemlich streng. Es muss einen absoluten Pfad zu einem Interpreten und höchstens ein Argument dafür haben.

Was Sie tun können , ist zu verwenden env:

#!/usr/bin/env interpreter

Nun ist der Weg envist in der Regel /usr/bin/env , aber technisch , dass keine Garantie ist.

Dadurch können Sie das anpassen PATHUmgebungsvariable auf jedem System , so dass interpreter(es sein bash, pythonoder perloder was auch immer Sie haben) gefunden wird.

Ein Nachteil dieses Ansatzes ist, dass es unmöglich ist, ein Argument portabel an den Interpreter weiterzuleiten.

Das bedeutet, dass

#!/usr/bin/env awk -f

und

#!/usr/bin/env sed -f

ist unwahrscheinlich, auf einigen Systemen zu arbeiten.

Ein anderer offensichtlicher Ansatz ist die Verwendung von GNU-Autotools (oder einem einfacheren Template-System), um den Interpreter zu finden und den korrekten Pfad in die Datei in einem ./configureSchritt einzufügen , der bei der Installation des Skripts auf jedem System ausgeführt wird.

Man könnte auch das Skript mit einem expliziten Interpreter ausführen, aber das ist offensichtlich das, was Sie vermeiden wollen:

$ sed -f script.sed
Kusalananda
quelle
Richtig, mir ist klar, dass #!das am Anfang stehen muss, da es nicht die Shell ist, die diese Zeile verarbeitet. Ich habe mich gefragt, ob es eine Möglichkeit gibt, Logik in die Hashbang-Zeile einzufügen, die der Option if / else entspricht. Ich hatte auch gehofft, dass PATHich nicht mit meinem herumalbern sollte, aber ich denke, das sind meine einzigen Möglichkeiten.
dkv
1
Wenn Sie #!/usr/bin/awkas verwenden , können Sie genau ein Argument angeben #!/usr/bin/awk -f. Wenn die Binärdatei, auf die Sie verweisen env, das Argument ist, nach der Sie envsuchen möchten, wie in #!/usr/bin/env awk.
DopeGhoti
2
@dkv Ist es nicht. Es verwendet einen Interpreter mit zwei Argumenten und funktioniert möglicherweise auf einigen Systemen, aber definitiv nicht auf allen.
Kusalananda
3
Unter Linux läuft @dkv /usr/bin/envmit dem einzigen Argument awk -f.
ilkkachu
1
@Kusalananda, nein, das war der Punkt. Wenn Sie ein Skript haben, das foo.awkmit der Hashbang-Zeile aufgerufen wurde, #!/usr/bin/env awk -fund es ./foo.awkdann unter Linux aufrufen , werden envdie beiden Parameter awk -fund angezeigt ./foo.awk. Es geht eigentlich darum, /usr/bin/awk -fmit einem Leerzeichen (etc.) zu suchen .
ilkkachu
27

Sie können jederzeit ein Wrapper-Skript erstellen, um den richtigen Interpreter für das aktuelle Programm zu finden:

#!/bin/bash
if something ; then
    interpreter=this
    script=/some/path/to/program.real
    flags=()
else
    interpreter=that
    script=/other/path/to/program.real
    flags=(-x -y)
fi
exec "$interpreter" "${flags[@]}" "$script" "$@"

Speichern Sie den Wrapper PATHunter programund legen Sie das eigentliche Programm beiseite oder tragen Sie einen anderen Namen ein.

Ich habe #!/bin/bashim Hashbang wegen des flagsArrays verwendet. Wenn Sie keine variable Anzahl von Flags oder dergleichen speichern müssen und darauf verzichten können, sollte das Skript portabel funktionieren #!/bin/sh.

ilkkachu
quelle
2
Ich habe auch gesehen exec "$interpreter" "${flags[@]}" "$script" "$@", wie man den Prozessbaum sauberer hält. Es gibt auch den Exit-Code weiter.
Rrauenza
@rrauenza, ah ja, natürlich mit exec.
ilkkachu
1
Wäre nicht #!/bin/shbesser als #!/bin/bash? Selbst wenn /bin/shes sich um einen Symlink zu einer anderen Shell handelt, sollte er auf den meisten (wenn nicht allen) * nix-Systemen vorhanden sein. Außerdem würde er den Skriptautor dazu zwingen, ein portierbares Skript zu erstellen, anstatt in Bashismen zu verfallen.
Sergiy Kolodyazhnyy
@SergiyKolodyazhnyy, heh, ich dachte darüber nach, das früher zu erwähnen, tat es aber dann nicht. Das verwendete Array flagsist keine Standardfunktion, aber es ist nützlich genug, um eine variable Anzahl von Flags zu speichern, sodass ich mich entschlossen habe, es beizubehalten.
ilkkachu
Oder nutzen Sie / bin / sh und rufen Sie einfach den Interpreter direkt in jedem Zweig: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". Sie können auch eine Fehlerprüfung für Ausführungsfehler hinzufügen.
jrw32982 unterstützt Monica
11

Sie können auch einen Polyglot schreiben (zwei Sprachen kombinieren). / bin / sh ist garantiert vorhanden.

Dies hat den Nachteil von hässlichem Code und möglicherweise können einige /bin/shs verwirrt werden. Sie kann jedoch verwendet werden, wenn envsie nicht existiert oder woanders als / usr / bin / env existiert. Es kann auch verwendet werden, wenn Sie eine ziemlich ausgefallene Auswahl treffen möchten.

Der erste Teil des Skripts bestimmt, welcher Interpreter verwendet werden soll, wenn / bin / sh als Interpreter ausgeführt wird. Er wird jedoch ignoriert, wenn er vom richtigen Interpreter ausgeführt wird. Verwenden Sie execdiese Option , um zu verhindern, dass die Shell mehr als den ersten Teil ausführt.

Python-Beispiel:

#!/bin/sh
'''
' 2>/dev/null
# Python thinks this is a string, docstring unfortunately.
# The shell has just tried running the <newline> program.
find_best_python ()
{
    for candidate in pypy3 pypy python3 python; do
        if [ -n "$(which $candidate)" ]; then
            echo $candidate
            return
        fi
    done
    echo "Can't find any Python" >/dev/stderr
    exit 1
}
interpreter="$(find_best_python)"   # Replace with something fancier.
# Run the rest of the script
exec "$interpreter" "$0" "$@"
'''
Oskar Skog
quelle
3
Ich denke, ich habe eines davon schon einmal gesehen, aber die Idee ist immer noch genauso schrecklich ... Aber Sie möchten wahrscheinlich exec "$interpreter" "$0" "$@"den Namen des Skripts selbst auch dem eigentlichen Interpreter mitteilen. (Und dann hoffe, niemand hat beim Aufstellen gelogen $0.)
ilkkachu
6
Scala unterstützt tatsächlich mehrsprachige Skripte in seiner Syntax: Beginnt ein Scala-Skript mit #!, ignoriert Scala alles bis auf eine Übereinstimmung !#. Auf diese Weise können Sie beliebig komplexen Skriptcode in einer beliebigen Sprache und anschließend execdie Scala-Ausführungsengine mit dem Skript einfügen .
Jörg W Mittag
1
@ Jörg W Mittag: +1 für Scala
jrw32982 unterstützt Monica
2

Ich bevorzuge die Antworten von Kusalananda und Ilkkachu, aber hier ist eine alternative Antwort, die direkter das tut, wonach die Frage gestellt wurde, einfach weil sie gestellt wurde.

#!/usr/bin/ruby -e exec "non-existing-interpreter", ARGV[0] rescue exec "python", ARGV[0]

if True:
  print("hello world!")

Beachten Sie, dass Sie dies nur tun können, wenn der Interpreter das Schreiben von Code im ersten Argument zulässt. Hier -eund alles danach wird wörtlich als Argument für Ruby genommen. Soweit ich das beurteilen kann, können Sie Bash nicht für den Shebang-Code verwenden, da bash -cder Code in einem separaten Argument stehen muss.

Ich habe versucht, dasselbe mit Python für Shebang-Code zu tun:

#!/usr/bin/python -cexec("import sys,os\ntry: os.execlp('non-existing-interpreter', 'non-existing-interpreter', sys.argv[1])\nexcept: os.execlp('ruby', 'ruby', sys.argv[1])")

if true
  puts "hello world!"
end

aber es stellt sich als zu lang heraus und Linux (zumindest auf meinem Computer) schneidet den Shebang auf 127 Zeichen ab. Bitte entschuldigen Sie die Verwendung von exec, um Zeilenumbrüche einzufügen, da Python keine Try-Excepts oder imports ohne Zeilenumbrüche zulässt.

Ich bin mir nicht sicher, wie portabel das ist, und ich würde es nicht mit Code machen, der für die Verteilung gedacht ist. Trotzdem ist es machbar. Vielleicht findet es jemand nützlich für schnelles und schmutziges Debuggen oder so.

JoL
quelle
2

Dadurch wird zwar der Interpreter im Shell-Skript nicht ausgewählt (es wird pro Computer ausgewählt), es ist jedoch eine einfachere Alternative, wenn Sie Administratorzugriff auf alle Computer haben, auf denen Sie das Skript ausführen möchten.

Erstellen Sie einen Symlink (oder einen Hardlink, falls gewünscht), um auf den gewünschten Interpreterpfad zu verweisen. Auf meinem System befinden sich Perl und Python beispielsweise in / usr / bin:

cd /bin
ln -s /usr/bin/perl perl
ln -s /usr/bin/python python

würde einen Symlink erstellen, der es dem Hashbang ermöglicht, nach / bin / perl usw. aufzulösen. Dadurch bleibt die Möglichkeit erhalten, Parameter auch an die Skripte zu übergeben.

rauben
quelle
1
+1 Das ist so einfach. Wie Sie bemerken, beantwortet es die Frage nicht ganz, aber es scheint genau das zu tun, was das OP will. Obwohl ich denke, dass die Verwendung von env den Root-Zugriff auf jedem Computerproblem umgeht.
Joe
0

Ich war heute mit einem ähnlichen Problem konfrontiert ( python3das auf eine auf einem System zu alte Version von Python hinwies) und hatte einen anderen Ansatz als den hier beschriebenen: Verwenden Sie die "falsche" Version von Python zu booten in die "richtige". Die Einschränkung besteht darin, dass einige Python-Versionen zuverlässig erreichbar sein müssen, dies kann jedoch normalerweise erreicht werden, indem z #!/usr/bin/env python3.

Also starte ich mein Skript mit:

#!/usr/bin/env python3
import sys
import os

# On one of our systems, python3 is pointing to python3.3
# which is too old for our purposes. 'Upgrade' if needed
if sys.version_info[1] < 4:
    for py_version in ['python3.7', 'python3.6', 'python3.5', 'python3.4']:
        try:
            os.execlp(py_version, py_version, *sys.argv)
        except:
            pass # Deliberately ignore errors, pick first available version

Was dies bewirkt ist:

  • Überprüfen Sie die Interpreterversion auf ein Akzeptanzkriterium
  • Wenn dies nicht akzeptabel ist, gehen Sie eine Liste der Kandidatenversionen durch und führen Sie die erste Version erneut aus, die verfügbar ist
Mikrotherion
quelle