¿Cómo puedo tener más de una posibilidad en la línea shebang de un script?

16

Estoy en una situación interesante en la que tengo un script de Python que teóricamente puede ser ejecutado por una variedad de usuarios con una variedad de entornos (y PATHs) y en una variedad de sistemas Linux. Quiero que este script sea ejecutable en la mayor cantidad posible sin restricciones artificiales. Aquí hay algunas configuraciones conocidas:

  • Python 2.6 es la versión de Python del sistema, por lo que python, python2 y python2.6 existen en / usr / bin (y son equivalentes).
  • Python 2.6 es la versión de Python del sistema, como se indicó anteriormente, pero Python 2.7 está instalado junto a ella como python2.7.
  • Python 2.4 es la versión de Python del sistema, que mi script no admite. En / usr / bin tenemos python, python2 y python2.4 que son equivalentes, y python2.5, que admite el script.

Quiero ejecutar el mismo script ejecutable de Python en estos tres. Sería bueno si intentara usar /usr/bin/python2.7 primero, si existe, luego volver a /usr/bin/python2.6, luego volver a /usr/bin/python2.5, luego simplemente error si ninguno de esos estaba presente. Sin embargo, no estoy demasiado obsesionado con el 2.x más reciente posible, siempre que pueda encontrar uno de los intérpretes correctos si está presente.

Mi primera inclinación fue cambiar la línea shebang de:

#!/usr/bin/python

a

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

ya que esto funciona bien en bash. Pero ejecutar el script da:

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

Bien, entonces intento lo siguiente, que también funciona en bash:

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

Pero de nuevo, esto falla con:

/bin/bash: - : invalid option

De acuerdo, obviamente podría escribir un script de shell separado que encuentre el intérprete correcto y ejecute el script de Python usando el intérprete que encuentre. Simplemente me resultaría una molestia distribuir dos archivos donde uno debería ser suficiente siempre que se ejecute con el intérprete de Python 2 más actualizado instalado. Pedirle a la gente que invoque al intérprete explícitamente (p. Ej. $ python2.5 script.py) No es una opción. Confiar en que la RUTA del usuario se configure de cierta manera tampoco es una opción.

Editar:

La verificación de versiones dentro del script Python no funcionará ya que estoy usando la instrucción "con" que existe a partir de Python 2.6 (y se puede usar en 2.5 con from __future__ import with_statement). Esto hace que el script falle inmediatamente con un SyntaxError hostil para el usuario, y me impide tener la oportunidad de verificar primero la versión y emitir un error apropiado.

Ejemplo: (intente esto con un intérprete de Python menor que 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')
usuario108471
fuente
Realmente no es lo que quieres, por lo tanto, comenta. Pero puede usar import sys; sys.version_info()para verificar si el usuario tiene la versión de Python requerida.
Bernhard
2
@Bernhard Sí, esto es cierto, pero para entonces es demasiado tarde para hacer algo al respecto. Para la tercera situación que mencioné anteriormente, ejecutar el script directamente (es decir, ./script.py) causaría que python2.4 lo ejecute, lo que haría que su código detecte que se trata de una versión incorrecta (y que se cierre, presumiblemente). ¡Pero hay un python2.5 perfectamente bueno que podría haber sido utilizado como intérprete!
user108471
2
Use una secuencia de comandos de envoltura para determinar si hay una python adecuada y exec, de ser así, imprima un error.
Kevin
1
Así que hazlo primero en el mismo archivo.
Kevin
99
@ user108471: está asumiendo que bash maneja la línea shebang. No lo es, es una llamada al sistema ( execve). Los argumentos son literales de cadena, sin globalización, sin expresiones regulares. Eso es. Incluso si el primer argumento es "/ bin / bash" y las segundas opciones ("-c ...") esas opciones no son analizadas por un shell. Se entregan sin tratar al ejecutable de bash, por lo que obtienes esos errores. Además, el shebang solo funciona si está al principio. Entonces, no tienes suerte aquí, me temo (a falta de un script que encuentre un intérprete de Python y lo alimente con un documento AQUÍ, que suena como un desastre horrible).
Ricitos de oro

Respuestas:

13

No soy experto, pero creo que no debe especificar la versión exacta de Python para usar y dejar esa opción al sistema / usuario.

También debe usar eso en lugar de la ruta de codificación fija a Python en el script:

#!/usr/bin/env python

o

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

Se recomienda por Python doc en todas las versiones:

Una buena opción suele ser

#!/usr/bin/env python

que busca el intérprete de Python en toda la RUTA. Sin embargo, algunos Unices pueden no tener el comando env, por lo que es posible que deba codificar / usr / bin / python como la ruta del intérprete.

En varias distribuciones, Python puede instalarse en diferentes lugares, por envlo que lo buscará en PATH. Debería estar disponible en todas las principales distribuciones de Linux y de lo que veo en FreeBSD.

El script debe ejecutarse con esa versión de Python que está en su RUTA y que es elegida por su distribución *.

Si su secuencia de comandos es compatible con todas las versiones de Python, excepto 2.4, debe verificar dentro de ella si se ejecuta en Python 2.4 e imprimir información y salir.

Más para leer

  • Aquí puede encontrar ejemplos en qué lugares se puede instalar Python en diferentes sistemas.
  • Aquí puede encontrar algunas ventajas y desventajas para su uso env.
  • Aquí puede encontrar ejemplos de manipulación de RUTA y diferentes resultados.

Nota

* En Gentoo hay una herramienta llamada eselect. Al usarlo, puede establecer versiones predeterminadas de diferentes aplicaciones (incluida Python) como predeterminadas:

$ 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
fuente
2
Aprecio que lo que quiero hacer es contrarrestar lo que se consideraría una buena práctica. Lo que has publicado es completamente razonable, pero al mismo tiempo no es lo que estoy pidiendo. No quiero que mis usuarios tengan que apuntar explícitamente mi script a la versión apropiada de Python cuando es perfectamente posible detectar la versión apropiada de Python en todas las situaciones que me importan.
user108471
1
Consulte mi actualización para saber por qué no puedo "simplemente comprobar dentro de ella si se ejecuta en Python 2.4 e imprimir alguna información y salir".
user108471
Tienes razón. Acabo de encontrar esta pregunta en SO y ahora puedo ver que no hay opción para hacerlo si desea tener un solo archivo ...
pbm
9

Basándome en algunas ideas de algunos comentarios, logré armar un truco realmente feo que parece funcionar. El script se convierte en un script bash que envuelve un script de Python y lo pasa a un intérprete de Python a través de un "documento aquí".

Al principio:

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

El código de Python va aquí. Luego al final:

# EOF

Cuando el usuario ejecuta el script, la versión más reciente de Python entre 2.5 y 2.7 se usa para interpretar el resto del script como un documento aquí.

Una explicación sobre algunas travesuras:

Las cosas de comillas triples que he agregado también permiten que este mismo script se importe como un módulo de Python (que uso para fines de prueba). Cuando Python lo importa, todo lo que se encuentra entre la primera y la segunda comillas triples se interpreta como una cadena de nivel de módulo, y la tercera comilla triple se comenta. El resto es Python ordinario.

Cuando se ejecuta directamente (como un script bash ahora), las dos primeras comillas simples se convierten en una cadena vacía, y la tercera comilla simple forma otra cadena con la cuarta comilla simple, que contiene solo dos puntos. Bash interpreta esta cadena como un no-op. Todo lo demás es sintaxis de Bash para bloquear los binarios de Python en / usr / bin, seleccionar el último y ejecutar exec, pasando el resto del archivo como un documento aquí. El documento aquí comienza con una comilla triple triple de Python que contiene solo un signo hash / pound / octothorpe. El resto del script se interpreta como normal hasta que la línea que dice '# EOF' finaliza el documento aquí.

Siento que esto es perverso, así que espero que alguien tenga una mejor solución.

usuario108471
fuente
Tal vez no sea tan desagradable después de todo;) +1
Ricitos de Oro
La desventaja de esto es que estropeará el color de la sintaxis en la mayoría de los editores
Lie Ryan
@LieRyan Eso depende. Mi script usa una extensión de nombre de archivo .py, que la mayoría de los editores de texto prefieren al elegir qué sintaxis usar para colorear. Hipotéticamente, si tuviera que cambiar el nombre de esta sin la extensión .py, podría usar un modeline dar a entender la sintaxis correcta (para usuarios de Vim por lo menos) con algo como: # ft=python.
user108471
7

La línea shebang solo puede especificar una ruta fija a un intérprete. Existe el #!/usr/bin/envtruco para buscar el intérprete en el PATHpero eso es todo. Si quieres más sofisticación, deberás escribir un código de shell envoltorio.

La solución más obvia es escribir un script de envoltura. Llame al script de python foo.realy cree un script de envoltura foo:

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

Si desea poner todo en un archivo, a menudo puede convertirlo en un políglota que comienza con una #!/bin/shlínea (por lo que será ejecutado por el shell) pero también es un script válido en otro idioma. Dependiendo del idioma, un políglota puede ser imposible (si #!causa un error de sintaxis, por ejemplo). En Python, no es muy difícil.

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

(Todo el texto entre '''y '''es una cadena de Python en el nivel superior, que no tiene ningún efecto. Para el shell, la segunda línea es la ''':'que después de eliminar las comillas es el comando no-op :.)

Gilles 'SO- deja de ser malvado'
fuente
La segunda solución es buena ya que no requiere agregar # EOFal final como en esta respuesta . Su enfoque básicamente es el mismo que se describe aquí .
sschuberth
6

Como sus requisitos establecen una lista conocida de binarios, puede hacerlo en Python con lo siguiente. No funcionaría más allá de una versión menor / mayor de un solo dígito de Python, pero no veo que eso suceda pronto.

Ejecuta la versión más alta ubicada en el disco desde la lista ordenada y creciente de pitones versionadas, si la versión etiquetada en el binario es más alta que la versión actual de Python en ejecución. La "lista creciente de versiones ordenadas" es la parte importante de este código.

#!/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")

Disculpas por mi pitón rasposo

Mate
fuente
¿no continuaría ejecutándose después de ejecutar? Entonces, ¿las cosas normales se ejecutarían dos veces?
Janus Troelsen
1
execv reemplaza el programa que se está ejecutando actualmente con una imagen de programa recién cargada
Matt
Esto parece una gran solución que se siente mucho menos fea de lo que se me ocurrió. Tendré que intentarlo para ver si funciona para este propósito.
user108471
Esta sugerencia casi funciona para lo que necesito. El único defecto es algo que no mencioné originalmente: para usar Python 2.5, uso from __future__ import with_statement, que debe ser lo primero en el script de Python. ¿Supongo que no sabes una forma de realizar esa acción al iniciar el nuevo intérprete?
user108471
¿está seguro de que tiene que ser la muy primera cosa? ¿O justo antes de intentar usar cualquier withs como una importación normal? ¿Un if mypy == 25: from __future__ import with_statementtrabajo extra justo antes de 'cosas normales'? Probablemente no necesite el if, si no es compatible con 2.4.
Matt
0

Puede escribir un pequeño script bash que verifique el ejecutable phython disponible y lo llame con el script como parámetro. Luego puede hacer que este script sea el objetivo de la línea shebang:

#!/my/python/search/script

Y este script simplemente hace (después de la búsqueda):

"$python_path" "$1"

No estaba seguro de si el núcleo aceptaría esta secuencia de comandos indirectamente, pero lo comprobé y funciona.

Editar 1

Para hacer de esta vergonzosa falta de percepción una buena propuesta finalmente:

Es posible combinar ambos scripts en un archivo. Simplemente escriba la secuencia de comandos de Python como un documento aquí en la secuencia de comandos bash (si cambia la secuencia de comandos de Python, solo necesita copiar las secuencias de comandos juntas de nuevo). O crea un archivo temporal en, por ejemplo, / tmp o (si Python lo admite, no lo sé), proporciona el script como entrada para el intérprete:

# 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
fuente
¡Esta es más o menos la solución que ya se indicó en el último párrafo de la pregunta!
Bernhard
Inteligente, pero requiere que este script mágico se instale en algún lugar de cada sistema.
user108471
@Bernhard Oooops, atrapado. En el futuro leeré hasta el final. Como compensación voy a mejorarlo a una solución de un archivo.
Hauke ​​Laging
@ user108471 El script mágico puede contener algo como esto: $(ls /usr/bin/python?.? | tail -n1 )pero no logré usarlo de manera inteligente en un shebang.
Bernhard
@ Bernhard ¿Quieres hacer la búsqueda dentro de la línea shebang? IIRC al núcleo no le importa citar en la línea shebang. De lo contrario (si esto ha cambiado mientras tanto) uno podría hacer algo como `#! / Bin / bash -c do_search_here_without_whitespace ...; exec $ python" $ 1 "Pero, ¿cómo hacerlo sin espacios en blanco?
Hauke ​​Laging