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

16

Estou em uma situação um pouco interessante, onde 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. Quero que esse script seja executável no maior número possível, sem restrições artificiais. Aqui estão algumas configurações conhecidas:

  • Python 2.6 é a versão do sistema em Python, portanto, python, python2 e python2.6 existem em / usr / bin (e são equivalentes).
  • Python 2.6 é a versão do sistema Python, como acima, mas o Python 2.7 é instalado ao lado dele como python2.7.
  • 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 python executável em todos os três. Seria bom se ele tentasse usar o /usr/bin/python2.7 primeiro, se existir, volte para /usr/bin/python2.6 e depois para /usr/bin/python2.5, depois simplesmente erro se nenhum deles estiver presente. Não estou muito empolgado com isso usando o 2.x mais recente possível, desde que seja capaz de 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 intérprete correto e execute o script python usando o interpretador encontrado. Eu consideraria um incômodo distribuir dois arquivos onde um seria suficiente, desde que seja executado com o intérprete python 2 mais atualizado instalado. Pedir às pessoas que invoquem o intérprete explicitamente (por exemplo, $ python2.5 script.py) não é uma opção. Confiar no PATH do usuário sendo configurado de uma certa maneira também não é uma opção.

Editar:

A verificação de versão no script Python não funcionará, pois estou usando a instrução "with" que existe no Python 2.6 (e pode ser usada no 2.5 com from __future__ import with_statement). Isso faz com que o script falhe imediatamente com um SyntaxError hostil ao usuário e me impede de ter 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')
user108471
fonte
Não é exatamente o que você deseja, portanto, comente. Mas você pode usar import sys; sys.version_info()para verificar se o usuário tem a versão python necessária.
Bernhard
2
@ Bernhard Sim, isso é verdade, mas até então é tarde demais para fazer algo sobre isso. Para a terceira situação listada acima, a execução direta do script (ou seja, ./script.py) faria com que o python2.4 o executasse, o que faria com que o seu código detetasse que era a versão errada (e saia, presumivelmente). Mas há um python2.5 perfeitamente bom que poderia ter sido usado como intérprete!
user108471
2
Use um script wrapper para descobrir se existe um python apropriado e exec, se houver, imprima um erro.
26413 Kevin
1
Então faça isso primeiro no mesmo arquivo.
26413 Kevin
9
@ user108471: Você está assumindo que a linha shebang é tratada pelo bash. Não é, é uma chamada de sistema ( execve). Os argumentos são literais de string, sem globbing, sem regexps. É isso aí. Mesmo se o primeiro argumento for "/ bin / bash" e a segunda opção ("-c ..."), essas opções não serão analisadas por um shell. Eles são entregues sem tratamento para o executável do bash, e é por isso que você obtém esses erros. Além disso, o shebang só funciona se for no começo. Acho que você está sem sorte aqui (com exceção de um script que encontra um interpretador python e o alimenta com um documento HERE, que soa como uma bagunça terrível).
GOLDILOCKS

Respostas:

13

Não sou especialista, mas acredito que você não deve especificar a versão exata do python a ser usada e deixar essa opção para o sistema / usuário.

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

#!/usr/bin/env python

ou

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

É recomendado pelo documento Python em todas as versões:

Uma boa escolha é geralmente

#!/usr/bin/env python

que procura o intérprete Python em todo o PATH. No entanto, alguns Unices podem não ter o comando env, portanto, você pode precisar codificar / usr / bin / python como o caminho do interpretador.

Em várias distribuições, o Python pode ser instalado em locais diferentes, portanto, ele envserá procurado em PATH. Ele deve estar disponível em todas as principais distribuições Linux e pelo que vejo no FreeBSD.

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

Se o seu script é compatível com todas as versões do Python, exceto a 2.4, você deve 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 ser instalado em diferentes sistemas.
  • Aqui você pode encontrar algumas vantagens e desvantagens de usar env.
  • Aqui você pode encontrar exemplos de manipulação PATH e diferentes resultados.

Nota de rodapé

* No Gentoo, existe uma ferramenta chamada eselect. Usando-o, você pode definir versões padrão de diferentes aplicativos (incluindo 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
pbm
fonte
2
Compreendo que o que eu quero fazer seja contrário ao que seria considerado uma boa prática. O que você postou é inteiramente razoável, mas, ao mesmo tempo, não é o que estou pedindo. Eu não quero que meus usuários apontem explicitamente meu script para a versão apropriada do Python quando for perfeitamente possível detectar a versão apropriada do Python em todas as situações que me interessam.
user108471
1
Por favor, veja minha atualização para saber por que não consigo "apenas verificar dentro dela se ela é executada no Python 2.4 e imprimir algumas informações e sair".
user108471
Você está certo. Eu encontrei esta pergunta sobre SO e agora eu posso ver que não há nenhuma opção de fazer isso se você quer ter apenas um arquivo ...
PBM
9

Com base em algumas idéias de alguns comentários, consegui juntar um truque realmente feio que parece funcionar. O script se torna um script bash que envolve um script Python e o passa para um intérprete Python através de um "documento aqui".

No inicio:

#!/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 está 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 adicionei também permite que esse mesmo script seja importado como um módulo Python (que eu uso para fins de teste). Quando importado pelo Python, tudo entre a primeira e a segunda aspas simples triplas é interpretado como uma sequência no nível do módulo, e a terceira aspas simples triplas é comentada. O resto é Python comum.

Quando executadas diretamente (como um script bash agora), as duas primeiras aspas simples se tornam uma sequência vazia, e a terceira aspas simples forma outra sequência com a quarta aspas simples, contendo apenas dois pontos. Essa sequência é interpretada pelo Bash como não operacional. Tudo o resto é a sintaxe do Bash para observar os binários Python em / usr / bin, selecionar o último e executar exec, passando o restante do arquivo como um documento aqui. O documento aqui começa com uma citação tripla entre Python contendo apenas um sinal de hash / pound / octothorpe. O restante do script é interpretado como normal até que a linha que lê '# EOF' encerre o documento aqui.

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

user108471
fonte
Talvez não seja tão desagradável, afinal;) ​​+1
goldilocks 27/02
A desvantagem desta situação é que ele vai estragar a sintaxe de coloração na maioria dos editores
Lie Ryan
@LieRyan Isso depende. Meu script usa uma extensão de arquivo .py, que a maioria dos editores de texto prefere ao escolher qual sintaxe usar para colorir. Hipoteticamente, se eu fosse para mudar o nome deste sem a extensão .py, eu poderia usar um modeline sugerir a sintaxe correta (para usuários do Vim pelo menos) com algo como: # ft=python.
user108471
7

A linha shebang pode especificar apenas um caminho fixo para um intérprete. Existe o #!/usr/bin/envtruque para procurar o intérprete no PATHmas é isso. Se você quiser mais sofisticação, precisará escrever algum código de shell do wrapper.

A solução mais óbvia é escrever um script de wrapper. Chame o script python foo.reale crie um script 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ê deseja colocar tudo em um arquivo, geralmente pode torná-lo um poliglota que começa com uma #!/bin/shlinha (que 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). No 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 seqüência de caracteres Python no nível superior, que não tem efeito. Para o shell, a segunda linha é a ''':'que, após retirar as aspas, é o comando no-op :.)

Gilles 'SO- parar de ser mau'
fonte
A segunda solução é boa, pois não requer adição # EOFno final, como nesta resposta . Sua abordagem é basicamente a mesma descrita aqui .
sschuberth
6

Como seus requisitos indicam uma lista conhecida de binários, você pode fazê-lo no Python da seguinte forma. Não funcionaria além de uma versão menor / maior de um dígito do Python, mas não vejo isso acontecer tão cedo.

Executa a versão mais alta localizada no disco da lista crescente e ordenada de pythons com versão, 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 ordenada" é a parte importante desse 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 arranhado

Matt
fonte
não continuaria correndo após a execução? coisas tão normais seriam executadas duas vezes?
Janus Troelsen
1
execv substitui o programa em execução no momento por uma imagem de programa recém-carregada #
26/02 Matt
Parece uma ótima solução que parece muito menos feia do que a que eu criei. Vou ter que tentar ver se funciona para esse fim.
user108471
Essa sugestão quase funciona para o que eu preciso. A única falha é algo que eu não mencionei originalmente: para suportar o Python 2.5, eu uso from __future__ import with_statement, que deve ser a primeira coisa no script Python. Acho que você não conhece uma maneira de executar essa ação ao iniciar o novo intérprete?
user108471
Tem certeza que ele precisa ser o muito primeira coisa? ou imediatamente antes de tentar usar qualquer withs como uma importação normal ?. Um if mypy == 25: from __future__ import with_statementtrabalho extra logo antes de 'coisas normais'? Você provavelmente não precisa do if, se não suportar o 2.4.
27413 Matt
0

Você pode escrever um pequeno script bash que verifique o executável phython disponível e o chame com o script como parâmetro. Você pode fazer desse script o destino 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 indiretamente, mas verifiquei e funciona.

Editar 1

Para tornar essa imperceptível 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, precisará copiar os scripts juntos novamente). Ou você cria um arquivo temporário em, por exemplo, / tmp ou (se o python suportar isso, eu não sei), você fornece o script como entrada para o 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
fonte
Esta é mais ou menos a solução que já foi afirmada no último parágrafo da pergunta!
Bernhard
Inteligente, mas exige que esse script mágico seja instalado em algum lugar de qualquer sistema.
user108471
@ Bernhard Oooops, pego. No futuro, vou ler até o fim. Como compensação, vou aprimorá-lo para uma solução de um arquivo.
Hauke ​​Laging
@ user108471 O script mágico pode conter algo assim: $(ls /usr/bin/python?.? | tail -n1 )mas não consegui usá-lo de maneira inteligente em um shebang.
Bernhard
@ Bernhard Você quer fazer a pesquisa dentro da linha shebang? IIRC, o kernel não se preocupa em citar na linha shebang. Caso contrário (se isso mudou enquanto isso), alguém poderia fazer algo como `#! / Bin / bash -c do_search_here_without_whitespace ...; exec $ python" $ 1 "Mas como fazer isso sem espaço em branco?
Hauke ​​Laging