Choisissez l'interpréteur après le démarrage du script, par exemple si / sinon à l'intérieur du hashbang

16

Existe-t-il un moyen de choisir dynamiquement l'interpréteur qui exécute un script? J'ai un script que j'exécute sur deux systèmes différents, et l'interpréteur que je veux utiliser se trouve à différents endroits sur les deux systèmes. Ce que je dois finir par changer, c'est la ligne de hachage à chaque fois que je passe. Je voudrais faire quelque chose qui est l' équivalent logique de cela (je me rends compte que cette construction exacte est impossible):

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

#Rest of script goes here

Ou encore mieux serait ceci, pour qu'il essaie d'utiliser le premier interprète, et s'il ne le trouve pas, il utilise le second:

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

#Rest of script goes here

Évidemment, je peux plutôt l'exécuter comme /path/to/python/on/systemA myscript.py ou /path/on/systemB myscript.py selon l'endroit où je suis, mais j'ai en fait un script wrapper qui se lance myscript.py, donc je voudrais spécifier le chemin vers l'interpréteur python par programme plutôt qu'à la main.

dkv
la source
3
passer le «reste du script» sous forme de fichier à l'interprète sans shebang, et utiliser la ifcondition n'est pas une option pour vous? comme,if something; then /bin/sh restofscript.sh elif...
mazs
C'est une option, je l'ai également envisagée, mais un peu plus compliquée que je ne le souhaiterais. Puisque la logique dans la ligne de hashbang est impossible, je pense que je vais en effet suivre cette voie.
dkv
J'aime le large éventail de réponses différentes que cette question a généré.
Oskar Skog

Réponses:

27

Non, ça ne marchera pas. Les deux caractères #!doivent absolument être les deux premiers caractères du fichier (comment spécifieriez-vous de toute façon ce qui a interprété l'instruction if?). Ceci constitue le "nombre magique" que la exec()famille de fonctions détecte lorsqu'elles déterminent si un fichier qu'elles sont sur le point d'exécuter est un script (qui a besoin d'un interprète) ou un fichier binaire (qui ne le fait pas).

Le format de la ligne shebang est assez strict. Il doit avoir un chemin absolu vers un interprète et tout au plus un argument.

Ce que vous pouvez faire, c'est utiliser env:

#!/usr/bin/env interpreter

Maintenant, le chemin vers envest généralement /usr/bin/env , mais techniquement, ce n'est pas une garantie.

Cela vous permet d'ajuster la PATHvariable d'environnement sur chaque système afin interpreter(que ce soit bash, pythonou , perlou tout ce que vous avez) est trouvé.

Un inconvénient de cette approche est qu'il sera impossible de transmettre un argument de manière portable à l'interprète.

Cela signifie que

#!/usr/bin/env awk -f

et

#!/usr/bin/env sed -f

ne fonctionnera probablement pas sur certains systèmes.

Une autre approche évidente consiste à utiliser les autotools GNU (ou un système de modélisation plus simple) pour trouver l'interpréteur et placer le chemin correct dans le fichier en une ./configureétape, qui serait exécutée lors de l'installation du script sur chaque système.

On pourrait également recourir à l'exécution du script avec un interpréteur explicite, mais c'est évidemment ce que vous essayez d'éviter:

$ sed -f script.sed
Kusalananda
la source
Bon, je me rends compte que cela #!doit venir au début, car ce n'est pas le shell qui traite cette ligne. Je me demandais s'il y avait un moyen de mettre de la logique à l' intérieur de la ligne de hachage qui serait équivalent à l'if / else. J'espérais également éviter de déconner avec mon PATHmais je suppose que ce sont mes seules options.
dkv
1
Lorsque vous utilisez #!/usr/bin/awk, vous pouvez fournir exactement un argument, comme #!/usr/bin/awk -f. Si le binaire que vous pointez est env, l'argument est le binaire que vous demandez env, comme dans #!/usr/bin/env awk.
DopeGhoti
2
@dkv Ce n'est pas le cas. Il utilise un interpréteur avec deux arguments, et il peut fonctionner sur certains systèmes, mais certainement pas sur tous.
Kusalananda
3
@dkv sur Linux, il fonctionne /usr/bin/envavec le seul argument awk -f.
ilkkachu
1
@Kusalananda, non, c'était le point. Si vous avez un script appelé foo.awkavec la ligne hashbang #!/usr/bin/env awk -fet appelez-le avec ./foo.awkalors, sur Linux, ce qui envvoit , ce sont les deux paramètres awk -fet ./foo.awk. Il va en fait chercher /usr/bin/awk -f(etc.) avec un espace.
ilkkachu
27

Vous pouvez toujours créer un script wrapper pour trouver l'interpréteur correct pour le programme réel:

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

Enregistrez le wrapper dans les utilisateurs PATHsous programet mettez le programme réel de côté ou sous un autre nom.

J'ai utilisé #!/bin/bashdans le hashbang à cause du flagstableau. Si vous n'avez pas besoin de stocker un nombre variable d'indicateurs ou autre et que vous pouvez vous en passer, le script devrait fonctionner de manière portable avec #!/bin/sh.

ilkkachu
la source
2
J'ai vu exec "$interpreter" "${flags[@]}" "$script" "$@"aussi utilisé pour garder l'arbre de processus plus propre. Il propage également le code de sortie.
rrauenza
@rrauenza, ah oui, naturellement avec exec.
ilkkachu
1
Ce ne serait pas #!/bin/shmieux au lieu de #!/bin/bash? Même s'il /bin/shs'agit d'un lien symbolique vers un autre shell, il devrait exister sur la plupart (sinon tous) les systèmes * nix, et il forcerait l'auteur du script à créer un script portable plutôt que de tomber dans des bashismes.
Sergiy Kolodyazhnyy
@SergiyKolodyazhnyy, heh, j'ai pensé à le mentionner plus tôt, mais je ne l'ai pas fait. Le tableau utilisé pour flagsest une fonctionnalité non standard, mais il est suffisamment utile pour stocker un nombre variable d'indicateurs, j'ai donc décidé de le conserver.
ilkkachu
Ou utilisez / bin / sh et il suffit d' appeler l'interprète directement dans chaque branche: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". Vous pouvez également ajouter une vérification d'erreur pour les échecs d'exécution.
jrw32982 prend en charge Monica
11

Vous pouvez également écrire un polyglotte (combiner deux langues). / bin / sh est garanti d'exister.

Cela a l'inconvénient d'un code laid et peut-être que certains /bin/shs pourraient être confus. Mais il peut être utilisé lorsqu'il envn'existe pas ou existe ailleurs que / usr / bin / env. Il peut également être utilisé si vous souhaitez faire une sélection assez sophistiquée.

La première partie du script détermine l'interpréteur à utiliser lorsqu'il est exécuté avec / bin / sh comme interprète, mais est ignoré lorsqu'il est exécuté par l'interpréteur approprié. Utilisez execpour empêcher le shell de fonctionner plus que la première partie.

Exemple 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
la source
3
Je pense que j'en ai déjà vu un, mais l'idée est toujours aussi horrible ... Mais, vous voulez probablement aussi donner exec "$interpreter" "$0" "$@"le nom du script à l'interprète lui-même. (Et puis j'espère que personne n'a menti lors de l'installation $0.)
ilkkachu
6
Scala prend en charge les scripts polyglottes dans sa syntaxe: si un script Scala commence par #!, Scala ignore tout jusqu'à une correspondance !#; cela vous permet de mettre du code de script arbitrairement complexe dans un langage arbitraire, puis execle moteur d'exécution Scala avec le script.
Jörg W Mittag
1
@ Jörg W Mittag: +1 pour Scala
jrw32982 prend en charge Monica
2

Je préfère les réponses de Kusalananda et d'ilkkachu, mais voici une réponse alternative qui fait plus directement ce que la question demandait, tout simplement parce qu'elle a été posée.

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

if True:
  print("hello world!")

Notez que vous ne pouvez le faire que lorsque l'interpréteur autorise l'écriture de code dans le premier argument. Ici, -eet tout ce qui suit est pris mot pour mot comme un argument pour ruby. Pour autant que je sache, vous ne pouvez pas utiliser bash pour le code shebang, car bash -cnécessite que le code soit dans un argument séparé.

J'ai essayé de faire la même chose avec python pour le code 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

mais cela s'avère trop long et linux (au moins sur ma machine) tronque le shebang à 127 caractères. Veuillez excuser l'utilisation de execpour insérer des sauts de ligne car python ne permet pas les essais-exceptions ou les imports sans sauts de ligne.

Je ne sais pas à quel point c'est portable, et je ne le ferais pas avec du code destiné à être distribué. Néanmoins, c'est faisable. Peut-être que quelqu'un le trouvera utile pour un débogage rapide et sale ou quelque chose.

JoL
la source
2

Bien que cela ne sélectionne pas l'interpréteur dans le script shell (il le sélectionne par machine), c'est une alternative plus facile si vous avez un accès administratif à toutes les machines sur lesquelles vous essayez d'exécuter le script.

Créez un lien symbolique (ou un lien dur si vous le souhaitez) pour pointer vers le chemin d'interpréteur souhaité. Par exemple, sur mon système, perl et python sont dans / usr / bin:

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

créerait un lien symbolique pour permettre au hashbang de se résoudre pour / bin / perl, etc. Cela préserve également la possibilité de passer des paramètres aux scripts.

Rob
la source
1
+1 C'est tellement simple. Comme vous le constatez, cela ne répond pas tout à fait à la question, mais il semble faire exactement ce que le PO souhaite. Bien que je suppose que l'utilisation d'env contourne l'accès root sur chaque problème de machine.
Joe
0

J'ai été confronté à un problème similaire comme celui-ci aujourd'hui ( python3pointant vers une version de python qui était trop ancienne sur un système), et j'ai trouvé une approche qui est un peu différente de celles discutées ici: Utilisez la "mauvaise" version de python pour démarrer dans le "bon". La limitation est qu'une certaine version de python doit être accessible de manière fiable, mais cela peut généralement être réalisé par exemple #!/usr/bin/env python3.

Donc ce que je fais, c'est commencer mon script avec:

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

Ce que cela signifie:

  • Vérifiez la version interprète pour certains critères d'acceptation
  • Si ce n'est pas acceptable, parcourez une liste de versions candidates et réexécutez-la avec la première d'entre elles disponible
microtherion
la source