Wybierz interpreter po uruchomieniu skryptu, np. If / else wewnątrz hashbang

16

Czy istnieje sposób na dynamiczne wybranie interpretera wykonującego skrypt? Mam skrypt, który uruchamiam na dwóch różnych systemach, a tłumacz, którego chcę użyć, znajduje się w różnych lokalizacjach na dwóch systemach. Ostatecznie muszę zmienić linię hashbang za każdym razem, gdy się przełączam. Chciałbym zrobić coś, co jest logicznym odpowiednikiem tego (zdaję sobie sprawę, że ta dokładna konstrukcja jest niemożliwa):

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

#Rest of script goes here

Lub jeszcze lepiej byłoby, gdyby próbował użyć pierwszego tłumacza, a jeśli go nie znajdzie, użyje drugiego:

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

#Rest of script goes here

Oczywiście mogę zamiast tego wykonać go jako /path/to/python/on/systemA myscript.py lub w /path/on/systemB myscript.py zależności od tego, gdzie jestem, ale tak naprawdę mam skrypt, który się uruchamia myscript.py, więc chciałbym określić ścieżkę do interpretera Pythona programowo, a nie ręcznie.

dkv
źródło
3
przekazywanie „reszty skryptu” jako pliku do interpretera bez shebang i używanie ifwarunku nie jest dla ciebie opcją? jak,if something; then /bin/sh restofscript.sh elif...
labirynty
Jest to opcja, również to rozważałem, ale nieco bardziej niechlujna, niż bym chciał. Ponieważ logika w linii hashbanga jest niemożliwa, myślę, że rzeczywiście pójdę tą drogą.
dv
Podoba mi się szeroki wachlarz różnych odpowiedzi na to pytanie.
Oskar Skog

Odpowiedzi:

27

Nie, to nie zadziała. Te dwa znaki #!absolutnie muszą być pierwszymi dwoma znakami w pliku (jak byś określił, co interpretowałoby instrukcję if?). Stanowi to „magiczną liczbę”, którą exec()rodzina funkcji wykrywa, gdy określają, czy plik, który zamierzają wykonać, jest skryptem (który wymaga interpretera) lub plikiem binarnym (który tego nie robi).

Format linii shebang jest dość ścisły. Musi mieć bezwzględną ścieżkę do tłumacza i co najwyżej jeden argument.

Co można zrobić, to do wykorzystania env:

#!/usr/bin/env interpreter

Teraz droga envjest zwykle /usr/bin/env , ale technicznie nie jest to żadną gwarancją.

Pozwala to dostosować PATHzmienną środowiskową w każdym systemie, tak że interpreter(niezależnie od tego bash, pythonczy perllub co masz) została znaleziona.

Minusem tego podejścia jest to, że niemożliwe będzie przenośne przekazanie argumentu tłumaczowi.

To znaczy że

#!/usr/bin/env awk -f

i

#!/usr/bin/env sed -f

jest mało prawdopodobne, aby działał na niektórych systemach.

Innym oczywistym podejściem jest użycie autotoolów GNU (lub prostszego systemu szablonów) do znalezienia interpretera i umieszczenia poprawnej ścieżki do pliku w ./configurekroku, który zostanie uruchomiony po zainstalowaniu skryptu w każdym systemie.

Można również skorzystać z uruchomienia skryptu z wyraźnym tłumaczem, ale oczywiście tego staramy się unikać:

$ sed -f script.sed
Kusalananda
źródło
Racja, zdaję sobie sprawę, że #!musi to nastąpić na początku, ponieważ to nie powłoka przetwarza tę linię. Zastanawiałem się, czy jest jakiś sposób na umieszczenie logiki w linii hashbanga, która byłaby równoważna z if / else. Miałem również nadzieję, że uniknę bałaganu z moim, PATHale wydaje mi się, że to moje jedyne opcje.
dkv
1
Kiedy używasz #!/usr/bin/awk, możesz podać dokładnie jeden argument, as #!/usr/bin/awk -f. Jeśli wskazywany jest plik binarny env, argumentem jest plik binarny, którego szukasz env, jak w #!/usr/bin/env awk.
DopeGhoti
2
@dkv To nie jest. Używa interpretera z dwoma argumentami i może działać na niektórych systemach, ale na pewno nie na wszystkich.
Kusalananda
3
@dkv w systemie Linux działa /usr/bin/envz jednym argumentem awk -f.
ilkkachu
1
@Kusalananda, nie, o to właśnie chodziło. Jeśli masz skrypt wywoływany foo.awkza pomocą wiersza hashbang #!/usr/bin/env awk -fi wywołujesz go ./foo.awkwtedy, w systemie Linux zobaczysz envdwa parametry awk -fi ./foo.awk. Poszukuje /usr/bin/awk -f(itp.) Spacji.
ilkkachu
27

Zawsze możesz utworzyć skrypt opakowujący, aby znaleźć poprawnego interpretera dla rzeczywistego programu:

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

Zachować opakowanie w użytkowników, PATHjak programi umieścić rzeczywistego programu na bok lub z inną nazwą.

Użyłem #!/bin/bashw hashbangu z powodu flagstablicy. Jeśli nie musisz przechowywać zmiennej liczby flag lub podobnych i możesz się bez tego obejść, skrypt powinien działać przenośnie #!/bin/sh.

ilkkachu
źródło
2
Widziałem exec "$interpreter" "${flags[@]}" "$script" "$@"również, aby zachować czystość drzewa procesów. Propaguje także kod wyjścia.
rrauenza
@rrauenza, ah tak, oczywiście z exec.
ilkkachu
1
Nie #!/bin/shbyłoby lepiej zamiast #!/bin/bash? Nawet jeśli /bin/shjest dowiązaniem symbolicznym do innej powłoki, powinien on istnieć w większości (jeśli nie we wszystkich) * systemach nix, a ponadto zmusiłby autora skryptu do stworzenia przenośnego skryptu, zamiast popadać w bashizmy.
Sergiy Kolodyazhnyy
@SergiyKolodyazhnyy, heh, myślałem o tym wcześniej, ale nie zrobiłem tego. Zastosowana tablica flagsjest niestandardową funkcją, ale jest wystarczająca do przechowywania zmiennej liczby flag, więc postanowiłem ją zachować.
ilkkachu
Lub użyj / bin / sh i po prostu wezwać tłumacza bezpośrednio w każdej gałęzi: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". Możesz także dodać sprawdzanie błędów pod kątem błędów wykonania.
jrw32982 obsługuje Monikę
11

Możesz także napisać poliglota (połączyć dwa języki). Na pewno istnieje / bin / sh.

Ma to wadę brzydkiego kodu i być może niektóre /bin/shs mogą się pomylić. Ale można go użyć, gdy envnie istnieje lub istnieje gdzieś indziej niż / usr / bin / env. Można go również użyć, jeśli chcesz dokonać dość fantazyjnego wyboru.

Pierwsza część skryptu określa, którego interpretera należy użyć, gdy jest uruchamiany z / bin / sh jako interpreter, ale jest ignorowana, gdy jest uruchamiany przez poprawnego interpretera. Użyj, execaby zapobiec uruchomieniu powłoki przez więcej niż pierwszą część.

Przykład w języku 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
źródło
3
Wydaje mi się, że widziałem już jedną z nich, ale pomysł jest nadal równie okropny ... Ale prawdopodobnie chcesz exec "$interpreter" "$0" "$@"przekazać nazwę samego skryptu również rzeczywistemu tłumaczowi. (I wtedy mam nadzieję, że nikt nie skłamał podczas konfiguracji $0.)
ilkkachu
6
Scala faktycznie obsługuje skrypty polyglot w swojej składni: jeśli skrypt Scala zaczyna się od #!, Scala ignoruje wszystko, aż do dopasowania !#; pozwala to umieścić dowolnie złożony kod skryptu w dowolnym języku, a następnie execsilnik wykonawczy Scala ze skryptem.
Jörg W Mittag
1
@ Jörg W Mittag: +1 dla Scali
jrw32982 obsługuje Monikę
2

Wolę odpowiedzi Kusalanandy i ilkkachu, ale tutaj jest alternatywna odpowiedź, która bardziej bezpośrednio robi to, o co pytało pytanie, po prostu dlatego, że zostało zadane.

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

if True:
  print("hello world!")

Zauważ, że możesz to zrobić tylko wtedy, gdy interpreter pozwala na pisanie kodu w pierwszym argumencie. Tutaj, -ea wszystko po tym jest traktowane dosłownie jako 1 argument za ruby. O ile mi wiadomo, nie można użyć bash dla kodu shebang, ponieważ bash -cwymaga, aby kod był osobnym argumentem.

Próbowałem zrobić to samo z pythonem dla kodu 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

ale okazuje się, że jest za długi i linux (przynajmniej na mojej maszynie) obcina shebang do 127 znaków. Proszę wybaczyć użycie execwstawiania nowych linii, ponieważ Python nie zezwala na try-wyjątki lub imports bez nowych linii.

Nie jestem pewien, jak to jest przenośne i nie zrobiłbym tego na kodzie przeznaczonym do dystrybucji. Niemniej jednak jest to wykonalne. Może ktoś uzna to za przydatne do szybkiego i brudnego debugowania lub czegoś takiego.

JoL
źródło
2

Chociaż nie wybiera to interpretera w skrypcie powłoki (wybiera go na maszynę), jest łatwiejszą alternatywą, jeśli masz dostęp administracyjny do wszystkich komputerów, na których próbujesz uruchomić skrypt.

Utwórz dowiązanie symboliczne (lub w razie potrzeby dowiązanie twarde), aby wskazać żądaną ścieżkę tłumacza. Na przykład w moim systemie perl i python znajdują się w / usr / bin:

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

utworzyłby dowiązanie symboliczne, aby umożliwić hashbangowi rozpoznanie dla / bin / perl itp. Zachowuje to również możliwość przekazywania parametrów do skryptów.

Obrabować
źródło
1
+1 To takie proste. Jak zauważasz, nie do końca odpowiada na pytanie, ale wydaje się, że robi dokładnie to, czego chce OP. Chociaż wydaje mi się, że użycie env pozwala uzyskać dostęp do katalogu głównego na każdym komputerze.
Joe
0

Zetknąłem się dzisiaj z podobnym problemem takim jak ten ( python3wskazując na wersję Pythona, która była zbyt stara w jednym systemie), i wpadłem na podejście nieco inne niż omówione tutaj: użyj „złej” wersji python, aby załadować się do „właściwego”. Ograniczeniem jest to, że niektóre wersje Pythona muszą być osiągalne w sposób niezawodny, ale zwykle można to osiągnąć np #!/usr/bin/env python3.

Więc zaczynam mój skrypt od:

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

To robi:

  • Sprawdź wersję interpretera pod kątem kryterium akceptacji
  • Jeśli nie jest to możliwe, przejrzyj listę wersji kandydujących i ponownie uruchom się z pierwszą dostępną wersją
mikrotherion
źródło