Como posso ter mais de uma possibilidade na linha shebang de um script?

14

Eu estou em uma situação interessante, onde eu tenho um script Python que teoricamente pode ser executado por uma variedade de usuários com uma variedade de ambientes (e PATHs) e em uma variedade de sistemas Linux. Eu quero que este script seja executável no maior número possível sem restrições artificiais. Aqui estão algumas configurações conhecidas:

  • O Python 2.6 é a versão do sistema Python, portanto, python, python2 e python2.6, todos existem em / usr / bin (e são equivalentes).
  • O Python 2.6 é a versão do sistema Python, como acima, mas o Python 2.7 é instalado junto com ele como python2.7.
  • O Python 2.4 é a versão do sistema Python, que meu script não suporta. Em / usr / bin, temos python, python2 e python2.4, que são equivalentes, e python2.5, que o script suporta.

Eu quero executar o mesmo script executável de python em todos esses três. Seria legal se ele tentasse usar o /usr/bin/python2.7 primeiro, se ele existir, então volte para /usr/bin/python2.6, então volte para /usr/bin/python2.5, então simplesmente errar se nenhum deles estivesse presente. Eu não estou muito ligado a ele usando o 2.x mais recente possível, desde que ele consiga encontrar um dos intérpretes corretos, se presente.

Minha primeira inclinação foi mudar a linha shebang de:

#!/usr/bin/python

para

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

pois isso funciona bem no bash. Mas a execução do script fornece:

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

Ok, então eu tento o seguinte, que também funciona no bash:

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

Mas, novamente, isso falha com:

/bin/bash: - : invalid option

Ok, obviamente eu poderia escrever um script de shell separado que encontre o interpretador correto e execute o script python usando qualquer interpretador encontrado. Eu apenas acho um problema distribuir dois arquivos onde um deve ser suficiente, desde que seja executado com o mais atualizado intérprete do python 2 instalado. Pedir que as pessoas invoquem o interpretador explicitamente (por exemplo, $ python2.5 script.py ) não é uma opção. Confiar no PATH do usuário que está sendo configurado de certa maneira também não é uma opção.

Editar:

A verificação de versão dentro do script Python está não funcionando, já que estou usando a instrução "with" que existe no Python 2.6 (e pode ser usada em 2.5 com from __future__ import with_statement ). Isso faz com que o script falhe imediatamente com um SyntaxError hostil ao usuário e impede que eu tenha a oportunidade de verificar a versão primeiro e emitir um erro apropriado.

Exemplo: (tente isso com um interpretador 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')
    
por user108471 26.02.2013 / 21:40

5 respostas

9

Com base em algumas idéias de alguns comentários, consegui montar um hack realmente feio que parece funcionar. O script se torna um script bash que envolve um script Python e o transmite para um interpretador Python por meio de um "documento aqui".

No começo:

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

O código Python vai aqui. Então, no final:

# EOF

Quando o usuário executa o script, a versão mais recente do Python entre 2.5 e 2.7 é usada para interpretar o restante do script como um documento aqui.

Uma explicação sobre algumas travessuras:

O material de aspas triplas que eu adicionei também permite que esse mesmo script seja importado como um módulo Python (que eu uso para propósitos de teste). Quando importado pelo Python, tudo entre o primeiro e o segundo aspas simples é interpretado como uma string no nível do módulo, e o terceiro termo entre aspas simples é comentado. O resto é Python comum.

Quando executado diretamente (como um script bash agora), as duas primeiraspas simples se tornam uma string vazia, e a terceira aspas simples forma outra string com a quarta aspa simples, contendo apenas dois-pontos. Essa string é interpretada pelo Bash como não-op. Todo o resto é a sintaxe Bash para globbing os binários do Python em / usr / bin, selecionando o último e executando exec, passando o resto do arquivo como um documento aqui. O documento aqui começa com um aspas simples em Python contendo apenas um sinal de hash / libra / octothorpe. O restante do script é interpretado como normal até que a linha '# EOF' termine o documento aqui.

Eu sinto que isso é perverso, então espero que alguém tenha uma solução melhor.

    
por 26.02.2013 / 22:51
11

Eu não sou especialista, mas acredito que você não deveria especificar a versão exata do Python para usar e deixar essa escolha para o sistema / usuário.

Além disso, você deve usar isso em vez do caminho de hardcoding para python no script:

#!/usr/bin/env python

ou

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

recomendado by Python doc em todas as versões:

A good choice is usually

#!/usr/bin/env python

which searches for the Python interpreter in the whole PATH. However, some Unices may not have the env command, so you may need to hardcode /usr/bin/python as the interpreter path.

Em várias distribuições, o Python pode ser instalado em lugares diferentes, portanto env irá procurá-lo em PATH . Ele deve estar disponível em todas as principais distribuições Linux e pelo que eu vejo no FreeBSD.

O script deve ser executado com essa versão do Python que está no seu PATH e que é escolhido pela sua distribuição *.

Se o seu script é compatível com todas as versões do Python, exceto o 2.4, você deve apenas verificar dentro dele se ele é executado no Python 2.4 e imprimir algumas informações e sair.

Mais para ler

  • Aqui você pode encontrar exemplos em quais locais o Python pode estar instalado em sistemas diferentes.
  • Aqui você pode encontrar algumas vantagens e desvantagens para usar env .
  • Aqui você pode encontrar exemplos de manipulação do PATH e resultados diferentes.

Nota de rodapé

* No Gentoo existe uma ferramenta chamada eselect . Ao usá-lo, você pode definir as versões padrão de diferentes aplicativos (incluindo o Python) como padrão:

$ 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
    
por 26.02.2013 / 22:31
7

A linha shebang só pode especificar um caminho fixo para um intérprete. Há o truque #!/usr/bin/env para procurar o interpretador no PATH , mas é isso. Se você quiser mais sofisticação, precisará escrever um código shell para o wrapper.

A solução mais óbvia é escrever um script de wrapper. Chame o script python foo.real e crie um script de wrapper foo :

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

Se você quiser colocar tudo em um arquivo, muitas vezes você pode torná-lo um poliglota que começa com uma linha #!/bin/sh (assim será executada pelo shell), mas também é um script válido em outro idioma. Dependendo do idioma, um poliglota pode ser impossível (se #! causar um erro de sintaxe, por exemplo). Em Python, não é muito 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 …

(O texto inteiro entre ''' e ''' é uma string Python no nível superior, o que não tem efeito. Para o shell, a segunda linha é ''':' , que depois das cotações é o comando no-op : .)

    
por 27.02.2013 / 02:48
6

Conforme seus requisitos indicarem uma lista conhecida de binários, você poderá fazê-lo em Python com o seguinte. Não funcionaria após uma versão menor / maior de um dígito do Python, mas não vejo isso em breve.

Executa a versão mais alta localizada no disco a partir da lista ordenada e crescente de pitons versionados, se a versão marcada no binário for maior que a versão atual da execução do python. A "lista crescente de versões ordenadas" é o bit importante para 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")

Desculpas pelo meu python áspero

    
por 27.02.2013 / 00:30
0

Você pode escrever um pequeno script bash que verifica o executável phython disponível e o chama com o script como parâmetro. Você pode então fazer deste script o alvo da linha shebang:

#!/my/python/search/script

E esse script simplesmente faz (após a pesquisa):

"$python_path" "$1"

Eu não tinha certeza se o kernel aceitaria esse script indireto, mas eu verifiquei e funciona.

Editar 1

Para tornar esta imperfeição embaraçosa uma boa proposta finalmente:

É possível combinar os dois scripts em um arquivo. Você acabou de escrever o script python como um documento aqui no script bash (se você alterar o script python você só precisa copiar os scripts juntos novamente). Você cria um arquivo temporário em, e. / tmp ou (se o python suportar isso, eu não sei) você fornece o script como entrada para o interpretador:

# 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
    
por 26.02.2013 / 22:08