Comment puis-je avoir plus d'une possibilité dans la ligne de shebang d'un script?

16

Je suis dans une situation intéressante où j'ai un script Python qui peut théoriquement être exécuté par une variété d'utilisateurs avec une variété d'environnements (et de CHEMINS) et sur une variété de systèmes Linux. Je veux que ce script soit exécutable sur autant de ceux-ci que possible sans restrictions artificielles. Voici quelques configurations connues:

  • Python 2.6 est la version système de Python, donc python, python2 et python2.6 existent tous dans / usr / bin (et sont équivalents).
  • Python 2.6 est la version système de Python, comme ci-dessus, mais Python 2.7 est installé à côté d'elle en tant que python2.7.
  • Python 2.4 est la version système de Python, que mon script ne prend pas en charge. Dans / usr / bin, nous avons python, python2 et python2.4 qui sont équivalents et python2.5, que le script prend en charge.

Je veux exécuter le même script python exécutable sur ces trois. Ce serait bien s'il essayait d'abord d'utiliser /usr/bin/python2.7, s'il existe, puis de revenir à /usr/bin/python2.6, puis de revenir à /usr/bin/python2.5, puis erreur simplement si aucun d'entre eux n'était présent. Je ne suis pas trop accroché à cela en utilisant la version 2.x la plus récente possible, tant qu'il est capable de trouver l'un des interprètes appropriés s'il est présent.

Ma première inclination a été de changer la ligne de shebang de:

#!/usr/bin/python

à

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

car cela fonctionne très bien en bash. Mais l'exécution du script donne:

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

D'accord, j'essaie donc ce qui suit, qui fonctionne également en bash:

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

Mais encore une fois, cela échoue avec:

/bin/bash: - : invalid option

Bien sûr, je pourrais simplement écrire un script shell séparé qui trouve l'interpréteur correct et exécute le script python à l'aide de l'interpréteur qu'il a trouvé. Je trouverais juste compliqué de distribuer deux fichiers où un devrait suffire tant qu'il est exécuté avec l'interpréteur python 2 le plus récent installé. Demander aux gens d'invoquer explicitement l'interpréteur (par exemple, $ python2.5 script.py) n'est pas une option. S'appuyer sur le PATH de l'utilisateur en cours de configuration d'une certaine manière n'est pas non plus une option.

Éditer:

La vérification de version dans le script Python ne fonctionnera pas car j'utilise l'instruction "with" qui existe à partir de Python 2.6 (et peut être utilisée dans 2.5 avec from __future__ import with_statement). Cela provoque l'échec immédiat du script avec une erreur de syntaxe peu conviviale et m'empêche d'avoir la possibilité de vérifier d'abord la version et d'émettre une erreur appropriée.

Exemple: (essayez ceci avec un interpréteur Python inférieur à 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
la source
Pas vraiment ce que vous voulez, donc un commentaire. Mais vous pouvez utiliser import sys; sys.version_info()pour vérifier si l'utilisateur a la version python requise.
Bernhard
2
@Bernhard Oui, c'est vrai, mais il est alors trop tard pour y faire quoi que ce soit. Pour la troisième situation que j'ai énumérée ci-dessus, l'exécution directe du script (c.-à-d. ./script.py) Entraînerait l'exécution de python2.4, ce qui entraînerait votre code à détecter qu'il s'agissait de la mauvaise version (et à quitter, probablement). Mais il y a un très bon python2.5 qui aurait pu être utilisé comme interprète à la place!
user108471
2
Utilisez un script wrapper pour déterminer s'il existe un python approprié et execsi c'est le cas, sinon affichez une erreur.
Kevin
1
Faites-le donc en premier dans le même fichier.
Kevin
9
@ user108471: Vous supposez que la ligne shebang est gérée par bash. Ce n'est pas, c'est un appel système ( execve). Les arguments sont des littéraux de chaîne, pas de globalisation, pas de regexps. C'est ça. Même si le premier argument est "/ bin / bash" et les secondes options ("-c ...") ces options ne sont pas analysées par un shell. Ils sont remis non traités à l'exécutable bash, c'est pourquoi vous obtenez ces erreurs. De plus, le shebang ne fonctionne que s'il est au début. Donc, vous n'avez pas de chance ici, je le crains (à court d'un script qui trouve un interprète python et le nourrit d'un document ICI, ce qui ressemble à un désordre horrible).
goldilocks

Réponses:

13

Je ne suis pas expert, mais je pense que vous ne devriez pas spécifier la version exacte de python à utiliser et laisser ce choix au système / utilisateur.

Vous devriez également utiliser cela au lieu du chemin de codage en dur vers python dans le script:

#!/usr/bin/env python

ou

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

Il est recommandé par Python doc dans toutes les versions:

Un bon choix est généralement

#!/usr/bin/env python

qui recherche l'interpréteur Python dans tout le CHEMIN. Cependant, certains Unices peuvent ne pas avoir la commande env, vous devrez donc peut-être coder en dur / usr / bin / python comme chemin d'interpréteur.

Dans diverses distributions, Python peut être installé à différents endroits, il envle recherchera donc dans PATH. Il devrait être disponible dans toutes les principales distributions Linux et d'après ce que je vois dans FreeBSD.

Le script doit être exécuté avec cette version de Python qui est dans votre PATH et qui est choisie par votre distribution *.

Si votre script est compatible avec toutes les versions de Python sauf 2.4, vous devez simplement vérifier à l'intérieur s'il est exécuté en Python 2.4 et imprimer quelques informations et quitter.

Plus à lire

  • Ici vous pouvez trouver des exemples dans quels endroits Python peut être installé sur différents systèmes.
  • Ici vous pouvez trouver des avantages et des inconvénients pour l' utilisation env.
  • Ici vous pouvez trouver des exemples de manipulation de PATH et différents résultats.

note de bas de page

* Dans Gentoo, il existe un outil appelé eselect. En l'utilisant, vous pouvez définir les versions par défaut de différentes applications (y compris Python) par défaut:

$ 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
la source
2
J'apprécie que ce que je veux faire soit contraire à ce qui serait considéré comme une bonne pratique. Ce que vous avez posté est tout à fait raisonnable, mais en même temps ce n'est pas ce que je demande. Je ne veux pas que mes utilisateurs aient à pointer explicitement mon script vers la version appropriée de Python alors qu'il est parfaitement possible de détecter la version appropriée de Python dans toutes les situations qui me tiennent à cœur.
user108471
1
Veuillez voir ma mise à jour pour savoir pourquoi je ne peux pas "vérifier simplement à l'intérieur si elle est exécutée en Python 2.4 et imprimer des informations et quitter."
user108471
Tu as raison. Je viens de trouver cette question sur SO et maintenant je peux voir qu'il n'y a pas d'option pour le faire si vous voulez avoir un seul fichier ...
pbm
9

Sur la base de quelques idées de quelques commentaires, j'ai réussi à concocter un hack vraiment laid qui semble fonctionner. Le script devient un script bash enveloppe un script Python et le transmet à un interpréteur Python via un "document ici".

Au début:

#!/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 - "$@"
''' #'''

Le code Python va ici. Puis à la toute fin:

# EOF

Lorsque l'utilisateur exécute le script, la version Python la plus récente entre 2.5 et 2.7 est utilisée pour interpréter le reste du script comme un document ici.

Une explication sur certains manigances:

Le truc de guillemet triple que j'ai ajouté permet également d'importer ce même script en tant que module Python (que j'utilise à des fins de test). Lorsqu'il est importé par Python, tout ce qui se trouve entre le premier et le deuxième guillemet triple simple est interprété comme une chaîne de niveau module, et le troisième guillemet triple simple est mis en commentaire. Le reste est du Python ordinaire.

Lorsqu'il est exécuté directement (en tant que script bash maintenant), les deux premiers guillemets simples deviennent une chaîne vide et le troisième guillemet simple forme une autre chaîne avec le quatrième guillemet simple, ne contenant que deux points. Cette chaîne est interprétée par Bash comme un no-op. Tout le reste est la syntaxe Bash pour globaliser les binaires Python dans / usr / bin, sélectionner le dernier et exécuter exec, en passant le reste du fichier comme document ici. Le document ici commence par une citation triple-simple Python contenant uniquement un signe de hachage / livre / octothorpe. Le reste du script est alors interprété comme normal jusqu'à ce que la ligne lisant '# EOF' termine le document ici.

J'ai l'impression que c'est pervers, alors j'espère que quelqu'un a une meilleure solution.

user108471
la source
Peut-être que ce n'est pas si méchant après tout;) +1
goldilocks
L'inconvénient de cela est qu'il gâchera la coloration de la syntaxe sur la plupart des éditeurs
Lie Ryan
@LieRyan Cela dépend. Mon script utilise une extension de nom de fichier .py, que la plupart des éditeurs de texte préfèrent lorsqu'ils choisissent la syntaxe à utiliser pour la coloration. Hypothétiquement, si je devais renommer ce sans l'extension .py, je pouvais utiliser un modeline pour laisser entendre la syntaxe correcte (pour les utilisateurs) au moins Vim avec quelque chose comme: # ft=python.
user108471
7

La ligne shebang ne peut spécifier qu'un chemin fixe vers un interprète. Il y a l' #!/usr/bin/envastuce pour rechercher l'interprète dans le PATHmais c'est tout. Si vous voulez plus de sophistication, vous devrez écrire du code shell wrapper.

La solution la plus évidente consiste à écrire un script wrapper. Appelez le script python foo.realet créez un script wrapper foo:

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

Si vous voulez tout mettre dans un fichier, vous pouvez souvent en faire un polyglotte qui commence par une #!/bin/shligne (donc sera exécuté par le shell) mais est également un script valide dans une autre langue. Selon la langue, un polyglotte peut être impossible (si cela #!provoque une erreur de syntaxe, par exemple). En Python, ce n'est pas très difficile.

#!/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 …

(Le texte entier entre '''et '''est une chaîne Python au niveau supérieur, ce qui n'a aucun effet. Pour le shell, la deuxième ligne est celle ''':'qui, après suppression des guillemets, est la commande no-op :.)

Gilles 'SO- arrête d'être méchant'
la source
La deuxième solution est intéressante car elle ne nécessite pas d'ajouter # EOFà la fin comme dans cette réponse . Votre approche est fondamentalement la même que celle décrite ici .
sschuberth
6

Comme vos besoins indiquent une liste connue de binaires, vous pouvez le faire en Python avec ce qui suit. Cela ne fonctionnerait pas après une version mineure / majeure à un chiffre de Python, mais je ne vois pas cela se produire de si tôt.

Exécute la version la plus élevée située sur le disque à partir de la liste ordonnée et croissante de pythons versionnés, si la version balisée sur le binaire est supérieure à la version actuelle de python en cours d'exécution. La "liste ordonnée croissante des versions" étant l'élément le plus important pour ce 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")

Toutes mes excuses pour mon python rugueux

Mat
la source
ne continuerait-il pas de fonctionner après avoir été exécuté? donc des trucs normaux seraient exécutés deux fois?
Janus Troelsen
1
execv remplace le programme en cours d'exécution par une image de programme nouvellement chargée
Matt
Cela ressemble à une excellente solution qui semble beaucoup moins moche que ce que j'ai imaginé. Je vais devoir essayer ceci pour voir si cela fonctionne à cet effet.
user108471
Cette suggestion fonctionne très bien pour ce dont j'ai besoin. Le seul défaut est quelque chose que je n'avais pas mentionné à l'origine: pour prendre en charge Python 2.5, j'utilise from __future__ import with_statement, ce qui doit être la première chose dans le script Python. Je suppose que vous ne savez pas comment exécuter cette action lors du démarrage du nouvel interprète?
user108471
êtes-vous sûr que ce doit être la toute première chose? ou juste avant d'essayer d'utiliser des withs comme une importation normale?. Un if mypy == 25: from __future__ import with_statementtravail supplémentaire fonctionne-t-il juste avant les «choses normales»? Vous n'avez probablement pas besoin du if, si vous ne supportez pas 2.4.
Matt
0

Vous pouvez écrire un petit script bash qui vérifie l'exécutable phython disponible et l'appelle avec le script comme paramètre. Vous pouvez ensuite faire de ce script la cible de la ligne shebang:

#!/my/python/search/script

Et ce script fait simplement (après la recherche):

"$python_path" "$1"

J'étais incertain si le noyau accepterait cette indirection de script mais j'ai vérifié et cela fonctionne.

Modifier 1

Pour faire de cette imperceptibilité embarrassante une bonne proposition enfin:

Il est possible de combiner les deux scripts dans un seul fichier. Vous écrivez simplement le script python en tant que document ici dans le script bash (si vous changez le script python, il vous suffit de recopier les scripts ensemble). Soit vous créez un fichier temporaire dans eg / tmp ou (si python le supporte, je ne sais pas) vous fournissez le script comme entrée à l'interpréteur:

# 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
la source
C'est plus ou moins la solution qui a déjà été énoncée dans le dernier paragraphe de la question!
Bernhard
Intelligent, mais il nécessite que ce script magique soit installé quelque part sur chaque système.
user108471
@Bernhard Oooops, pris. À l'avenir, je lirai jusqu'à la fin. En guise de compensation, je vais l'améliorer en une solution à fichier unique.
Hauke ​​Laging du
@ user108471 Le script Magic peut contenir quelque chose comme ceci: $(ls /usr/bin/python?.? | tail -n1 )mais je n'ai pas réussi à l'utiliser intelligemment dans un shebang.
Bernhard
@Bernhard Vous voulez faire la recherche dans la ligne shebang? IIRC le noyau ne se soucie pas de citer dans la ligne shebang. Sinon (si cela a changé entre-temps), on pourrait faire quelque chose comme `#! / Bin / bash -c do_search_here_without_whitespace ...; exec $ python" $ 1 "Mais comment faire sans espace blanc?
Hauke ​​Laging