Wiele argumentów w shebang


32

Zastanawiam się, czy istnieje ogólny sposób przekazywania wielu opcji do pliku wykonywalnego za pomocą linii shebang ( #!).

Używam NixOS, a pierwsza część shebang w każdym skrypcie, który piszę, jest zwykle /usr/bin/env. Problem, z którym się wtedy spotykam, polega na tym, że wszystko, co następuje, jest interpretowane przez system jako pojedynczy plik lub katalog.

Załóżmy na przykład, że chcę napisać skrypt do wykonania bashw trybie posix. Naiwnym sposobem pisania shebang byłoby:

#!/usr/bin/env bash --posix

ale próba wykonania wynikowego skryptu powoduje następujący błąd:

/usr/bin/env: ‘bash --posix’: No such file or directory

Znam ten post , ale zastanawiałem się, czy istnieje bardziej ogólne i czystsze rozwiązanie.


EDYCJA : Wiem, że w przypadku skryptów Guile istnieje sposób na osiągnięcie tego, czego chcę, udokumentowany w Sekcji 4.3.4 podręcznika:

 #!/usr/bin/env sh
 exec guile -l fact -e '(@ (fac) main)' -s "$0" "$@"
 !#

Sztuczka polega na tym, że druga linia (od exec ) jest interpretowana jako kod, shale będąc w #!... !#bloku, jako komentarz, a zatem ignorowana przez interpretera Guile'a.

Czy nie byłoby możliwe uogólnienie tej metody na żadnego tłumacza?


Druga EDYCJA : Po krótkiej zabawie wydaje się, że w przypadku tłumaczy, którzy potrafią odczytać dane wejściowe stdin, zadziała następująca metoda:

#!/usr/bin/env sh
sed '1,2d' "$0" | bash --verbose --posix /dev/stdin; exit;

Prawdopodobnie nie jest to optymalne, ponieważ shproces trwa, dopóki tłumacz nie zakończy pracy. Wszelkie uwagi lub sugestie będą mile widziane.



Odpowiedzi:


27

Nie ma ogólnego rozwiązania, przynajmniej jeśli potrzebujesz obsługi Linuksa, ponieważ jądro Linuksa traktuje wszystko następujące po pierwszym „słowie” w wierszu shebang jako pojedynczy argument .

Nie jestem pewien, jakie są ograniczenia NixOS, ale zazwyczaj po prostu napisałbym twój shebang jako

#!/bin/bash --posix

lub, gdzie to możliwe, ustaw opcje w skrypcie :

set -o posix

Ewentualnie możesz uruchomić skrypt ponownie, używając odpowiedniego wywołania powłoki:

#!/bin/sh -

if [ "$1" != "--really" ]; then exec bash --posix -- "$0" --really "$@"; fi

shift

# Processing continues

Podejście to można uogólnić na inne języki, o ile znajdziesz sposób, aby pierwsze kilka wierszy (które są interpretowane przez powłokę) były ignorowane przez język docelowy.

GNU coreutils" envstanowi obejście od wersji 8.30, patrz unode „s odpowiedź szczegóły. (Jest to dostępne w Debianie 10 i nowszych wersjach, RHEL 8 i nowszych wersjach, Ubuntu 19.04 i nowszych wersjach itp.)


18

Chociaż nie do końca przenośny, począwszy od Coreutils 8.30 i zgodnie z jego dokumentacją będziesz mógł używać:

#!/usr/bin/env -S command arg1 arg2 ...

Biorąc pod uwagę:

$ cat test.sh
#!/usr/bin/env -S showargs here 'is another' long arg -e "this and that " too

dostaniesz:

% ./test.sh 
$0 is '/usr/local/bin/showargs'
$1 is 'here'
$2 is 'is another'
$3 is 'long'
$4 is 'arg'
$5 is '-e'
$6 is 'this and that '
$7 is 'too'
$8 is './test.sh'

a jeśli jesteś ciekawy showargsto:

#!/usr/bin/env sh
echo "\$0 is '$0'"

i=1
for arg in "$@"; do
    echo "\$$i is '$arg'"
    i=$((i+1))
done

To bardzo dobrze wiedzieć na przyszłość.
John McGehee

Ta opcja została skopiowana z FreeBSD, envgdzie -Szostała dodana w 2005 roku. Zobacz lists.gnu.org/r/coreutils/2018-04/msg00011.html
Stéphane Chazelas

Działa na Fedorze 29
Eric

@unode kilka ulepszeń showargs: pastebin.com/q9m6xr8H i pastebin.com/gS8AQ5WA (one-liner)
Eric

FYI: od coreutils 8.31, envzawiera własną showargs: opcję -v np.#!/usr/bin/env -vS --option1 --option2 ...
chocolateboy

9

Standard POSIX bardzo zwięźle opisuje #!:

Z sekcji uzasadnienia dokumentacji exec()rodziny interfejsów systemowych :

Innym sposobem, w jaki niektóre historyczne implementacje obsługują skrypty powłoki, jest rozpoznanie pierwszych dwóch bajtów pliku jako ciągu znaków #!i użycie pozostałej części pierwszego wiersza pliku jako nazwy interpretera poleceń do wykonania.

Z sekcji Wprowadzenie do powłoki :

Powłoka odczytuje dane wejściowe z pliku (patrz sh), z -copcji lub z funkcji system()i popen()zdefiniowanych w woluminie interfejsów systemowych POSIX.1-2008. Jeśli pierwszy wiersz pliku poleceń powłoki zaczyna się od znaków #!, wyniki nie są określone .

Zasadniczo oznacza to, że każda implementacja (Unix, którego używasz) może dowolnie wykonywać specyfikację parsowania linii shebang.

Niektóre Unices, takie jak macOS (nie można przetestować bankomatu), podzielą argumenty podane interpreterowi w linii shebang na osobne argumenty, podczas gdy Linux i większość innych Unices poda te argumenty jako jedną opcję dla interpretera.

Dlatego nierozsądne jest poleganie na tym, że linia shebang może przyjąć więcej niż jeden argument.

Zobacz także sekcję Przenośność w artykule Shebang na Wikipedii .


Jednym łatwym rozwiązaniem, które można uogólnić na dowolny program narzędziowy lub język, jest utworzenie skryptu opakowującego, który wykonuje prawdziwy skrypt z odpowiednimi argumentami wiersza poleceń:

#!/bin/sh
exec /bin/bash --posix /some/path/realscript "$@"

Nie sądzę, bym osobiście spróbował zmusić go do ponownego uruchomienia , ponieważ wydaje się to nieco kruche.


7

Shebang jest opisany na stronie podręcznika execve(2) w następujący sposób:

#! interpreter [optional-arg]

W tej składni akceptowane są dwa spacje:

  1. Jedna spacja przed ścieżką interpretera , ale ta spacja jest opcjonalna.
  2. Jedna spacja oddzielająca ścieżkę interpretera i jej opcjonalny argument.

Zauważ, że nie użyłem liczby mnogiej, gdy mówię o opcjonalnym argumencie [optional-arg ...], podobnie jak powyższa składnia , ponieważ możesz podać najwyżej jeden argument .

Jeśli chodzi o skrypty powłoki, możesz użyć set wbudowanej komendy na początku skryptu, która pozwoli ustawić parametry interpretera, zapewniając taki sam wynik, jak w przypadku użycia argumentów wiersza polecenia.

W Twoim przypadku:

set -o posix

W wierszu polecenia Bash sprawdź dane wyjściowe opcji, help setaby uzyskać wszystkie dostępne opcje.


1
Możesz mieć więcej niż dwie spacje, są one po prostu uważane za część opcjonalnego argumentu.
Stephen Kitt

@StephenKitt: Rzeczywiście, białe spacje należy tutaj traktować bardziej jako kategorię niż rzeczywisty znak spacji. Przypuszczam, że inne białe spacje, takie jak tabulatory, również powinny być powszechnie akceptowane.
WhiteWinterWolf

3

W Linuksie shebang nie jest zbyt elastyczny; według wielu odpowiedzi ( odpowiedź Stephena KITT i Jörg W Mittag użytkownika ), nie ma wyznaczonego sposób przekazać wiele argumentów w shebang linii.

Nie jestem pewien, czy przyda się komukolwiek, ale napisałem krótki skrypt, aby zaimplementować brakującą funkcję. Zobacz https://gist.github.com/loxaxs/7cbe84aed1c38cf18f70d8427bed1efa .

Możliwe jest również pisanie wbudowanych obejść. Poniżej przedstawiam cztery obejścia niezależne od języka zastosowane w tym samym skrypcie testowym, a wynik każdego z nich drukowany. Przypuszczam, że skrypt jest wykonywalny i jest w /tmp/shebang.


Zawijanie skryptu w bash heredoc wewnątrz substytucji procesu

O ile mi wiadomo, jest to najbardziej niezawodny, niezależny od języka sposób robienia tego. Pozwala przekazywać argumenty i zachowuje standardowe wejście. Wadą jest to, że tłumacz nie zna (rzeczywistej) lokalizacji pliku, który czyta.

#!/bin/bash
exec python3 -O <(cat << 'EOWRAPPER'
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv
try:
    print("input() 0 ::", input())
    print("input() 1 ::", input())
except EOFError:
    print("input() caused EOFError")
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")
EOWRAPPER
) "$@"

Wywoływanie echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'wydruków:

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /dev/fd/62
argv[1:]  :: ['arg1', 'arg2 contains spaces', 'arg3\\ uses\\ \\\\escapes\\\\']
__debug__ :: False
PYTHON_SCRIPT_END

Zauważ, że podstawienie procesu tworzy specjalny plik. To może nie pasować do wszystkich plików wykonywalnych. Na przykład #!/usr/bin/lessnarzeka:/dev/fd/63 is not a regular file (use -f to see it)

Nie wiem, czy jest możliwe posiadanie heredoc wewnątrz podstawienia procesu w desce rozdzielczej.


Zawijanie skryptu w prosty heredoc

Krótsze i prostsze, ale nie będziesz mieć dostępu do stdinskryptu, a interpreter musi móc odczytać i wykonać skrypt stdin.

#!/bin/sh
exec python3 - "$@" << 'EOWRAPPER'
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

try:
    print("input() 0 ::", input())
    print("input() 1 ::", input())
except EOFError:
    print("input() caused EOFError")
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")
EOWRAPPER

Wywoływanie echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'wydruków:

PYTHON_SCRIPT_BEGINNING
input() caused EOFError
argv[0]   :: -
argv[1:]  :: ['arg1', 'arg2 contains spaces', 'arg3\\ uses\\ \\\\escapes\\\\']
__debug__ :: True
PYTHON_SCRIPT_END

Użyj system()wywołania awk, ale bez argumentów

Prawidłowo przekazuje nazwę wykonanego pliku, ale skrypt nie odbiera argumentów, które mu podajesz. Zauważ, że awk jest jedynym znanym mi językiem, którego interpreter jest domyślnie zainstalowany na Linuksie i domyślnie odczytuje instrukcje z wiersza poleceń.

#!/usr/bin/gawk BEGIN {system("python3 -O " ARGV[1])}
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

print("input() 0 ::", input())
print("input() 1 ::", input())
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")

Wywoływanie echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'wydruków:

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /tmp/shebang
argv[1:]  :: []
__debug__ :: False
PYTHON_SCRIPT_END

Użyj system()wywołania awk 4.1+ , pod warunkiem, że twoje argumenty nie zawierają spacji

Fajnie, ale tylko wtedy, gdy masz pewność, że twój skrypt nie zostanie wywołany z argumentami zawierającymi spacje. Jak widać, twoje argumenty zawierające spacje zostałyby podzielone, chyba że spacje zostaną usunięte.

#!/usr/bin/gawk @include "join"; BEGIN {system("python3 -O " join(ARGV, 1, ARGC, " "))}
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

print("input() 0 ::", input())
print("input() 1 ::", input())
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")

Wywoływanie echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'wydruków:

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /tmp/shebang
argv[1:]  :: ['arg1', 'arg2', 'contains', 'spaces', 'arg3 uses \\escapes\\']
__debug__ :: False
PYTHON_SCRIPT_END

W przypadku wersji awk poniżej 4.1 będziesz musiał użyć konkatenacji łańcuchów wewnątrz pętli for, patrz przykładowa funkcja https://www.gnu.org/software/gawk/manual/html_node/Join-Function.html .


1
Podaj terminator dokumentu tutaj, aby zahamować $variablelub `command`zastąpić:exec python3 -O <(cat <<'EOWRAPPER'
John McGehee

2

Trik do użycia LD_LIBRARY_PATHz #!pythonem w wierszu (shebang), który nie zależy od niczego innego niż powłoka i działa przysmak:

#!/bin/sh
'''' 2>/dev/null; exec /usr/bin/env LD_LIBRARY_PATH=. python -x "$0" "$@" #'''

__doc__ = 'A great module docstring'

Jak wyjaśniono gdzie indziej na tej stronie, niektóre powłoki sh mogą pobierać skrypt na standardowe wejście.

Skrypt, który dajemy, shpróbuje wykonać polecenie, ''''które jest uproszczone ''(pusty ciąg znaków) shi oczywiście nie wykonuje go, ponieważ nie ma ''polecenia, więc zwykle wypisuje line 2: command not foundna standardowym deskryptorze błędów, ale przekierowujemy ten komunikat za 2>/dev/nullpomocą najbliższa czarna dziura, ponieważ wyświetlenie jej byłoby nieporządne i mylące dla użytkownika sh.

Następnie przechodzimy do interesującego nas polecenia: execktóre zastępuje bieżący proces powłoki następującym, w naszym przypadku: /usr/bin/env pythonodpowiednimi parametrami:

  • "$0" aby python wiedział, który skrypt powinien otworzyć i zinterpretować, a także ustawić sys.argv[0]
  • "$@"aby ustawić python sys.argv[1:]na argumenty przekazywane w wierszu poleceń skryptu.

Prosimy również o envustawienie LD_LIBRARY_PATHzmiennej środowiskowej, która jest jedynym punktem włamania.

Polecenie powłoki kończy się na komentarzu rozpoczynającym się od, #dzięki czemu powłoka ignoruje końcowe potrójne cudzysłowy '''.

shjest następnie zastępowane przez shinny nową instancję interpretera Pythona, która otwiera i odczytuje skrypt źródłowy Pythona podany jako pierwszy argument (the "$0").

Python otwiera plik i pomija pierwszy wiersz źródła dzięki -xargumentowi. Uwaga: działa również bez, -xponieważ dla Pythona shebang jest tylko komentarzem .

Następnie Python interpretuje drugi wiersz jako ciąg docstring dla bieżącego pliku modułu, więc jeśli potrzebujesz poprawnego docstring modułu, po prostu ustaw __doc__pierwszą rzecz w swoim programie python, tak jak w powyższym przykładzie.



Biorąc pod uwagę, że pusty ciąg jest… um… pusty, powinieneś być w stanie porzucić polecenie, nie znaleziono małpiego biznesu: ''''exec ...powinieneś wykonać zadanie. Zauważ, że nie ma spacji przed exec, bo spowoduje, że będzie szukał pustego polecenia. Chcesz podzielić puste na pierwszy argument, więc tak $0jest exec.
Caleb

1

Znalazłem dość głupie obejście, gdy szukałem pliku wykonywalnego, który wyłącza skrypt jako pojedynczy argument:

#!/usr/bin/awk BEGIN{system("bash --posix "ARGV[1])}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.