Elija el intérprete después del inicio del script, por ejemplo, si / si no dentro de hashbang

16

¿Hay alguna forma de elegir dinámicamente el intérprete que ejecuta un script? Tengo un script que estoy ejecutando en dos sistemas diferentes, y el intérprete que quiero usar está ubicado en diferentes ubicaciones en los dos sistemas. Lo que tengo que hacer es cambiar la línea hashbang cada vez que cambio. Me gustaría hacer algo que sea el equivalente lógico de esto (me doy cuenta de que esta construcción exacta es imposible):

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

#Rest of script goes here

O incluso mejor sería esto, de modo que intente usar el primer intérprete, y si no lo encuentra, usa el segundo:

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

#Rest of script goes here

Obviamente, en su lugar, puedo ejecutarlo como /path/to/python/on/systemA myscript.py o /path/on/systemB myscript.py dependiendo de dónde estoy, pero en realidad tengo un script de contenedor que se inicia myscript.py, por lo que me gustaría especificar la ruta al intérprete de Python mediante programación en lugar de hacerlo a mano.

dkv
fuente
3
¿Pasar el 'resto del script' como un archivo al intérprete sin shebang, y usar la ifcondición no es una opción para usted? como,if something; then /bin/sh restofscript.sh elif...
mazs
Es una opción, también lo consideré, pero algo más desordenado de lo que me gustaría. Dado que la lógica en la línea hashbang es imposible, creo que iré por esa ruta.
dkv
Me gusta la amplia gama de respuestas diferentes que ha generado esta pregunta.
Oskar Skog

Respuestas:

27

No, eso no funcionará. Los dos caracteres #!deben ser absolutamente los primeros dos caracteres en el archivo (¿cómo especificaría qué interpreta la declaración if de todos modos?). Esto constituye el "número mágico" que la exec()familia de funciones detecta cuando determina si un archivo que está a punto de ejecutar es un script (que necesita un intérprete) o un archivo binario (que no lo hace).

El formato de la línea shebang es bastante estricto. Necesita tener una ruta absoluta hacia un intérprete y, como máximo, un argumento para ello.

Lo que puedes hacer es usar env:

#!/usr/bin/env interpreter

Ahora, el camino enves generalmente /usr/bin/env , pero técnicamente eso no es garantía.

Esto le permite ajustar la PATHvariable de entorno en cada sistema de manera que interpreter(ya sea bash, pythono perl, o lo que tengas) se encuentra.

Una desventaja de este enfoque es que será imposible pasar un argumento de forma portátil al intérprete.

Esto significa que

#!/usr/bin/env awk -f

y

#!/usr/bin/env sed -f

Es poco probable que funcione en algunos sistemas.

Otro enfoque obvio es usar las herramientas automáticas GNU (o algún sistema de plantillas más simple) para encontrar el intérprete y colocar la ruta correcta en el archivo en un ./configurepaso, que se ejecutará al instalar el script en cada sistema.

También se podría recurrir a ejecutar el script con un intérprete explícito, pero eso es obviamente lo que está tratando de evitar:

$ sed -f script.sed
Kusalananda
fuente
Bien, me doy cuenta de que eso #!tiene que venir al principio, ya que no es el shell que procesa esa línea. Me preguntaba si hay una forma de poner lógica dentro de la línea hashbang que sería equivalente a if / else. También esperaba evitar perder el tiempo con mi PATHpero creo que esas son mis únicas opciones.
dkv
1
Cuando lo use #!/usr/bin/awk, puede proporcionar exactamente un argumento, como #!/usr/bin/awk -f. Si el binario al que está apuntando es env, el argumento es el binario que está pidiendo envque busque, como en #!/usr/bin/env awk.
DopeGhoti
2
@dkv No lo es. Utiliza un intérprete con dos argumentos, y puede funcionar en algunos sistemas, pero definitivamente no en todos.
Kusalananda
3
@dkv en Linux se ejecuta /usr/bin/envcon el argumento único awk -f.
ilkkachu
1
@Kusalananda, no, ese era el punto. Si tiene un script llamado foo.awkcon la línea hashbang #!/usr/bin/env awk -fy ./foo.awkluego lo llama con , en Linux, lo que envve son los dos parámetros awk -fy ./foo.awk. En realidad va buscando /usr/bin/awk -f(etc.) con un espacio.
ilkkachu
27

Siempre puede hacer un script de contenedor para encontrar el intérprete correcto para el programa real:

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

Guarde el contenedor en los usuarios PATHcomo programy deje a un lado el programa real o con otro nombre.

Solía #!/bin/bashen el hashbang debido a la flagsmatriz. Si no necesita almacenar un número variable de banderas o algo así y puede prescindir de él, el script debería funcionar de manera portátil #!/bin/sh.

ilkkachu
fuente
2
He visto que exec "$interpreter" "${flags[@]}" "$script" "$@"también solía mantener el árbol del proceso más limpio. También propaga el código de salida.
rrauenza
@rrauenza, ah sí, naturalmente con exec.
ilkkachu
1
¿No #!/bin/shsería mejor en lugar de #!/bin/bash? Incluso si /bin/shes un enlace simbólico a un shell diferente, debería existir en la mayoría (si no todos) los sistemas * nix, además de forzar al autor del script a crear un script portátil en lugar de caer en bashismos.
Sergiy Kolodyazhnyy
@SergiyKolodyazhnyy, heh, pensé en mencionar eso antes, pero no lo hice, entonces. La matriz utilizada flagses una característica no estándar, pero es lo suficientemente útil para almacenar un número variable de banderas, por lo que decidí mantenerla.
ilkkachu
O el uso / bin / sh y simplemente llamar al intérprete directamente en cada rama: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". También puede agregar la comprobación de errores para fallas de ejecución.
jrw32982 apoya a Monica
11

También puedes escribir un políglota (combina dos idiomas). / bin / sh está garantizado para existir.

Esto tiene la desventaja del código feo y quizás algunos /bin/shs podrían confundirse. Pero se puede usar cuando envno existe o existe en otro lugar que no sea / usr / bin / env. También se puede usar si desea hacer una selección bastante elegante.

La primera parte del script determina qué intérprete usar cuando se ejecuta con / bin / sh como intérprete, pero se ignora cuando lo ejecuta el intérprete correcto. Úselo execpara evitar que el shell se ejecute más que la primera parte.

Ejemplo de Python:

#!/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
fuente
3
Creo que he visto uno de estos antes, pero la idea sigue siendo igualmente horrible ... Pero, es probable que también desee exec "$interpreter" "$0" "$@"llevar el nombre del guión al intérprete real. (Y luego espero que nadie haya mentido al configurar $0)
Ilkkachu
66
Scala en realidad tiene soporte para scripts políglotas en su sintaxis: si un script Scala comienza con #!, Scala ignora todo hasta una coincidencia !#; esto le permite poner código de script complejo arbitrariamente en un lenguaje arbitrario allí, y luego execel motor de ejecución Scala con el script.
Jörg W Mittag
1
@ Jörg W Mittag: +1 para Scala
jrw32982 apoya a Monica
2

Prefiero las respuestas de Kusalananda e ilkkachu, pero aquí hay una respuesta alternativa que hace más directamente lo que pedía la pregunta, simplemente porque se hizo.

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

if True:
  print("hello world!")

Tenga en cuenta que solo puede hacer esto cuando el intérprete permite escribir código en el primer argumento. Aquí, -ey todo después de esto se toma literalmente como 1 argumento para ruby. Por lo que puedo decir, no puedes usar bash para el código shebang, porque bash -crequiere que el código esté en un argumento separado.

Intenté hacer lo mismo con Python para el código shebang:

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

pero resulta demasiado largo y Linux (al menos en mi máquina) trunca el shebang a 127 caracteres. execDisculpe el uso de insertar nuevas líneas, ya que python no permite try-excepts o imports sin nuevas líneas.

No estoy seguro de cuán portátil es, y no lo haría en un código destinado a ser distribuido. Sin embargo, es factible. Tal vez alguien lo encuentre útil para la depuración rápida y sucia o algo así.

JoL
fuente
2

Si bien esto no selecciona el intérprete dentro del script de shell (lo selecciona por máquina) es una alternativa más fácil si tiene acceso administrativo a todas las máquinas en las que está intentando ejecutar el script.

Cree un enlace simbólico (o un enlace duro si lo desea) para apuntar a la ruta del intérprete deseada. Por ejemplo, en mi sistema, Perl y Python están en / usr / bin:

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

crearía un enlace simbólico para permitir que el hashbang se resuelva para / bin / perl, etc. Esto preserva la capacidad de pasar parámetros a los scripts también.

Robar
fuente
1
+1 Esto es muy simple. Como observa, no responde a la pregunta, pero parece hacer exactamente lo que quiere el OP. Aunque supongo que usar env evita el acceso a la raíz en cada problema de la máquina.
Joe
0

Me enfrenté a un problema similar como este hoy ( python3señalando una versión de Python que era demasiado antigua en un sistema), y se me ocurrió un enfoque que es un poco diferente de los discutidos aquí: use la versión "incorrecta" de Python para arrancar en el "correcto". La limitación es que algunas versiones de python deben ser accesibles de manera confiable, pero eso generalmente se puede lograr por ejemplo #!/usr/bin/env python3.

Entonces, lo que hago es comenzar mi script con:

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

Lo que esto hace es:

  • Consulte la versión del intérprete para conocer algunos criterios de aceptación.
  • Si no es aceptable, revise una lista de versiones candidatas y vuelva a ejecutarla con la primera de ellas disponible.
microtherion
fuente