Semantyka skryptów Bash?

87

Bardziej niż w jakimkolwiek innym języku, który znam, „nauczyłem się” Bash przez Google za każdym razem, gdy potrzebuję jakiejś małej rzeczy. W związku z tym mogę łączyć ze sobą małe skrypty, które wydają się działać. Jednak tak naprawdę nie wiem, co się dzieje i liczyłem na bardziej formalne wprowadzenie do Bash jako języka programowania. Na przykład: Jaka jest kolejność oceny? jakie są zasady określania zakresu? Jaka jest dyscyplina pisania, np. Czy wszystko jest ciągiem znaków? Jaki jest stan programu - czy jest to przypisanie ciągów znaków do nazw zmiennych; czy jest coś więcej, np. stos? Czy jest kupa? I tak dalej.

Pomyślałem, że poszuka tego w podręczniku GNU Bash, ale nie wydaje mi się, żeby to było to, czego chcę; jest to raczej lista do prania cukru syntaktycznego niż wyjaśnienie podstawowego modelu semantycznego. Milion i jeden „samouczków bash” online jest tylko gorszy. Może powinienem najpierw przestudiować shi zrozumieć Bash jako dodatek do cukru syntaktycznego? Nie wiem jednak, czy to dokładny model.

Jakieś sugestie?

EDYCJA: Poproszono mnie o podanie przykładów tego, czego idealnie szukam. Dość skrajnym przykładem tego, co uważam za „semantykę formalną”, jest ten artykuł na temat „istoty JavaScript” . Być może nieco mniej formalnym przykładem jest raport Haskell 2010 .

jameshfisher
źródło
3
Czy podręcznik Advanced Bash Scripting Guide jest jednym z „miliona i jeden”?
choroba
2
Nie jestem przekonany, że bash ma „podstawowy model semantyczny” (no może „prawie wszystko jest ciągiem”); myślę, że to naprawdę cukier syntaktyczny do samego końca.
Gordon Davisson
4
To, co nazywasz „listą do prania cukru syntaktycznego”, jest w rzeczywistości semantycznym modelem ekspansji - niezwykle ważną częścią wykonania. 90% błędów i zamieszania wynika z niezrozumienia modelu ekspansji.
ten drugi facet
4
Rozumiem, dlaczego ktoś mógłby pomyśleć, że to szerokie pytanie, jeśli przeczytasz je tak, jak mam napisać skrypt powłoki ? Ale prawdziwe pytanie brzmi: jaka jest formalna semantyka i podstawa języka powłoki, a w szczególności bash? i jest to dobre pytanie z jedną spójną odpowiedzią. Głosowanie za ponownym otwarciem.
kojiro
1
Dowiedziałem się całkiem sporo na linuxcommand.org i jest nawet darmowy plik PDF z bardziej szczegółową książką o linii poleceń i pisaniu skryptów powłoki
samrap Kwietnia

Odpowiedzi:

107

Powłoka jest interfejsem systemu operacyjnego. Zwykle jest to mniej lub bardziej niezawodny język programowania sam w sobie, ale z funkcjami zaprojektowanymi tak, aby ułatwić interakcję z systemem operacyjnym i systemem plików. Semantyka powłoki POSIX (dalej nazywana po prostu "powłoką") jest trochę zmienna, łącząc niektóre cechy LISP (wyrażenia s mają wiele wspólnego z dzieleniem słów powłoki ) i C (większość składni arytmetycznej powłoki semantyka pochodzi z C).

Drugi rdzeń składni powłoki pochodzi z jej wychowania jako pomieszania poszczególnych narzędzi UNIX. Większość elementów często wbudowanych w powłokę można w rzeczywistości zaimplementować jako polecenia zewnętrzne. Rzuca wielu neofitów powłoki w pętlę, gdy zdają sobie sprawę, że /bin/[istnieje w wielu systemach.

$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t

wat?

Ma to o wiele więcej sensu, jeśli spojrzysz na sposób implementacji powłoki. Oto implementacja, którą wykonałem jako ćwiczenie. Jest w Pythonie, ale mam nadzieję, że to nie jest problem dla nikogo. Nie jest strasznie wytrzymały, ale jest pouczający:

#!/usr/bin/env python

from __future__ import print_function
import os, sys

'''Hacky barebones shell.'''

try:
  input=raw_input
except NameError:
  pass

def main():
  while True:
    cmd = input('prompt> ')
    args = cmd.split()
    if not args:
      continue
    cpid = os.fork()
    if cpid == 0:
      # We're in a child process
      os.execl(args[0], *args)
    else:
      os.waitpid(cpid, 0)

if __name__ == '__main__':
  main()

Mam nadzieję, że z powyższego wynika, że ​​model wykonania powłoki to prawie:

1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.

Rozbudowa, rozdzielczość poleceń, wykonanie. Cała semantyka powłoki jest związana z jedną z tych trzech rzeczy, chociaż są one znacznie bogatsze niż implementacja, którą napisałem powyżej.

Nie wszystkie polecenia fork. W rzeczywistości istnieje kilka poleceń, które nie mają zbyt wiele sensu zaimplementowanych jako zewnętrzne (takie, że musiałyby fork), ale nawet te często są dostępne jako zewnętrzne w celu zapewnienia ścisłej zgodności z POSIX.

Bash rozwija się na tej podstawie, dodając nowe funkcje i słowa kluczowe w celu ulepszenia powłoki POSIX. Jest prawie kompatybilny z sh, a bash jest tak wszechobecny, że niektórzy autorzy skryptów przez lata nie zdają sobie sprawy, że skrypt może w rzeczywistości nie działać w systemie ściśle POSIX-owym. (Zastanawiam się też, jak ludzie mogą tak bardzo przejmować się semantyką i stylem jednego języka programowania, a tak mało semantyce i stylowi powłoki, ale ja się różnią).

Kolejność wyceny

To trochę podchwytliwe pytanie: Bash interpretuje wyrażenia w swojej podstawowej składni od lewej do prawej, ale w składni arytmetycznej ma pierwszeństwo C. Wyrażenia różnią się jednak od rozszerzeń . Z EXPANSIONsekcji podręcznika bash:

Kolejność rozwinięć jest następująca: rozwijanie nawiasów; interpretacja tyldy, interpretacja parametrów i zmiennych, interpretacja wyrażeń arytmetycznych i podstawianie poleceń (wykonywane w sposób od lewej do prawej); podział na słowa; i rozwijanie nazw plików.

Jeśli rozumiesz dzielenie słów, rozwijanie nazw plików i rozwijanie parametrów, jesteś na dobrej drodze do zrozumienia większości tego, co robi bash. Zauważ, że rozwijanie nazw plików po rozdzieleniu słów jest krytyczne, ponieważ zapewnia, że ​​plik z białymi znakami w nazwie może być nadal dopasowany przez glob. Dlatego ogólnie dobre użycie rozszerzeń glob jest lepsze niż analizowanie poleceń .

Zakres

Zakres funkcji

Podobnie jak stary ECMAscript, powłoka ma zakres dynamiczny, chyba że jawnie zadeklarujesz nazwy w funkcji.

$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo

$ bar

$ x=123
$ foo
123
$ bar

$ …

Środowisko i „zakres” procesu

Podpowłoki dziedziczą zmienne swoich powłok nadrzędnych, ale inne rodzaje procesów nie dziedziczą niewyeksportowanych nazw.

$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'

$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y' # another way to transiently export a name
123

Możesz łączyć te zasady określania zakresu:

$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar

$

Dyscyplina typowania

Typy. Tak. Bash tak naprawdę nie ma typów i wszystko rozwija się do łańcucha (a może słowo byłoby bardziej odpowiednie). Ale przyjrzyjmy się różnym typom rozszerzeń.

Smyczki

Prawie wszystko można traktować jako ciąg. Barewords w bash to ciągi znaków, których znaczenie zależy całkowicie od zastosowanego do nich rozszerzenia.

Brak ekspansji

Warto wykazać, że nagie słowo jest tak naprawdę tylko słowem, a cytaty nic w tym nie zmieniają.

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Rozwinięcie podłańcucha
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Więcej informacji na temat rozszerzeń można znaleźć w Parameter Expansionsekcji podręcznika. Jest dość potężny.

Liczby całkowite i wyrażenia arytmetyczne

Możesz nasycić nazwy atrybutem liczby całkowitej, aby powiedzieć powłoce, aby traktowała prawą stronę wyrażeń przypisania jako arytmetykę. Następnie, gdy parametr się rozwinie, zostanie obliczony jako liczba całkowita przed rozwinięciem do… ciągu.

$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
2

Tablice

Argumenty i parametry pozycyjne

Zanim porozmawiamy o tablicach, warto omówić parametry pozycyjne. Argumenty do skryptu powłoki można uzyskać za pomocą ponumerowanych parametrów, $1, $2, $3, itd. Aby uzyskać dostęp do wszystkich tych parametrów jednocześnie za pomocą "$@", których ekspansja ma wiele wspólnego z tablic. Można ustawić i zmienić parametry pozycyjne uzywajac setlub shiftpoleceń wbudowanych, lub po prostu przez wywołanie powłoki lub funkcji powłoki z tymi parametrami:

$ bash -c 'for ((i=1;i<=$#;i++)); do
>   printf "\$%d => %s\n" "$i" "${@:i:1}"
> done' -- foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showpp() {
>   local i
>   for ((i=1;i<=$#;i++)); do
>     printf '$%d => %s\n' "$i" "${@:i:1}"
>   done
> }
$ showpp foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showshift() {
>   shift 3
>   showpp "$@"
> }
$ showshift foo bar baz biz quux xyzzy
$1 => biz
$2 => quux
$3 => xyzzy

Podręcznik bash czasami odnosi się również do $0parametru pozycyjnego. Uważam to za mylące, ponieważ nie uwzględnia go w liczbie argumentów $#, ale jest to numerowany parametr, więc ue. $0to nazwa powłoki lub bieżącego skryptu powłoki.

Tablice

Składnia tablic jest wzorowana na parametrach pozycyjnych, więc myślenie o tablicach jako o nazwanym rodzaju „zewnętrznych parametrów pozycyjnych” jest zazwyczaj zdrowe. Tablice można deklarować przy użyciu następujących metod:

$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )

Możesz uzyskać dostęp do elementów tablicy według indeksu:

$ echo "${foo[1]}"
element1

Możesz ciąć tablice:

$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"

Jeśli traktujesz tablicę jako normalny parametr, otrzymasz indeks zerowy.

$ echo "$baz"
element0
$ echo "$bar" # Even if the zeroth index isn't set

$ …

Jeśli użyjesz cudzysłowów lub odwrotnych ukośników, aby zapobiec dzieleniu słów, tablica zachowa określone dzielenie słów:

$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2

Główną różnicą między tablicami a parametrami pozycyjnymi są:

  1. Parametry pozycyjne nie są rzadkie. Jeśli $12jest ustawione, możesz być pewien, że $11jest ustawione. (Może być ustawiony na pusty łańcuch, ale $#nie będzie mniejszy niż 12.) Jeśli "${arr[12]}"jest ustawiona, nie ma gwarancji, że "${arr[11]}"zostanie ustawiona, a długość tablicy może wynosić zaledwie 1.
  2. Zerowy element tablicy jest jednoznacznie zerowym elementem tej tablicy. W parametrach pozycyjnych zerowy element nie jest pierwszym argumentem , ale nazwą powłoki lub skryptu powłoki.
  3. Do shifttablicy musisz wyciąć i przypisać ją ponownie, na przykład arr=( "${arr[@]:1}" ). Możesz też to zrobić unset arr[0], ale wtedy pierwszy element będzie miał indeks 1.
  4. Tablice mogą być współdzielone niejawnie między funkcjami powłoki jako globalne, ale musisz jawnie przekazać parametry pozycyjne do funkcji powłoki, aby je zobaczyła.

Często wygodnie jest używać rozszerzeń nazw plików do tworzenia tablic nazw plików:

$ dirs=( */ )

Polecenia

Polecenia są kluczowe, ale są też omówione bardziej szczegółowo niż w instrukcji. Przeczytaj SHELL GRAMMARsekcję. Istnieją różne rodzaje poleceń:

  1. Proste polecenia (np. $ startx)
  2. Rurociągi (np. $ yes | make config) (Lol)
  3. Listy (np. $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Polecenia złożone (np. $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Koprocesy (złożone, bez przykładu)
  6. Funkcje (nazwane polecenie złożone, które można traktować jako proste polecenie)

Model wykonania

Model wykonania obejmuje oczywiście zarówno stertę, jak i stos. Jest to typowe dla wszystkich programów UNIX. Bash ma również stos wywołań funkcji powłoki, widoczny poprzez zagnieżdżone użycie callerwbudowanego.

Bibliografia:

  1. SHELL GRAMMARRozdział podręcznika bash
  2. XCU Shell Command Language dokumentacja
  3. Bash Przewodnik na wiki GreyCat użytkownika.
  4. Zaawansowane programowanie w środowisku UNIX

Proszę o komentarze, jeśli chcesz, abym dalej się rozwijał w określonym kierunku.

kojiro
źródło
16
+1: Świetne wyjaśnienie. Doceń czas spędzony na pisaniu tego z przykładami.
jaypal singh
+1 za yes | make config;-) Ale poważnie, bardzo dobry opis.
Digital Trauma,
właśnie zacząłem to czytać… fajnie. zostawi kilka komentarzy. 1) jeszcze większa niespodzianka pojawia się, gdy to widzisz /bin/[i /bin/testczęsto jest to ta sama aplikacja 2) „Załóżmy, że pierwsze słowo to polecenie”. - spodziewaj się, kiedy wykonasz zadanie ...
Karoly Horvath
@KarolyHorvath Tak, celowo wykluczyłem przypisanie z mojej powłoki demonstracyjnej, ponieważ zmienne są skomplikowanym bałaganem. Ta powłoka demonstracyjna nie została napisana z myślą o tej odpowiedzi - została napisana znacznie wcześniej. Myślę, że mógłbym to zrobić execlei wstawić pierwsze słowa do środowiska, ale to i tak uczyniłoby to nieco bardziej skomplikowanym.
kojiro,
@kojiro: nie, to by to po prostu skomplikowało, ale z pewnością nie był to mój zamiar! ale przypisanie działa trochę inaczej (x) i IMHO powinieneś wspomnieć o tym gdzieś w tekście. (x): i źródło nieporozumień ... Nie mogę już nawet zliczyć, ile razy widziałem ludzi narzekających, że a = 1nie pracują).
Karoly Horvath
5

Odpowiedź na Twoje pytanie „Na czym polega dyscyplina pisania, np. Czy wszystko jest ciągiem znaków” Zmienne Bash to ciągi znaków. Ale Bash zezwala na operacje arytmetyczne i porównania na zmiennych, gdy zmienne są liczbami całkowitymi. Wyjątkiem od reguły Bash zmienne są łańcuchy znaków, gdy te zmienne są złożone lub zadeklarowane w inny sposób

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

Zadeklaruj znaczenie opcji:

  • -a Zmienna to tablica.
  • -f Używa tylko nazw funkcji.
  • -i Zmienną należy traktować jako liczbę całkowitą; ocena arytmetyczna jest wykonywana, gdy zmiennej przypisana jest wartość.
  • -p Wyświetla atrybuty i wartości każdej zmiennej. Gdy używane jest -p, dodatkowe opcje są ignorowane.
  • -r Ustaw zmienne jako tylko do odczytu. Zmiennym tym nie można następnie przypisać wartości za pomocą kolejnych instrukcji przypisania ani nie można ich cofnąć.
  • -t Nadaj każdej zmiennej atrybut śledzenia.
  • -x Zaznacz każdą zmienną do wyeksportowania do kolejnych poleceń za pośrednictwem środowiska.
Keith Reynolds
źródło
1

Strona podręcznika basha zawiera trochę więcej informacji niż większość stron podręcznika, a także zawiera część tego, o co prosisz. Moje założenie po ponad dekadzie skryptowania bash jest takie, że ze względu na swoją historię jako rozszerzenie sh, ma on pewną funky składnię (aby zachować wsteczną kompatybilność z sh).

FWIW, moje doświadczenie było takie jak Twoje; chociaż różne książki (np. O'Reilly „Learning the Bash Shell” i podobne) pomagają w składni, istnieje wiele dziwnych sposobów rozwiązywania różnych problemów, a niektórych z nich nie ma w książce i należy je przeszukać.

philwalk
źródło