Come posso avere più di una possibilità nella riga shebang di uno script?


16

Sono in una situazione un po 'interessante in cui ho uno script Python che può teoricamente essere gestito da una varietà di utenti con una varietà di ambienti (e PATH) e su una varietà di sistemi Linux. Voglio che questo script sia eseguibile sul maggior numero possibile di questi senza restrizioni artificiali. Ecco alcune impostazioni note:

  • Python 2.6 è la versione di Python del sistema, quindi python, python2 e python2.6 esistono tutti in / usr / bin (e sono equivalenti).
  • Python 2.6 è la versione del sistema Python, come sopra, ma Python 2.7 è installato accanto ad esso come python2.7.
  • Python 2.4 è la versione del sistema Python, che il mio script non supporta. In / usr / bin abbiamo python, python2 e python2.4 che sono equivalenti e python2.5, che supporta lo script.

Voglio eseguire lo stesso script python eseguibile su tutti e tre questi. Sarebbe bello se tentasse di usare prima /usr/bin/python2.7, se esiste, quindi ripiegare su /usr/bin/python2.6, quindi ripiegare su /usr/bin/python2.5, quindi semplicemente errore se nessuno di quelli era presente. Tuttavia, non sono troppo preso dall'uso della versione 2.x più recente possibile, purché sia ​​in grado di trovare uno degli interpreti corretti, se presente.

La mia prima inclinazione era quella di cambiare la linea di shebang da:

#!/usr/bin/python

per

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

dal momento che funziona bene in bash. Ma eseguire lo script dà:

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

Va bene, quindi provo quanto segue, che funziona anche in bash:

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

Ma ancora una volta, questo non riesce con:

/bin/bash: - : invalid option

Ok, ovviamente potrei semplicemente scrivere uno script shell separato che trova l'interprete corretto ed esegue lo script Python usando qualunque interprete trovato. Troverei una seccatura distribuire due file dove uno dovrebbe bastare purché sia ​​eseguito con l'interprete Python 2 più aggiornato installato. Chiedere alle persone di invocare esplicitamente l'interprete (ad es. $ python2.5 script.py) Non è un'opzione. Fare affidamento sul PERCORSO dell'utente impostato in un determinato modo non è un'opzione.

Modificare:

Il controllo della versione all'interno dello script Python non funzionerà poiché sto usando l'istruzione "with" che esiste a partire da Python 2.6 (e può essere utilizzata in 2.5 con from __future__ import with_statement). Questo fa sì che lo script fallisca immediatamente con un SyntaxError ostile all'utente e mi impedisce di avere mai l'opportunità di controllare prima la versione ed emettere un errore appropriato.

Esempio: (prova questo con un interprete Python inferiore a 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')

Non proprio quello che vuoi, quindi commentare. Ma puoi usare import sys; sys.version_info()per verificare se l'utente ha la versione di Python richiesta.
Bernhard,

2
@Bernhard Sì, è vero, ma ormai è troppo tardi per fare qualcosa al riguardo. Per la terza situazione che ho elencato sopra, l'esecuzione diretta dello script (cioè, ./script.py) provocherebbe la sua esecuzione da parte di python2.4, facendo sì che il tuo codice rilevi che era la versione sbagliata (ed esce, presumibilmente). Ma c'è un python2.5 perfettamente valido che avrebbe potuto essere usato come interprete!
user108471,

2
Utilizzare uno script wrapper per capire se esiste un pitone appropriato e exec, in tal caso, stampare un errore.
Kevin,

1
Quindi fallo per prima cosa nello stesso file.
Kevin,

9
@ user108471: Stai supponendo che la linea shebang sia gestita da bash. Non lo è, è una chiamata di sistema ( execve). Gli argomenti sono letterali a stringhe, niente strappi, nessuna regexps. Questo è tutto. Anche se il primo argomento è "/ bin / bash" e le seconde opzioni ("-c ...") tali opzioni non vengono analizzate da una shell. Vengono consegnati non trattati all'eseguibile bash, motivo per cui si ottengono tali errori. Inoltre, lo shebang funziona solo se è all'inizio. Quindi sei sfortunato qui, temo (a corto di una sceneggiatura che trova un interprete Python e lo alimenta un documento QUI, che suona come un disastro terribile).
Riccioli d'oro,

Risposte:


13

Non sono esperto, ma credo che non dovresti specificare la versione esatta di Python da usare e lasciare quella scelta al sistema / utente.

Inoltre dovresti usare questo invece del percorso hardcoding a Python nello script:

#!/usr/bin/env python

o

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

È raccomandato dal documento Python in tutte le versioni:

Una buona scelta è di solito

#!/usr/bin/env python

che cerca l'interprete Python in tutto il PERCORSO. Tuttavia, alcuni Unices potrebbero non avere il comando env, quindi potrebbe essere necessario hardcode / usr / bin / python come percorso dell'interprete.

In varie distribuzioni Python può essere installato in luoghi diversi, quindi lo envcercheràPATH . Dovrebbe essere disponibile in tutte le principali distribuzioni Linux e da quello che vedo in FreeBSD.

Lo script dovrebbe essere eseguito con quella versione di Python che si trova nel PERCORSO e che è stata scelta dalla tua distribuzione *.

Se il tuo script è compatibile con tutte le versioni di Python tranne la 2.4, dovresti semplicemente controllare al suo interno se è eseguito in Python 2.4 e stampare alcune informazioni ed uscire.

Altro da leggere

  • Qui puoi trovare esempi in quali luoghi Python potrebbe essere installato in diversi sistemi.
  • Qui puoi trovare alcuni vantaggi e svantaggi per l'uso env.
  • Qui puoi trovare esempi di manipolazione PATH e risultati diversi.

Nota

* In Gentoo c'è uno strumento chiamato eselect. Usandolo puoi impostare le versioni predefinite di diverse applicazioni (incluso Python) come predefinito:

$ 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

2
Apprezzo che ciò che voglio fare sia contrario a quella che sarebbe considerata una buona pratica. Quello che hai pubblicato è del tutto ragionevole, ma allo stesso tempo non è quello che sto chiedendo. Non voglio che i miei utenti debbano puntare esplicitamente il mio script sulla versione appropriata di Python quando è perfettamente possibile rilevare la versione appropriata di Python in tutte le situazioni a cui tengo.
user108471,

1
Si prega di vedere il mio aggiornamento per il motivo per cui non riesco a "controllare al suo interno se è eseguito in Python 2.4 e stampare alcune informazioni ed uscire".
user108471,

Hai ragione. Ho appena trovato questa domanda su SO e ora vedo che non esiste alcuna opzione per farlo se si desidera avere un solo file ...
pbm

9

Sulla base di alcune idee tratte da alcuni commenti, sono riuscito a mettere insieme un brutto hack che sembra funzionare. Lo script diventa uno script bash che avvolge uno script Python e lo passa a un interprete Python tramite un "documento qui".

All'inizio:

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

Il codice Python va qui. Quindi alla fine:

# EOF

Quando l'utente esegue lo script, la versione più recente di Python tra 2.5 e 2.7 viene utilizzata per interpretare il resto dello script come un documento qui.

Una spiegazione su alcuni shenanigans:

Le cose a tre virgolette che ho aggiunto consentono anche di importare lo stesso script come modulo Python (che utilizzo a scopo di test). Quando viene importato da Python, tutto tra la prima e la seconda virgoletta singola tripla viene interpretato come una stringa a livello di modulo e la terza virgoletta tripla singola viene commentata. Il resto è Python ordinario.

Quando eseguito direttamente (come uno script bash ora), le prime due virgolette singole diventano una stringa vuota e la terza virgoletta singola costituisce un'altra stringa con la quarta virgoletta singola, contenente solo due punti. Questa stringa è interpretata da Bash come no-op. Tutto il resto è sintassi di Bash per alterare i binari di Python in / usr / bin, selezionare l'ultimo ed eseguire exec, passando il resto del file come documento qui. Il documento qui inizia con una tripla virgoletta Python contenente solo un segno hash / pound / octothorpe. Il resto dello script viene quindi interpretato come normale fino a quando la riga "# EOF" termina il documento qui.

Sento che questo è perverso, quindi spero che qualcuno abbia una soluzione migliore.


Forse non è poi così brutto;) +1
Riccioli d'oro

Lo svantaggio di questo è che rovinerà la colorazione della sintassi sulla maggior parte degli editor
Lie Ryan,

@LieRyan Dipende. Il mio script utilizza un'estensione di file .py, che la maggior parte degli editor di testo preferisce quando sceglie quale sintassi usare per colorare. Ipoteticamente, se dovessi rinominare questo senza l'estensione .py, ho potuto utilizzare una modeline per suggerire la sintassi corretta (per gli utenti di Vim almeno) con qualcosa come: # ft=python.
user108471,

7

La linea shebang può specificare solo un percorso fisso per un interprete. C'è il #!/usr/bin/envtrucco per cercare l'interprete nel PATHma questo è tutto. Se vuoi maggiore raffinatezza, dovrai scrivere del codice shell wrapper.

La soluzione più ovvia è scrivere uno script wrapper. Chiama lo script python foo.reale crea uno 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 vuoi mettere tutto in un file, spesso puoi trasformarlo in un poliglotta che inizia con una #!/bin/shlinea (quindi verrà eseguita dalla shell) ma è anche uno script valido in un'altra lingua. A seconda della lingua, un poliglotta può essere impossibile (se #!, ad esempio, causa un errore di sintassi). In Python, non è molto difficile.

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

(L'intero testo tra '''e '''è una stringa Python al livello superiore, che non ha alcun effetto. Per la shell, la seconda riga è ''':'che dopo aver rimosso le virgolette è il comando no-op :.)


La seconda soluzione è piacevole in quanto non richiede l'aggiunta # EOFalla fine come in questa risposta . Il tuo approccio è sostanzialmente lo stesso delineato qui .
sschuberth,

6

Poiché i tuoi requisiti indicano un elenco noto di file binari, puoi farlo in Python con il seguente. Non funzionerebbe oltre una versione minore / maggiore a una cifra di Python, ma non vedo che ciò accadrà presto.

Esegue la versione più alta che si trova sul disco dall'elenco ordinato e crescente di pitoni con versione, se la versione taggata sul binario è superiore alla versione corrente dell'esecuzione di Python. L '"elenco crescente ordinato di versioni" è il bit importante per questo codice.

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

Mi scuso per il mio pitone graffiante


non continuerebbe a funzionare dopo l'esecuzione? così roba normale verrebbe eseguita due volte?
Janus Troelsen,

1
execv sostituisce il programma attualmente in esecuzione con un'immagine del programma appena caricata
Matt,

Questa sembra un'ottima soluzione che sembra molto meno brutta di quella che mi è venuta in mente. Dovrò provarlo per vedere se funziona a questo scopo.
user108471,

Questo suggerimento funziona quasi per quello di cui ho bisogno. L'unico difetto è qualcosa che non ho menzionato in origine: per supportare Python 2.5, io uso from __future__ import with_statement, che deve essere la prima cosa nello script Python. Immagino non ti capiti di conoscere un modo per eseguire quell'azione all'avvio del nuovo interprete?
user108471,

Sei sicuro che deve essere il molto prima cosa? o appena prima di provare a utilizzare qualsiasi withs come una normale importazione ?. Un if mypy == 25: from __future__ import with_statementlavoro extra prima delle "cose ​​normali"? Probabilmente non è necessario il se, se non si supporta 2.4.
Matt,

0

È possibile scrivere un piccolo script bash che controlla l'eseguibile phython disponibile e lo chiama con lo script come parametro. Puoi quindi rendere questo script il bersaglio della linea shebang:

#!/my/python/search/script

E questo script fa semplicemente (dopo la ricerca):

"$python_path" "$1"

Non ero sicuro che il kernel avrebbe accettato questo indiretto di script ma ho controllato e funziona.

Modifica 1

Per rendere finalmente questa imbarazzante impercettibilità una buona proposta:

È possibile combinare entrambi gli script in un file. Devi solo scrivere lo script python come documento qui nello script bash (se cambi lo script python devi solo copiare nuovamente gli script insieme). O crei un file temporaneo in eg / tmp o (se python lo supporta, non lo so) fornisci lo script come input per l'interprete:

# 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

Questa è più o meno la soluzione che era già stata dichiarata nell'ultimo paragrafo della domanda!
Bernhard,

Intelligente, ma richiede che questo script magico sia installato da qualche parte su ogni sistema.
user108471,

@Bernhard Oooops, catturato. In futuro leggerò fino alla fine. Come compensazione ho intenzione di migliorarlo in una soluzione a un file.
Hauke ​​Laging,

@ user108471 Lo script magico può contenere qualcosa del genere: $(ls /usr/bin/python?.? | tail -n1 )ma non sono riuscito a usarlo abilmente in uno shebang.
Bernhard,

@Bernhard Vuoi fare la ricerca all'interno della linea shebang? IIRC il kernel non si preoccupa di quotare nella riga shebang. Altrimenti (se nel frattempo questo è cambiato) si potrebbe fare qualcosa come `#! / Bin / bash -c do_search_here_without_whitespace ...; exec $ python" $ 1 "Ma come si fa senza spazi bianchi?
Hauke ​​Laging,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.