Wie kann ich mehr als eine Möglichkeit in der Shebang-Zeile eines Skripts haben?

15

Ich bin in einer interessanten Situation, in der ich ein Python-Skript habe, das theoretisch von einer Vielzahl von Benutzern mit einer Vielzahl von Umgebungen (und PATHs) und auf einer Vielzahl von Linux-Systemen ausgeführt werden kann. Ich möchte, dass dieses Skript auf so vielen von ihnen wie möglich ohne künstliche Einschränkungen ausführbar ist. Hier sind einige bekannte Setups:

  • Python 2.6 ist die System-Python-Version, daher existieren Python, Python2 und Python2.6 alle in / usr / bin (und sind äquivalent).
  • Python 2.6 ist die System-Python-Version wie oben, aber Python 2.7 wird als Python2.7 daneben installiert.
  • Python 2.4 ist die System-Python-Version, die mein Skript nicht unterstützt. In / usr / bin haben wir Python, Python2 und Python2.4, die äquivalent sind, und Python2.5, die das Skript unterstützt.

Ich möchte auf allen drei das gleiche ausführbare Python-Skript ausführen. Es wäre schön, wenn es zuerst versuchen würde, /usr/bin/python2.7 zu verwenden, wenn es existiert, dann auf /usr/bin/python2.6 zurückgreifen und dann auf /usr/bin/python2.5 zurückgreifen Einfach Fehler raus, wenn keiner von denen anwesend war. Ich bin jedoch nicht zu sehr damit beschäftigt, das neueste 2.x zu verwenden, solange es in der Lage ist, einen der richtigen Interpreten zu finden, falls vorhanden.

Meine erste Neigung war es, die Shebang-Linie zu ändern von:

#!/usr/bin/python

zu

#!/usr/bin/python2.[5-7]

da dies gut in bash funktioniert. Das Ausführen des Skripts bietet jedoch Folgendes:

/usr/bin/python2.[5-7]: bad interpreter: No such file or directory

Okay, also versuche ich folgendes, was auch in bash funktioniert:

#!/bin/bash -c /usr/bin/python2.[5-7]

Aber auch dies schlägt fehl mit:

/bin/bash: - : invalid option

Okay, natürlich könnte ich einfach ein separates Shell-Skript schreiben, das den richtigen Interpreter findet und das Python-Skript mit dem gefundenen Interpreter ausführt. Ich finde es einfach mühsam, zwei Dateien zu verteilen, von denen eine ausreichen sollte, solange der aktuellste Python 2-Interpreter installiert ist. Das explizite Aufrufen des Interpreters (z. B. $ python2.5 script.py) ist keine Option. Es ist auch keine Option, sich darauf zu verlassen, dass der PATH des Benutzers auf eine bestimmte Weise eingerichtet wurde.

Bearbeiten:

Die Versionsprüfung innerhalb des Python-Skripts funktioniert nicht , da ich die "with" -Anweisung verwende, die ab Python 2.6 existiert (und in 2.5 mit verwendet werden kann from __future__ import with_statement). Dies führt dazu, dass das Skript mit einem benutzerunfreundlichen SyntaxError sofort fehlschlägt, und verhindert, dass ich jemals die Möglichkeit habe, die Version zuerst zu überprüfen und einen entsprechenden Fehler zu melden.

Beispiel: (versuchen Sie dies mit einem Python-Interpreter unter 2.6)

#!/usr/bin/env python

import sys

print "You'll never see this!"
sys.exit()

with open('/dev/null', 'w') as out:
    out.write('something')
user108471
quelle
Nicht wirklich was du willst, deshalb Kommentar. Sie können import sys; sys.version_info()jedoch überprüfen, ob der Benutzer die erforderliche Python-Version hat.
Bernhard
2
@Bernhard Ja, das stimmt, aber bis dahin ist es zu spät, etwas dagegen zu unternehmen. In der dritten oben aufgeführten Situation ./script.pywürde das direkte Ausführen des Skripts (dh ) dazu führen, dass Python 2.4 ausgeführt wird, wodurch Ihr Code erkennt, dass es sich um die falsche Version handelt (und möglicherweise beendet wird). Aber es gibt ein perfektes Python2.5, das stattdessen als Interpreter hätte verwendet werden können!
user108471
2
Verwenden Sie ein Wrapper-Skript, um herauszufinden, ob es ein geeignetes Python gibt, und geben execSie andernfalls einen Fehler aus.
Kevin
1
Also mach es als erstes in der gleichen Datei.
Kevin
9
@ user108471: Sie gehen davon aus, dass die shebang-Zeile von bash verarbeitet wird. Es ist kein Systemaufruf ( execve). Die Argumente sind Zeichenfolgenliterale, keine Globen, keine regulären Ausdrücke. Das ist es. Selbst wenn das erste Argument "/ bin / bash" und die zweiten Optionen ("-c ...") sind, werden diese Optionen nicht von einer Shell analysiert. Sie werden unbehandelt an die ausführbare Bash-Datei übergeben, weshalb Sie diese Fehler erhalten. Außerdem funktioniert der Shebang nur, wenn er am Anfang steht. Ich fürchte, Sie haben hier kein Glück (es fehlt ein Skript, das einen Python-Interpreter findet und ihm ein HIER-Dokument zuführt, das wie ein schreckliches Durcheinander klingt).
Goldlöckchen

Antworten:

12

Ich bin kein Experte, aber ich glaube, Sie sollten nicht die genaue Python-Version angeben, die verwendet werden soll, und diese Auswahl dem System / Benutzer überlassen.

Außerdem sollten Sie diesen Pfad anstelle des Hardcodierungspfads für Python im Skript verwenden:

#!/usr/bin/env python

oder

#!/usr/bin/env python3 (or python2)

Es wird von Python doc in allen Versionen empfohlen :

Eine gute Wahl ist normalerweise

#!/usr/bin/env python

der nach dem Python-Interpreter im gesamten PATH sucht. Einige Unices verfügen jedoch möglicherweise nicht über den Befehl env, sodass Sie möglicherweise / usr / bin / python als Interpreterpfad fest codieren müssen.

In verschiedenen Distributionen kann Python an verschiedenen Orten installiert sein, also envwird danach gesucht PATH. Es sollte in allen wichtigen Linux-Distributionen und in FreeBSD verfügbar sein.

Das Skript sollte mit der Version von Python ausgeführt werden, die sich in Ihrem PATH befindet und die von Ihrer Distribution * ausgewählt wurde.

Wenn Ihr Skript mit allen Versionen von Python außer 2.4 kompatibel ist, sollten Sie nur überprüfen, ob es in Python 2.4 ausgeführt wird, und einige Informationen ausdrucken und beenden.

Mehr zu lesen

  • Hier finden Sie Beispiele, an welchen Orten Python möglicherweise auf verschiedenen Systemen installiert ist.
  • Hier finden Sie einige Vor- und Nachteile für die Verwendung env.
  • Hier finden Sie Beispiele für die Manipulation von PATH und verschiedene Ergebnisse.

Fußnote

* In Gentoo gibt es ein Tool namens eselect. Damit können Sie Standardversionen verschiedener Anwendungen (einschließlich Python) als Standard festlegen:

$ eselect python list
Available Python interpreters:
  [1]   python2.6
  [2]   python2.7 *
  [3]   python3.2
$ sudo eselect python set 1
$ eselect python list
Available Python interpreters:
  [1]   python2.6 *
  [2]   python2.7
  [3]   python3.2
pbm
quelle
2
Ich schätze, dass das, was ich tun möchte, dem widerspricht, was als gute Praxis angesehen wird. Was Sie gepostet haben, ist völlig vernünftig, aber gleichzeitig ist es nicht das, wonach ich frage. Ich möchte nicht, dass meine Benutzer mein Skript explizit auf die entsprechende Version von Python verweisen müssen, wenn es perfekt möglich ist, die entsprechende Version von Python in allen Situationen zu erkennen, die mir wichtig sind.
user108471
1
Bitte sehen Sie in meinem Update nach, warum ich nicht "einfach nachsehen kann, ob es in Python 2.4 ausgeführt wird und einige Informationen drucken und beenden" kann.
user108471
Du hast recht. Ich habe diese Frage gerade auf SO gefunden und jetzt kann ich sehen, dass es keine Option gibt, dies zu tun, wenn Sie nur eine Datei haben möchten ...
pbm
9

Auf der Grundlage einiger Ideen aus einigen Kommentaren gelang es mir, einen wirklich hässlichen Hack zusammenzuschustern, der zu funktionieren scheint. Das Skript wird zu einem Bash-Skript, das ein Python-Skript umschließt und über ein "Here-Dokument" an einen Python-Interpreter weiterleitet.

Am Anfang:

#!/bin/bash

''':'
vers=( /usr/bin/python2.[5-7] )
latest="${vers[$((${#vers[@]} - 1))]}"
if !(ls $latest &>/dev/null); then
    echo "ERROR: Python versions < 2.5 not supported"
    exit 1
fi
cat <<'# EOF' | exec $latest - "$@"
''' #'''

Der Python-Code geht hier. Dann ganz am Ende:

# EOF

Wenn der Benutzer das Skript ausführt, wird die neueste Python-Version zwischen 2.5 und 2.7 verwendet, um den Rest des Skripts als Here-Dokument zu interpretieren.

Eine Erklärung zu einigen Spielereien:

Das Triple-Quote-Zeug, das ich hinzugefügt habe, ermöglicht es auch, dasselbe Skript als Python-Modul zu importieren (das ich zu Testzwecken verwende). Beim Import von Python wird alles zwischen dem ersten und dem zweiten einfachen Anführungszeichen als Zeichenfolge auf Modulebene interpretiert, und das dritte einfache Anführungszeichen wird auskommentiert. Der Rest ist gewöhnliches Python.

Bei direkter Ausführung (jetzt als Bash-Skript) werden die ersten beiden einfachen Anführungszeichen zu einer leeren Zeichenfolge, und das dritte einfache Anführungszeichen bildet eine weitere Zeichenfolge mit dem vierten einfachen Anführungszeichen, das nur einen Doppelpunkt enthält. Diese Zeichenfolge wird von Bash als No-Op interpretiert. Alles andere ist die Bash-Syntax zum Verschieben der Python-Binärdateien in / usr / bin, zum Auswählen der letzten und zum Ausführen von exec, wobei der Rest der Datei als Here-Dokument übergeben wird. Das vorliegende Dokument beginnt mit einem dreifachen einfachen Anführungszeichen für Python, das nur ein Hash- / Pfund- / Oktothorpe-Zeichen enthält. Der Rest des Skripts wird dann als normal interpretiert, bis die Zeile '# EOF' das Here-Dokument beendet.

Ich finde das pervers, also hoffe ich, dass jemand eine bessere Lösung hat.

user108471
quelle
Vielleicht ist es doch nicht so
schlimm
Der Nachteil dabei ist, dass die Syntaxfärbung bei den meisten Editoren durcheinander gebracht wird
Lie Ryan
@LieRyan Das kommt darauf an. Mein Skript verwendet die Dateinamenerweiterung .py, die die meisten Texteditoren bevorzugen, wenn sie die für die Färbung zu verwendende Syntax auswählen. Wenn ich dies ohne die Erweiterung .py umbenennen würde, könnte ich hypothetisch die korrekte Syntax (zumindest für Vim-Benutzer) mit einer Modeline andeuten # ft=python.
user108471
7

Die Shebang-Linie kann nur einen festen Pfad zu einem Interpreter angeben. Es gibt den #!/usr/bin/envTrick, den Dolmetscher im PATHnachzuschlagen, aber das war's. Wenn Sie mehr Raffinesse wünschen, müssen Sie Wrapper-Shell-Code schreiben.

Die naheliegendste Lösung besteht darin, ein Wrapper-Skript zu schreiben. Rufen Sie das Python-Skript auf foo.realund erstellen Sie ein Wrapper-Skript foo:

#!/bin/sh
if type python2 >/dev/null 2>/dev/null; then
  exec python2 "$0.real" "$@"
else
  exec python "$0.real" "$@"
fi

Wenn Sie alles in eine Datei einfügen möchten, können Sie es häufig zu einem Polyglot machen , das mit einer #!/bin/shZeile beginnt (also von der Shell ausgeführt wird), aber auch ein gültiges Skript in einer anderen Sprache ist. Abhängig von der Sprache ist eine Mehrsprachigkeit möglicherweise nicht möglich (wenn dies beispielsweise #!einen Syntaxfehler verursacht). In Python ist es nicht sehr schwierig.

#!/bin/sh
''':'
if type python2 >/dev/null 2>/dev/null; then
  exec python2 "$0.real" "$@"
else
  exec python "$0.real" "$@"
fi
'''
# real Python script starts here
def …

(Der gesamte Text zwischen '''und '''ist eine Python-Zeichenfolge auf oberster Ebene, die keine Auswirkung hat. Bei der Shell ist die zweite Zeile ''':'nach dem Entfernen von Anführungszeichen der Befehl no-op :.)

Gilles 'SO - hör auf böse zu sein'
quelle
Die zweite Lösung ist schön, da sie nicht # EOFwie in dieser Antwort am Ende hinzugefügt werden muss . Ihr Ansatz ist im Grunde derselbe wie hier beschrieben .
Schuberth
6

Da Ihre Anforderungen eine bekannte Liste von Binärdateien enthalten, können Sie dies in Python folgendermaßen tun. Es würde nach einer einstelligen Neben- / Hauptversion von Python nicht mehr funktionieren, aber ich sehe, dass dies in Kürze nicht mehr möglich ist.

Führt die höchste auf der Festplatte befindliche Version aus der geordneten, aufsteigenden Liste versionierter Pythons aus, wenn die in der Binärdatei markierte Version höher ist als die aktuelle Version von Python, die ausgeführt wird. Die "geordnete aufsteigende Liste von Versionen" ist das wichtige Bit für diesen Code.

#!/usr/bin/env python
import os, sys

pythons = [ '/usr/bin/python2.3','/usr/bin/python2.4', '/usr/bin/python2.5', '/usr/bin/python2.6', '/usr/bin/python2.7' ]
py = list(filter( os.path.isfile, pythons ))
if py:
  py = py.pop()
  thepy = int( py[-3:-2] + py[-1:] )
  mypy  = int( ''.join( map(str, sys.version_info[0:2]) ) )
  if thepy > mypy:
    print("moving versions to "+py)
    args = sys.argv
    args.insert( 0, sys.argv[0] )
    os.execv( py, args )

print("do normal stuff")

Entschuldigung für meine kratzige Python

Matt
quelle
würde es nach der Ausführung nicht weiterlaufen? würde so normales Zeug zweimal ausgeführt werden?
Janus Troelsen
1
execv ersetzt das aktuell ausgeführte Programm durch ein neu geladenes Programm-Image
Matt
Dies sieht nach einer großartigen Lösung aus, die sich weniger hässlich anfühlt als das, was ich mir ausgedacht habe. Ich muss es versuchen, um zu sehen, ob es für diesen Zweck funktioniert.
user108471
Dieser Vorschlag funktioniert fast für das, was ich brauche. Der einzige Fehler ist etwas, das ich ursprünglich nicht erwähnt habe: Um Python 2.5 zu unterstützen, verwende ich from __future__ import with_statement, was das erste sein muss, was im Python-Skript enthalten ist. Vermutlich wissen Sie nicht, wie Sie diese Aktion ausführen können, wenn Sie den neuen Interpreter starten.
user108471
Sind Sie sicher, dass es das allererste sein muss? oder kurz bevor Sie versuchen, irgendwelche withs wie ein normaler Import zu verwenden ?. Funktioniert ein Extra if mypy == 25: from __future__ import with_statementkurz vor "normalen Sachen"? Sie brauchen das if wahrscheinlich nicht, wenn Sie 2.4 nicht unterstützen.
Matt
0

Sie können ein kleines Bash-Skript schreiben, das nach der verfügbaren ausführbaren Phython-Datei sucht und diese mit dem Skript als Parameter aufruft. Sie können dieses Skript dann zum Ziel der Shebang-Linie machen:

#!/my/python/search/script

Und dieses Skript macht einfach (nach der Suche):

"$python_path" "$1"

Ich war mir nicht sicher, ob der Kernel diese Skript-Indirektion akzeptieren würde, aber ich habe sie überprüft und sie funktioniert.

Bearbeiten 1

Um diese peinliche Unaufmerksamkeit endlich zu einem guten Vorschlag zu machen:

Es ist möglich, beide Skripte in einer Datei zu kombinieren. Sie schreiben das Python-Skript einfach als Here-Dokument im Bash-Skript (wenn Sie das Python-Skript ändern, müssen Sie die Skripte nur noch einmal zusammen kopieren). Entweder erstellen Sie eine temporäre Datei in z. B. / tmp oder Sie geben das Skript als Eingabe für den Interpreter ein (falls Python dies unterstützt, weiß ich nicht):

# do the search here and then
# either
cat >"tmpfile" <<"EOF" # quoting EOF is important so that bash leaves the python code alone
# here is the python script
EOF
"$python_path" "tmpfile"
# or
"$python_path" <<"EOF"
# here is the python script
EOF
Hauke ​​Laging
quelle
Dies ist mehr oder weniger die Lösung, die bereits im letzten Absatz der Frage dargelegt wurde!
Bernhard
Clever, aber es erfordert, dass dieses magische Skript irgendwo auf jedem System installiert ist.
user108471
@Bernhard Oooops, erwischt. In Zukunft werde ich bis zum Ende lesen. Als Entschädigung werde ich es zu einer One-File-Lösung verbessern.
Hauke ​​Laging
@ user108471 Magic-Skript kann so etwas enthalten: Es ist $(ls /usr/bin/python?.? | tail -n1 )mir jedoch nicht gelungen, dies in einem Shebang geschickt zu verwenden.
Bernhard
@Bernhard Du willst die Suche in der Shebang-Zeile machen? IIRC dem Kernel ist es egal, in der Shebang-Zeile zu zitieren. Andernfalls (oder wenn sich dies inzwischen geändert hat) könnte man so etwas wie `#! / Bin / bash -c do_search_here_without_whitespace ...; exec $ python" $ 1 "machen. Aber wie geht das ohne Whitespace?
Hauke ​​Laging