Escolha o intérprete após o início do script, por exemplo, if / else dentro de hashbang

16

Existe alguma maneira de escolher dinamicamente o intérprete que está executando um script? Eu tenho um script que estou executando em dois sistemas diferentes, e o intérprete que eu quero usar está localizado em locais diferentes nos dois sistemas. O que eu preciso fazer é mudar a linha de hashbang toda vez que mudar. Eu gostaria de fazer algo que seja o equivalente lógico disso (eu percebo que essa construção exata é impossível):

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

#Rest of script goes here

Ou melhor ainda, é isso, para tentar usar o primeiro intérprete e, se não encontrar, usa o segundo:

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

#Rest of script goes here

Obviamente, posso executá-lo como /path/to/python/on/systemA myscript.py ou /path/on/systemB myscript.py dependendo de onde estou, mas na verdade tenho um script de wrapper que é iniciado myscript.py, portanto, gostaria de especificar o caminho para o interpretador python de forma programática e não manualmente.

dkv
fonte
3
passar o 'restante do script' como um arquivo para o intérprete sem shebang e usar a ifcondição não é uma opção para você? if something; then /bin/sh restofscript.sh elif...
mazs 16/05
Eu também considerei uma opção, mas um pouco mais bagunçada do que gostaria. Como a lógica na linha hashbang é impossível, acho que vou realmente seguir esse caminho.
Dkv 16/05/19
Gosto da grande variedade de respostas diferentes que essa pergunta gerou.
Oskar Skog

Respostas:

27

Não, isso não vai funcionar. Os dois caracteres #!absolutamente precisam ser os dois primeiros no arquivo (como você especificaria o que interpretou a instrução if?). Isso constitui o "número mágico" que a exec()família de funções detecta quando determina se um arquivo que está prestes a executar é um script (que precisa de um intérprete) ou um arquivo binário (que não).

O formato da linha shebang é bastante rigoroso. Ele precisa ter um caminho absoluto para um intérprete e, no máximo, um argumento para ele.

O que você pode fazer é usar env:

#!/usr/bin/env interpreter

Agora, o caminho para isso envé geralmente /usr/bin/env , mas tecnicamente isso não garante.

Isso permite que você ajustar a PATHvariável de ambiente em cada sistema para que interpreter(seja ele bash, pythonou perlou o que você tem) é encontrado.

Uma desvantagem dessa abordagem é que será impossível passar um argumento de maneira portável para o intérprete.

Isso significa que

#!/usr/bin/env awk -f

e

#!/usr/bin/env sed -f

é improvável que funcione em alguns sistemas.

Outra abordagem óbvia é usar as ferramentas automáticas GNU (ou algum sistema de modelos mais simples) para encontrar o intérprete e colocar o caminho correto no arquivo em uma ./configureetapa, que seria executada na instalação do script em cada sistema.

Pode-se também recorrer à execução do script com um intérprete explícito, mas é obviamente isso que você está tentando evitar:

$ sed -f script.sed
Kusalananda
fonte
Certo, eu sei que isso #!precisa acontecer no começo, já que não é o shell que processa essa linha. Eu queria saber se existe uma maneira de colocar lógica dentro da linha hashbang que seria equivalente ao if / else. Eu também esperava evitar mexer com o meu, PATHmas acho que essas são minhas únicas opções.
Dkv 16/05/19
11
Ao usar #!/usr/bin/awk, você pode fornecer exatamente um argumento, como #!/usr/bin/awk -f. Se o binário que você está apontando for env, o argumento é o binário que você está pedindo envpara procurar, como em #!/usr/bin/env awk.
DopeGhoti 16/05
2
@dkv Não é. Ele usa um intérprete com dois argumentos e pode funcionar em alguns sistemas, mas definitivamente não em todos.
Kusalananda
3
O @dkv no Linux roda /usr/bin/envcom o argumento único awk -f.
Ilkkachu
11
@ Kusalananda, não, esse era o ponto. Se você possui um script chamado foo.awkcom a linha hashbang #!/usr/bin/env awk -fe o chama ./foo.awk, no Linux, o que envvê são os dois parâmetros awk -fe ./foo.awk. Na verdade, ele procura /usr/bin/awk -f(etc.) com um espaço.
Ilkkachu 17/05/19
27

Você sempre pode criar um script de wrapper para encontrar o intérprete correto para o 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" "$@"

Salve o wrapper nos usuários PATHcomo programe colocar o programa real de lado ou com outro nome.

Eu usei #!/bin/bashno hashbang por causa da flagsmatriz. Se você não precisar armazenar um número variável de sinalizadores ou algo semelhante e puder ficar sem ele, o script deverá funcionar de maneira portável #!/bin/sh.

ilkkachu
fonte
2
Vi exec "$interpreter" "${flags[@]}" "$script" "$@"também usado para manter a árvore de processos mais limpa. Também propaga o código de saída.
Rrauenza 16/05/19
@ Rauenza, ah sim, naturalmente com exec.
Ilkkachu
11
Não #!/bin/shseria melhor em vez de #!/bin/bash? Mesmo que /bin/shseja um link simbólico para um shell diferente, ele deve existir na maioria dos sistemas (se não todos) * nix, além de forçar o autor do script a criar um script portátil, em vez de cair em basismos.
Sergiy Kolodyazhnyy 17/05/19
@SergiyKolodyazhnyy, heh, pensei em mencionar isso antes, mas não o fiz então. A matriz usada flagsé um recurso não padrão, mas é útil o suficiente para armazenar um número variável de sinalizadores, então decidi mantê-lo.
Ilkkachu 17/05/19
Ou use / bin / sh e apenas chamar o interpretador diretamente em cada ramo: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". Você também pode adicionar verificação de erros para falhas de execução.
jrw32982 suporta Monica 17/05
11

Você também pode escrever um poliglota (combinar dois idiomas). / bin / sh é garantido que existe.

Isso tem a desvantagem do código feio e talvez alguns /bin/shs possam se confundir. Mas pode ser usado quando envnão existe ou existe em outro lugar que não seja / usr / bin / env. Também pode ser usado se você quiser fazer uma seleção bastante sofisticada.

A primeira parte do script determina qual intérprete usar quando executado com / bin / sh como intérprete, mas é ignorado quando executado pelo intérprete correto. Use execpara impedir que o shell execute mais do que a primeira parte.

Exemplo 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
fonte
3
Acho que já vi um desses antes, mas a idéia ainda é igualmente horrível ... Mas, você provavelmente também quer exec "$interpreter" "$0" "$@"que o nome do script seja o próprio interpretador. (E então espero que ninguém mentiu ao configurar $0.)
ilkkachu
6
O Scala na verdade tem suporte para scripts poliglotas em sua sintaxe: se um script Scala começa com #!, Scala ignora tudo, até uma correspondência !#; isso permite que você coloque código de script arbitrariamente complexo em um idioma arbitrário e, em seguida, execo mecanismo de execução do Scala com o script.
Jörg W Mittag
11
@ Jörg W Mittag: +1 para Scala
jrw32982 suporta Monica
2

Prefiro as respostas de Kusalananda e ilkkachu, mas aqui está uma resposta alternativa que faz mais diretamente o que a pergunta estava pedindo, simplesmente porque foi feita.

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

if True:
  print("hello world!")

Observe que você só pode fazer isso quando o intérprete permite escrever código no primeiro argumento. Aqui, -ee tudo depois que é tomado literalmente como 1 argumento para ruby. Pelo que sei, você não pode usar o bash para o código shebang, porque bash -cexige que o código esteja em um argumento separado.

Eu tentei fazer o mesmo com python para 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

mas fica muito tempo e o linux (pelo menos na minha máquina) trunca o shebang para 127 caracteres. Por favor, desculpe o uso de execpara inserir novas linhas, pois o python não permite trechos de tentativa ou imports sem novas linhas.

Não tenho certeza de como isso é portátil e não o faria em código destinado a ser distribuído. No entanto, é factível. Talvez alguém ache útil para depuração rápida e suja ou algo assim.

JoL
fonte
2

Embora isso não selecione o intérprete no script de shell (ele o seleciona por máquina), é uma alternativa mais fácil se você tiver acesso administrativo a todas as máquinas nas quais está tentando executar o script.

Crie um link simbólico (ou um link físico, se desejado) para apontar para o caminho do intérprete desejado. Por exemplo, no meu sistema, perl e python estão em / usr / bin:

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

criaria um link simbólico para permitir que o hashbang resolva para / bin / perl, etc. Isso preserva a capacidade de passar parâmetros para os scripts também.

Roubar
fonte
11
+1 Isso é tão simples. Como você observa, isso não responde muito bem à pergunta, mas parece fazer exatamente o que o OP deseja. Embora eu ache que usar env contorna o acesso root em cada problema de máquina.
20917 Joe
0

Fui confrontado com um problema semelhante como esse hoje ( python3apontando para uma versão do python muito antiga em um sistema) e criei uma abordagem um pouco diferente das discutidas aqui: Use a versão "errada" do python para inicializar no "certo". A limitação é que algumas versões do python precisam ser acessíveis de maneira confiável, mas isso geralmente pode ser alcançado por exemplo #!/usr/bin/env python3.

Então, o que faço é iniciar meu script com:

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

O que isso faz é:

  • Verifique a versão do intérprete para obter algum critério de aceitação
  • Se não for aceitável, consulte uma lista de versões candidatas e execute-a novamente com a primeira delas disponível
microtherion
fonte