Jak mogę mieć więcej niż jedną możliwość w linii shebang skryptu?

16

Jestem w interesującej sytuacji, w której mam skrypt w języku Python, który teoretycznie może być uruchamiany przez różnych użytkowników w różnych środowiskach (i PATH) i na różnych systemach Linux. Chcę, aby ten skrypt był wykonywalny na tak wielu z nich, jak to możliwe bez sztucznych ograniczeń. Oto niektóre znane konfiguracje:

  • Python 2.6 jest systemową wersją Pythona, więc wszystkie python, python2 i python2.6 istnieją w katalogu / usr / bin (i są równoważne).
  • Python 2.6 to systemowa wersja Python, jak wyżej, ale Python 2.7 jest instalowany obok niego jako python2.7.
  • Python 2.4 to systemowa wersja Python, której mój skrypt nie obsługuje. W / usr / bin mamy python, python2 i python2.4, które są równoważne, i python2.5, które obsługuje skrypt.

Chcę uruchomić ten sam wykonywalny skrypt Pythona na wszystkich trzech z nich. Byłoby miło, gdyby najpierw próbował użyć /usr/bin/python2.7, jeśli istnieje, to wróć do /usr/bin/python2.6, a następnie wróć do /usr/bin/python2.5, a następnie po prostu pomiń błąd, jeśli żaden z nich nie był obecny. Nie jestem jednak zbytnio rozłączony przy użyciu najnowszej możliwej wersji 2.x, pod warunkiem, że jest w stanie znaleźć jednego z poprawnych tłumaczy, jeśli jest obecny.

Moją pierwszą skłonnością była zmiana linii shebang z:

#!/usr/bin/python

do

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

ponieważ działa to dobrze w bash. Ale uruchomienie skryptu daje:

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

Okej, więc wypróbowuję następujące, które działa również w bash:

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

Ale znowu to się nie udaje w przypadku:

/bin/bash: - : invalid option

Okay, oczywiście mógłbym napisać osobny skrypt powłoki, który znajdzie właściwy interpreter i uruchomi skrypt Pythona za pomocą dowolnego znalezionego interpretera. Po prostu uważam za kłopot z dystrybucją dwóch plików, w których jeden powinien wystarczyć, pod warunkiem, że jest uruchomiony z zainstalowanym najnowszym interpreterem języka Python 2. Poproszenie ludzi o jawne wywołanie tłumacza (np. $ python2.5 script.py) Nie wchodzi w grę. Poleganie na skonfigurowaniu PATH użytkownika w określony sposób również nie jest opcją.

Edytować:

Sprawdzanie wersji w skrypcie Python nie będzie działać, ponieważ używam instrukcji „z”, która istnieje od wersji Python 2.6 (i może być używana w wersji 2.5 z from __future__ import with_statement). Powoduje to, że skrypt natychmiast kończy się niepowodzeniem z nieprzyjaznym dla użytkownika błędem SyntaxError i uniemożliwia mi kiedykolwiek sprawdzenie najpierw wersji i wygenerowanie odpowiedniego błędu.

Przykład: (spróbuj tego z interpreterem języka Python mniejszym niż 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')
użytkownik108471
źródło
Niezupełnie to, czego chcesz, dlatego komentuj. Ale możesz użyć, import sys; sys.version_info()aby sprawdzić, czy użytkownik ma wymaganą wersję Pythona.
Bernhard
2
@Bernhard Tak, to prawda, ale do tego czasu jest już za późno, aby cokolwiek z tym zrobić. W przypadku trzeciej sytuacji, którą wymieniłem powyżej, uruchomienie skryptu bezpośrednio (tj. ./script.py) Spowodowałoby wykonanie go przez python2.4, co spowodowałoby, że kod wykryłby, że jest to niewłaściwa wersja (i prawdopodobnie,). Ale istnieje doskonale dobry python2.5, który mógłby zostać użyty jako interpreter!
user108471
2
Użyj skryptu opakowania, aby dowiedzieć się, czy istnieje odpowiedni python, a execjeśli tak, w przeciwnym razie wydrukuj błąd.
Kevin
1
Zrób to najpierw w tym samym pliku.
Kevin
9
@ user108471: Zakładasz, że linia shebang jest obsługiwana przez bash. To nie jest, to wywołanie systemowe ( execve). Argumenty to literały łańcuchowe, brak globowania, brak wyrażeń regularnych. Otóż ​​to. Nawet jeśli pierwszy argument to „/ bin / bash”, a drugie opcje („-c ...”), te opcje nie są analizowane przez powłokę. Są one przekazywane bez przetworzenia do pliku wykonywalnego bash, dlatego otrzymujesz te błędy. Dodatkowo shebang działa tylko wtedy, gdy jest na początku. Obawiam się, że nie masz szczęścia (brakuje skryptu, który znajduje interpreter języka Python i podaje go TUTAJ, co brzmi jak okropny bałagan).
goldilocks

Odpowiedzi:

13

Nie jestem ekspertem, ale uważam, że nie powinieneś określać dokładnej wersji Pythona do użycia i pozostawić ten wybór systemowi / użytkownikowi.

Powinieneś także użyć tej zamiast twardej ścieżki do Pythona w skrypcie:

#!/usr/bin/env python

lub

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

Jest zalecany przez Python doc we wszystkich wersjach:

Zazwyczaj jest to dobry wybór

#!/usr/bin/env python

który wyszukuje interpreter Pythona w całej ŚCIEŻCE. Jednak niektóre Unices mogą nie mieć komendy env, więc może być konieczne podanie na stałe kodu / usr / bin / python jako ścieżki interpretera.

W różnych dystrybucjach Python może być zainstalowany w różnych miejscach, więc envbędzie go szukał w PATH. Powinien być dostępny we wszystkich głównych dystrybucjach Linuksa oraz z tego, co widzę we FreeBSD.

Skrypt powinien być wykonywany z tą wersją Pythona, która jest w twojej ŚCIEŻCE i która jest wybrana przez twoją dystrybucję *.

Jeśli twój skrypt jest kompatybilny ze wszystkimi wersjami Pythona oprócz 2.4, powinieneś po prostu sprawdzić, czy jest uruchomiony w Pythonie 2.4 i wydrukować informacje i wyjść.

Więcej do przeczytania

  • Tutaj możesz znaleźć przykłady miejsc, w których Python może być zainstalowany w różnych systemach.
  • Tutaj znajdziesz niektóre zalety i wady użytkowania env.
  • Tutaj możesz znaleźć przykłady manipulacji PATH i różne wyniki.

Notatka

* W Gentoo istnieje narzędzie o nazwie eselect. Za jego pomocą możesz ustawić domyślne wersje różnych aplikacji (w tym Pythona) jako domyślne:

$ 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
źródło
2
Rozumiem, że to, co chcę zrobić, jest sprzeczne z dobrą praktyką. To, co napisałeś, jest całkowicie rozsądne, ale jednocześnie nie o to proszę. Nie chcę, aby moi użytkownicy musieli wyraźnie wskazywać skryptowi odpowiednią wersję Pythona, gdy jest możliwe wykrycie odpowiedniej wersji Pythona we wszystkich sytuacjach, na których mi zależy.
user108471
1
Proszę zobaczyć moją aktualizację, dlaczego nie mogę „po prostu sprawdzić w środku, czy jest uruchomiona w Pythonie 2.4 i wydrukować jakieś informacje i wyjść”.
user108471
Masz rację. Właśnie znalazłem to pytanie na SO i teraz widzę, że nie ma takiej możliwości, jeśli chcesz mieć tylko jeden plik ...
pbm
9

W oparciu o kilka pomysłów z kilku komentarzy udało mi się stworzyć naprawdę brzydki hack, który wydaje się działać. Skrypt staje się skryptem bashowym, który otacza skrypt w języku Python i przekazuje go do interpretera języka Python za pośrednictwem „dokumentu tutaj”.

Na początku:

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

Kod Pythona idzie tutaj. Na samym końcu:

# EOF

Gdy użytkownik uruchamia skrypt, do interpretacji pozostałej części skryptu jako dokumentu tutaj używana jest najnowsza wersja Pythona między 2.5 a 2.7.

Wyjaśnienie niektórych shenaniganów:

Dodane przeze mnie potrójne cytaty pozwalają również na import tego samego skryptu jako modułu Pythona (którego używam do celów testowych). Po zaimportowaniu przez Python wszystko pomiędzy pierwszym a drugim potrójnym pojedynczym cytatem jest interpretowane jako ciąg na poziomie modułu, a trzeci potrójny pojedynczy cytat jest komentowany. Reszta to zwykły Python.

Po uruchomieniu bezpośrednio (teraz jako skrypt bash) pierwsze dwa pojedyncze cudzysłowy stają się pustym ciągiem, a trzeci pojedynczy cudzysłów tworzy kolejny ciąg z czwartym pojedynczym cudzysłowem, zawierający tylko dwukropek. Ten ciąg interpretowany jest przez Bash jako brak możliwości działania. Cała reszta to składnia Bash do globowania plików binarnych Pythona w / usr / bin, wybierania ostatniego i uruchamiania exec, przekazując resztę pliku jako dokument tutaj. Niniejszy dokument zaczyna się od potrójnego pojedynczego cudzysłowu w języku Python zawierającego tylko znak krzyżyka / funta / oktotorpe. Pozostała część skryptu jest interpretowana normalnie, dopóki wiersz o treści „# EOF” nie zakończy dokumentu tutaj.

Czuję, że to jest przewrotne, więc mam nadzieję, że ktoś ma lepsze rozwiązanie.

użytkownik108471
źródło
Może jednak wcale nie jest taki paskudny;) +1
złotowłosa
Wadą tego jest to, że zepsuje kolorowanie składni większości edytorów
Lie Ryan
@LieRyan To zależy. Mój skrypt używa rozszerzenia nazwy pliku .py, które jest preferowane przez większość edytorów tekstu przy wyborze składni do kolorowania. Hipotetycznie, gdybym przemianować to bez rozszerzenia .py, mogłem użyć modeline podpowiedź poprawnej składni (dla użytkowników Vima przynajmniej) z czymś takim: # ft=python.
user108471
7

Linia shebang może określać tylko stałą ścieżkę do tłumacza. Jest #!/usr/bin/envsztuczka, by spojrzeć na tłumacza, PATHale to wszystko. Jeśli chcesz bardziej wyrafinować, musisz napisać kod powłoki otoki.

Najbardziej oczywistym rozwiązaniem jest napisanie skryptu opakowania. Wywołaj skrypt Pythona foo.reali stwórz skrypt otoki foo:

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

Jeśli chcesz umieścić wszystko w jednym pliku, często możesz uczynić go poliglotą, która zaczyna się od #!/bin/shlinii (więc zostanie wykonana przez powłokę), ale jest również poprawnym skryptem w innym języku. W zależności od języka poliglota może być niemożliwy ( #!na przykład powoduje błąd składniowy). W Pythonie nie jest to bardzo trudne.

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

(Cały tekst pomiędzy '''i '''jest ciągiem Pythona na najwyższym poziomie, co nie ma żadnego efektu. W przypadku powłoki drugim wierszem jest ''':'po usunięciu cudzysłowów polecenie no-op :.)

Gilles „SO- przestań być zły”
źródło
Drugie rozwiązanie jest fajne, ponieważ nie wymaga dodawania # EOFna końcu, jak w tej odpowiedzi . Twoje podejście jest w zasadzie takie samo, jak przedstawione tutaj .
sschuberth
6

Ponieważ wymagania określają znaną listę plików binarnych, możesz to zrobić w Pythonie, wykonując następujące czynności. Nie działałoby to w porównaniu z jednocyfrową wersją Pythona, ale nie widzę tego w najbliższym czasie.

Uruchamia najwyższą wersję znajdującą się na dysku z uporządkowanej, rosnącej listy wersjonowanych pytonów, jeśli wersja oznaczona na pliku binarnym jest wyższa niż bieżąca wersja wykonywanego Pythona. Ważną częścią tego kodu jest „uporządkowana rosnąca lista wersji”.

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

Przepraszam za mój porysowany python

Matt
źródło
czy nie będzie kontynuował działania po wykonaniu? więc normalne rzeczy byłyby wykonywane dwukrotnie?
Janus Troelsen
1
execv zastępuje aktualnie wykonywany program nowo załadowanym obrazem programu
Matt
To wygląda na świetne rozwiązanie, które wydaje się o wiele mniej brzydkie niż to, co wymyśliłem. Będę musiał spróbować, aby sprawdzić, czy to działa w tym celu.
user108471
Ta sugestia prawie działa na to, czego potrzebuję. Jedyną wadą jest coś, o czym pierwotnie nie wspomniałem: używam do obsługi Python 2.5 from __future__ import with_statement, co musi być pierwszą rzeczą w skrypcie Python. Nie sądzę, żebyś wiedział, jak to zrobić podczas uruchamiania nowego tłumacza?
user108471
Czy jesteś pewien, że musi być bardzo Pierwszą rzeczą? lub tuż przed próbą użycia jakiegokolwiek z nich withjak zwykłego importu ?. Czy dodatkowa funkcja if mypy == 25: from __future__ import with_statementdziała tuż przed „normalnymi sprawami”? Prawdopodobnie nie potrzebujesz if, jeśli nie wspierasz 2.4.
Matt
0

Możesz napisać mały skrypt bash, który sprawdza dostępny plik wykonywalny phython i wywołuje go ze skryptem jako parametrem. Następnie możesz ustawić ten skrypt jako cel linii shebang:

#!/my/python/search/script

A ten skrypt po prostu robi (po wyszukiwaniu):

"$python_path" "$1"

Nie byłem pewien, czy jądro zaakceptuje ten pośredni skrypt, ale sprawdziłem i działa.

Edytuj 1

Aby ta zawstydzająca nieufność stała się dobrą propozycją:

Możliwe jest połączenie obu skryptów w jednym pliku. Po prostu piszesz skrypt Pythona jako dokument tutaj w skrypcie bash (jeśli zmienisz skrypt Pythona, musisz tylko ponownie skopiować skrypty razem). Albo tworzysz plik tymczasowy w np. / Tmp lub (jeśli python to obsługuje, nie wiem) podajesz skrypt jako dane wejściowe do interpretera:

# 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
źródło
Jest to mniej więcej rozwiązanie, które zostało już podane w ostatnim akapicie pytania!
Bernhard
Sprytne, ale wymaga zainstalowania tego magicznego skryptu gdzieś w każdym systemie.
user108471
@Bernhard Oooops, złapany. W przyszłości przeczytam do końca. Jako rekompensatę zamierzam ulepszyć to rozwiązanie do jednego pliku.
Hauke ​​Laging
@ user108471 Magiczny skrypt może zawierać coś takiego: $(ls /usr/bin/python?.? | tail -n1 )ale nie udało mi się użyć tego sprytnie w shebang.
Bernhard
@Bernhard Chcesz przeprowadzić wyszukiwanie w linii shebang? Jądro IIRC nie przejmuje się cytowaniem w linii shebang. W przeciwnym razie (jeśli to się zmieniło w międzyczasie) można zrobić coś takiego jak `#! / Bin / bash -c do_search_here_with__bezpłatnej ...; exec $ python" $ 1 "Ale jak to zrobić bez białych znaków?
Hauke ​​Laging