#! / bin / sh vs #! / bin / bash dla maksymalnej przenośności

36

Zazwyczaj pracuję z serwerów Ubuntu LTS, który z tego co rozumiem dowiązania /bin/shdo /bin/dash. Wiele innych dystrybucjach though dowiązaniem /bin/shdo /bin/bash.

Z tego rozumiem, że jeśli skrypt używa #!/bin/shna górze, może nie działać w ten sam sposób na wszystkich serwerach?

Czy istnieje sugerowana praktyka, w której powłoce należy używać skryptów, gdy chce się maksymalnie przenosić te skrypty między serwerami?

Cherouvim
źródło
Istnieją niewielkie różnice między różnymi pociskami. Jeśli najważniejsza jest dla ciebie przenośność, używaj #!/bin/shi nie używaj niczego innego niż to, co zapewnia oryginalna powłoka.
Thorbjørn Ravn Andersen

Odpowiedzi:

65

Istnieją około cztery poziomy przenośności dla skryptów powłoki (jeśli chodzi o linię shebang):

  1. Najbardziej przenośny: użyj #!/bin/shshebang i używaj tylko podstawowej składni powłoki określonej w standardzie POSIX . Powinno to działać na prawie każdym systemie POSIX / unix / linux. (Cóż, z wyjątkiem Solaris 10 i wcześniejszych, który miał prawdziwą starszą powłokę Bourne'a, wyprzedzając POSIX tak niezgodny, jak /bin/sh.)

  2. Drugi najbardziej przenośny: użyj linii shebang #!/bin/bash(lub #!/usr/bin/env bash) i trzymaj się funkcji bash v3. Działa to na każdym systemie, który ma bash (w oczekiwanej lokalizacji).

  3. Trzecia najbardziej przenośna: użyj linii shebang #!/bin/bash(lub #!/usr/bin/env bash) i użyj funkcji bash v4. Nie powiedzie się to w każdym systemie, który ma bash v3 (np. MacOS, który musi go używać ze względów licencyjnych).

  4. Najmniej przenośny: użyj #!/bin/shshebang i użyj rozszerzeń bash do składni powłoki POSIX. Nie powiedzie się to w każdym systemie, który ma coś innego niż bash dla / bin / sh (np. Najnowsze wersje Ubuntu). Nigdy tego nie rób; to nie tylko problem ze zgodnością, to po prostu źle. Niestety wielu ludzi popełnia błąd.

Moja rekomendacja: użyj najbardziej konserwatywnej z pierwszych trzech, która zapewnia wszystkie funkcje powłoki potrzebne do skryptu. Aby uzyskać maksymalną przenośność, użyj opcji nr 1, ale z mojego doświadczenia wynika, że ​​niektóre funkcje bash (takie jak tablice) są na tyle pomocne, że pójdę z numerem 2.

Najgorsze, co możesz zrobić, to # 4, używając niewłaściwego shebang. Jeśli nie masz pewności, jakie funkcje są podstawowe POSIX, a które są rozszerzeniami bash, albo trzymaj się bash shebang (tj. Opcja # 2), lub dokładnie przetestuj skrypt za pomocą bardzo podstawowej powłoki (np. Dash na serwerach Ubuntu LTS). Wiki Ubuntu ma dobrą listę baszizmów, na które należy uważać .

Jest kilka bardzo dobrych informacji o historii i różnicach między powłokami w pytaniu na Unixa i Linuksa „Co to znaczy być kompatybilnym z sh?” oraz pytanie Stackoverflow „Różnica między sh i bash” .

Pamiętaj też, że powłoka nie jest jedyną rzeczą, która różni się w zależności od systemu; jeśli jesteś przyzwyczajony do Linuksa, przywykłeś do poleceń GNU, które mają wiele niestandardowych rozszerzeń, których możesz nie znaleźć w innych systemach uniksowych (np. bsd, macOS). Niestety nie ma tutaj prostej reguły, wystarczy znać zakres zmienności używanych poleceń.

Jednym z najniebezpieczniejszych poleceń w zakresie przenośności jest jednym z najbardziej podstawowych: echo. Za każdym razem, gdy użyjesz go z dowolnymi opcjami (np. echo -nLub echo -e), lub z dowolnymi znakami ucieczki (odwrotnymi ukośnikami) w ciągu do wydrukowania, różne wersje zrobią różne rzeczy. Za każdym razem, gdy chcesz wydrukować ciąg bez podawania wiersza po nim lub ze znakami ucieczki w ciągu, użyj printfzamiast tego (i dowiedz się, jak to działa - jest to bardziej skomplikowane niż echojest). psPolecenia jest także bałagan .

Inną ogólną rzeczą, na którą należy zwrócić uwagę, są najnowsze rozszerzenia / GNUish do składni opcji poleceń: stary (standardowy) format polecenia polega na tym, że po poleceniu następują opcje (z pojedynczym myślnikiem, a każda opcja to jedna litera), po którym następuje argumenty poleceń. Ostatnie (i często nieprzenośne) warianty obejmują długie opcje (zwykle wprowadzane wraz z --), pozwalające na pojawienie się opcji po argumentach i używanie ich --do oddzielania opcji od argumentów.

Gordon Davisson
źródło
12
Czwarta opcja to po prostu zły pomysł. Nie używaj go.
pabouk
3
@pabouk Zgadzam się całkowicie, więc zredagowałem swoją odpowiedź, aby było jaśniej.
Gordon Davisson
1
Twoje pierwsze stwierdzenie jest nieco mylące. Standard POSIX nie precyzuje nic o shebang poza powiedzeniem, że użycie go prowadzi do nieokreślonego zachowania. Ponadto POSIX nie określa, gdzie musi znajdować się powłoka posix, tylko jej name ( sh), więc /bin/shnie gwarantuje się, że jest to właściwa ścieżka. Najbardziej przenośnym jest wtedy, aby w ogóle nie określać żadnego odcinka ani dostosowywać odcinka do używanego systemu operacyjnego.
jlliagre
2
Niedawno wpadłem na # 4 z moim skryptem i po prostu nie mogłem zrozumieć, dlaczego to nie działa; w końcu te same polecenia działały solidnie i działały dokładnie tak, jak chciałem, gdy próbowałem ich bezpośrednio w powłoce. Tak szybko, jak zmieniła #!/bin/shsię #!/bin/bashjednak skrypt działał idealnie. (Na moją obronę ten scenariusz ewoluował z czasem z jednego, który naprawdę potrzebował tylko sh-ismów, do tego, który polegał na zachowaniu przypominającym bash).
CVn
2
@ MichaelKjörling (prawdziwa) powłoka Bourne'a prawie nigdy nie jest dołączana do dystrybucji Linuksa i tak czy inaczej nie jest zgodna z POSIX. Standardowa powłoka POSIX została stworzona z kshzachowania, a nie Bourne. Większość dystrybucji Linuksa po FHS to /bin/shsymboliczne łącze do powłoki, którą wybierają, aby zapewnić zgodność z POSIX, zwykle bashlub dash.
jlliagre
5

W ./configureskrypcie przygotowującym język TXR do budowania napisałem następujący prolog dla lepszej przenośności. Skrypt sam się uruchomi, nawet jeśli #!/bin/shjest niezgodny ze standardem POSIX starej powłoki Bourne'a. (Każde wydanie buduję na maszynie wirtualnej Solaris 10).

#!/bin/sh

# use your own variable name instead of txr_shell;
# adjust to taste: search for your favorite shells

if test x$txr_shell = x ; then
  for shell in /bin/bash /usr/bin/bash /usr/xpg4/bin/sh ; do
    if test -x $shell ; then
       txr_shell=$shell
       break
    fi
  done
  if test x$txr_shell = x ; then
    echo "No known POSIX shell found: falling back on /bin/sh, which may not work"
    txr_shell=/bin/sh
  fi
  export txr_shell
  exec $txr_shell $0 ${@+"$@"}
fi

# rest of the script here, executing in upgraded shell

Chodzi o to, że znajdujemy lepszą powłokę niż ta, w której działamy, i ponownie uruchamiamy skrypt za pomocą tej powłoki. txr_shellZmienna jest ustawiona tak, że skrypt ponownie wykonywane wie, że jest ponownie wykonywane rekurencyjne instancji.

(W moim skrypcie txr_shellzmienna jest następnie używana do dokładnie dwóch celów: po pierwsze jest drukowana jako część wiadomości informacyjnej na wyjściu skryptu. Po drugie, jest instalowana jako SHELLzmienna w Makefile, więc makeużyje tego shell również do wykonywania przepisów).

W systemie, w którym /bin/shjest myślnik, widać, że powyższa logika znajdzie /bin/bashi ponownie uruchomi skrypt za jego pomocą.

W przypadku systemu Solaris 10 uruchomi się /usr/xpg4/bin/sh, jeśli nie zostanie znalezione Bash.

Prolog napisany jest w konserwatywnym dialekcie powłoki, używanym testdo testów istnienia plików, a ${@+"$@"}sztuczka polegająca na rozszerzaniu argumentów uwzględniających niektóre zepsute stare powłoki (co byłoby po prostu "$@", gdybyśmy byli w powłoce zgodnej z POSIX).

Kaz
źródło
xNikt nie potrzebowałby hakowania, gdyby zastosowano prawidłowe cytowanie, ponieważ sytuacje, które nakazały temu idiomowi otaczać obecnie nieaktualne wywołania testowe -alub -ołączenie wielu testów.
Charles Duffy
@CharlesDuffy Indeed; to test x$whatever, co tam robię, wygląda jak cebula w lakierze. Jeśli nie możemy ufać zepsutej starej powłoce, aby cytować, to ostatnia ${@+"$@"}próba jest bezcelowa.
Kaz
2

Wszystkie odmiany języka powłoki Bourne'a są obiektywnie okropne w porównaniu do współczesnych języków skryptowych, takich jak Perl, Python, Ruby, node.js, a nawet (prawdopodobnie) Tcl. Jeśli musisz zrobić coś nawet trochę skomplikowanego, na dłuższą metę będziesz szczęśliwszy, jeśli użyjesz jednego z powyższych zamiast skryptu powłoki.

Jedyną zaletą, jaką język powłoki ma nadal w porównaniu z nowszymi językami, jest to, że istnieje pewność, że coś , co nazywa się samym, /bin/shistnieje na wszystkim, co może być uniksowe. Jednak to coś może nawet nie być zgodne z POSIX; wiele starszych, unikatowych uniksów zamroziło język /bin/shi narzędzia w domyślnej PATH przed zmianami wymaganymi przez Unix95 (tak, Unix95, dwadzieścia lat temu i wciąż rośnie). Może być zestaw Unix95, a nawet POSIX.1-2001, jeśli masz szczęście, narzędzia w katalogu innym niż domyślna ŚCIEŻKA (np. /usr/xpg4/bin), Ale nie gwarantuje się, że istnieją.

Jednak podstawy Perla częściej występują w arbitralnie wybranej instalacji Uniksa niż Bash. (Przez „podstawy Perla” mam na myśli, że /usr/bin/perlistnieje i jest to pewna , być może dość stara, wersja Perla 5, a jeśli masz szczęście, dostępny jest także zestaw modułów dostarczonych z tą wersją interpretera).

W związku z tym:

Jeśli piszesz coś, co musi działać wszędzie, co rzekomo jest uniksowe (np. Skrypt „konfiguruj”), musisz go użyć #! /bin/shi nie musisz używać żadnych rozszerzeń. W dzisiejszych czasach napisałbym w tej sytuacji powłokę zgodną z POSIX.1-2001, ale byłbym przygotowany na załatanie POSIXizmu, gdyby ktoś poprosił o wsparcie dla zardzewiałego żelaza.

Ale jeśli nie piszesz czegoś, co musi działać wszędzie, to w momencie, gdy masz ochotę w ogóle użyć bashizmu, powinieneś przerwać i przepisać całość w lepszym języku skryptowym. Twoje przyszłe ja będzie ci wdzięczne.

(Tak, gdy jest to stosowne korzystać z rozszerzeń bash pierwszej kolejności: nigdy do drugiej kolejności: tylko przedłużyć interaktywne środowisko Bash - np dostarczenie inteligentnych zakładki dopełniania i fantazyjne wyświetlanymi?.).

zwol
źródło
Pewnego dnia musiałem napisać scenariusz, który zrobił coś podobnego find ... -print0 |while IFS="" read -rd "" file; do ...; done |tar c --null -T -. Nie byłoby możliwe, aby działał poprawnie bez bashisms ( read -d "") i rozszerzeń GNU ( find -print0, tar --null). Ponowne wdrożenie tej linii w Perlu byłoby znacznie dłuższe i bardziej niezgrabne. Jest to sytuacja, w której używanie bashism i innych rozszerzeń innych niż POSIX jest właściwą rzeczą.
Michael
@michau Może nie kwalifikować się już jako jedno-liniowy, w zależności od tego, co dzieje się w tych elipsach, ale myślę, że nie bierzesz „przepisania całej rzeczy w lepszym języku skryptowym” tak dosłownie, jak miałem na myśli. Mój sposób wygląda perl -MFile::Find -e 'our @tarcmd = ("tar", "c"); find(sub { return unless ...; ...; push @tarcmd, $File::Find::name }, "."); exec @tarcmdtrochę jak Zauważ, że nie tylko nie potrzebujesz bashizmów, ale nie potrzebujesz żadnych rozszerzeń tar GNU. (Jeśli chodzi o długość wiersza poleceń lub jeśli chcesz, aby skanowanie i tar działały równolegle, potrzebujesz tar --null -T).
zwolnij
Rzecz findw tym, że w moim scenariuszu zwrócono ponad 50 000 nazw plików, więc twój skrypt prawdopodobnie przekroczy limit długości argumentów. Prawidłowa implementacja, która nie używa rozszerzeń innych niż POSIX, musiałaby budować dane wyjściowe tar stopniowo lub być może użyć Archive :: Tar :: Stream w kodzie Perla. Oczywiście można to zrobić, ale w tym przypadku skrypt Bash jest o wiele szybszy do napisania i ma mniej płyt kotłowych, a tym samym mniej miejsca na błędy.
michau
BTW, można używać zamiast Perl Bash w szybki i zwięzły sposób: find ... -print0 |perl -0ne '... print ...' |tar c --null -T -. Ale pytania są następujące: 1) Używam find -print0i tar --nulltak, po co więc unikać bashizmu read -d "", co jest dokładnie tym samym rodzajem (rozszerzenie GNU do obsługi separatora zerowego, które w przeciwieństwie do Perla może kiedyś stać się POSIX )? 2) Czy Perl jest naprawdę bardziej rozpowszechniony niż Bash? Ostatnio korzystam z obrazów Dockera i wiele z nich ma Bash, ale nie ma Perla. Nowe skrypty są tam częściej uruchamiane niż na starożytnych Unices.
michau