Środowisko programisty/Bash - skrypty złożone: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Pan (dyskusja | edycje)
m Pliki specjalne: Literówka
Pan (dyskusja | edycje)
Poprawki i formatowanie
 
Linia 1: Linia 1:
== Instrukcja wyboru ==
== Instrukcja wyboru ==


'''case''' jest instrukcją wyboru, która jest krótszą alternatywą dla instrukcji if, gdy testujemy jedną wartość. Jej składnia to:
<code>case</code> jest instrukcją wyboru, która jest krótszą alternatywą dla instrukcji <code>if</code>, gdy testujemy jedną wartość. Jej składnia to:
  case wartość in
  case wartość in
   wzorzec1)
   wzorzec1)
Linia 11: Linia 11:
   ...
   ...
  esac
  esac
Wzorce mają formę podobną do wzorców plików, tzn. może to być konkretna wartość, a może też zawierać znaki * i ?. '''case''' dopasowuje '''wartość''' do jednego z wzorców. Dla pierwszego dopasowanego wzorca wykonywane są odpowiadające mu instrukcje. Jeśli nie uda się dopasować do żadnego z wzorców, to nie są wykonywane żadne instrukcje.
Wzorce mają formę podobną do wzorców plików, tzn. może to być konkretna wartość, a może też zawierać znaki <code>*</code> i <code>?</code>. <code>case</code> dopasowuje <code>wartość</code> do jednego z wzorców. Dla pierwszego dopasowanego wzorca wykonywane są odpowiadające mu instrukcje. Jeśli nie uda się dopasować do żadnego z wzorców, to nie są wykonywane żadne instrukcje.
  #!/bin/sh
  #!/bin/sh
   
   
Linia 38: Linia 38:
   instrukcje
   instrukcje
  done
  done
Interpreter tak długo wykonuje '''instrukcje''', jak długo jest spełniony '''warunek'''. Podobnie, jak przy instrukcji '''if''', '''warunek''' jest poleceniem, które jest uruchamiane przy każdym obrocie pętli. Jeśli status wyjścia jest równy zero, to wykonywane są instrukcje podane w bloku pętli.
Interpreter tak długo wykonuje <code>instrukcje</code>, jak długo jest spełniony <code>warunek</code>. Podobnie, jak przy instrukcji <code>if</code>, <code>warunek</code> jest poleceniem, które jest uruchamiane przy każdym obrocie pętli. Jeśli status wyjścia jest równy zero, to wykonywane są instrukcje podane w bloku pętli.


Przykład.
Przykład.
Linia 50: Linia 50:
=== until ===
=== until ===


Pętla '''until''' jest bardzo podobna do pętli '''while''':
Pętla <code>until</code> jest bardzo podobna do pętli <code>while</code>:
  until warunek; do
  until warunek; do
   instrukcje
   instrukcje
  done
  done
Różnica polega na tym, że pętla jest wykonywana tak długo, jak warunek jest nieprawdziwy (przeciwnie do tego jak ma się to w pętli while). Na przykład pętla z poprzedniego przykładu mogła by wyglądać tak:
Różnica polega na tym, że pętla jest wykonywana tak długo, jak warunek jest nieprawdziwy (przeciwnie do tego jak ma się to w pętli <code>while</code>). Na przykład pętla z poprzedniego przykładu mogła by wyglądać tak:
  until [ "$zm" == koniec ]; do
  until [ "$zm" == koniec ]; do
   ...
   ...
Linia 61: Linia 61:
=== for ===
=== for ===


Pętla '''for''' ma dwie formy. Pierwsza forma służy wykonywania bloku instrukcji dla każdej wartości argumentów z listy, a druga forma ma bardziej złożoną składnię i jest zapożyczona języka C.
Pętla <code>for</code> ma dwie formy. Pierwsza forma służy wykonywania bloku instrukcji dla każdej wartości argumentów z listy, a druga forma ma bardziej złożoną składnię i jest zapożyczona z języka C.


==== for dla list ====
==== for dla list ====
Linia 69: Linia 69:
   instrukcje
   instrukcje
  done
  done
gdzie '''lista''' jest listą wartości. Listę podajemy w analogiczny sposób, jak argumenty poleceniu, czyli na przykład możemy używać wzorców nazw plików do podania wielu nazw plików naraz. Instrukcję w bloku są wykonywane dla każdej wartości znajdującej się na liście. W danym obrocie wartość z listy przypisywana jest na zmienną '''zm'''.
gdzie <code>lista</code> jest listą wartości. Listę podajemy w analogiczny sposób, jak argumenty poleceniu, czyli na przykład możemy używać wzorców nazw plików do podania wielu nazw plików naraz. Instrukcje w bloku są wykonywane dla każdej wartości znajdującej się na liście. W danym obrocie wartość z listy przypisywana jest na zmienną <code>zm</code>.


Przykład:
Przykład:
Linia 88: Linia 88:


Listą może też być wynik innego polecenia:
Listą może też być wynik innego polecenia:
  # Wyszukanie wszystkich tych plików o rozszerzeniu '''txt''',
  # Wyszukanie wszystkich tych plików o rozszerzeniu txt,
  # dla których ostatnia linia zawiera napis "Autor: Jan Kowalski"
  # dla których ostatnia linia zawiera napis "Autor: Jan Kowalski"
  for f in `find . -name "*.txt" -type f`; do
  for f in `find . -name "*.txt" -type f`; do
Linia 104: Linia 104:
  done
  done


Listę można utworzyć przez połączenie dwóch innych list, na przykład '''0 "$@"''' jest listą składającą się z elementu '''0''' oraz z wszystkich argumentów skryptu.
Listę można utworzyć przez połączenie dwóch innych list, na przykład <code>0 "$@"</code> jest listą składającą się z elementu <code>0</code> oraz z wszystkich argumentów skryptu.


==== for w stylu C ====
==== for w stylu C ====


Bardziej skomplikowana wersja pętli '''for''' w stylu C ma postać:
Bardziej skomplikowana wersja pętli <code>for</code> w stylu C ma postać:
  for ((inicjacja; warunek; post_modyfikacja)); do
  for ((inicjacja; warunek; post_modyfikacja)); do
   instrukcje
   instrukcje
  done
  done
'''inicjacja''', '''warunek''' i '''post_modyfikacja''' są wyrażeniami takimi jak wyrażenia używane w konstrukcjach '''$(( ... ))''' i '''(( ... ))'''. Działanie jest następujące:
<code>inicjacja</code>, <code>warunek</code> i <code>post_modyfikacja</code> są wyrażeniami takimi jak wyrażenia używane w konstrukcjach <code>$(( ... ))</code> i <code>(( ... ))</code>. Działanie jest następujące.


Na początku i tylko raz, jest uruchamiane wyrażenie '''inicjacja'''. To wyrażenie zazwyczaj ma za zadanie zainicjowanie zmiennych używanych do iterowania pętli.
Na początku i tylko raz, jest uruchamiane wyrażenie <code>inicjacja</code>. To wyrażenie zazwyczaj ma za zadanie zainicjowanie zmiennych używanych do iterowania pętli.


Następnie przed każdą iteracją wyliczane jest wyrażenie '''warunek'''. Jeśli jest ono fałszywe, wykonywanie pętli kończy się. Jeśli jest ono prawdziwe, wykonywany jest blok '''instrukcje'''.
Następnie przed każdą iteracją wyliczane jest wyrażenie <code>warunek</code>. Jeśli jest ono fałszywe, wykonywanie pętli kończy się. Jeśli jest ono prawdziwe, wykonywany jest blok <code>instrukcje</code>.


Po każdej iteracji wykonywane jest wyrażenie '''post_modyfikacja'''. To wyrażenie zazwyczaj ma za zadanie modyfikację zmiennych.
Po każdej iteracji wykonywane jest wyrażenie <code>post_modyfikacja</code>. To wyrażenie zazwyczaj ma za zadanie modyfikowanie zmiennych.


Przykład. Kilka sposobów wypisania liczb od 1 do 10:
Przykład. Kilka sposobów wypisania liczb od 1 do 10:
Linia 142: Linia 142:
  done
  done
  echo
  echo
Polecenie '''seq''' służy do generowanie ciągów arytmetycznych.
Polecenie <code>seq</code> służy do generowanie ciągów arytmetycznych.


=== break i continue ===
=== break i continue ===


Wewnątrz pętli dostępne są dwa dodatkowe polecenia:
Wewnątrz pętli dostępne są dwa dodatkowe polecenia:
* break,
* <code>break</code>,
* continue.
* <code>continue</code>.
'''break''' powoduje natychmiastowe przerwanie wykonywanej pętli. '''continue''' powoduje zakończenie aktualnej iteracji pętli i przejście do następnej iteracji.
<code>break</code> powoduje natychmiastowe przerwanie wykonywanej pętli. <code>continue</code> powoduje zakończenie aktualnej iteracji pętli i przejście do następnej iteracji.
  while true; do
  while true; do
   read a
   read a
Linia 167: Linia 167:
   instrukcje
   instrukcje
  }
  }
=== Wywoływanie funkcji i argumenty ===
=== Wywoływanie funkcji i argumenty ===
Zadeklarowana funkcja dostępna jest dla potrzeb funkcji jak nowe polecenie. Wywołujemy ją używając jej nazwy. Możemy przekazywać argumenty w ten sam sposób ,w jaki przekazujemy poleceniom.
 
Zadeklarowana funkcja dostępna jest dla potrzeb skryptu jak nowe polecenie. Wywołujemy ją używając jej nazwy. Możemy przekazywać argumenty w ten sam sposób, w jaki przekazujemy je poleceniom.
  # Przykład pokazujący deklarację i wywołanie funkcji z parametrami
  # Przykład pokazujący deklarację i wywołanie funkcji z parametrami
   
   
Linia 181: Linia 183:
   
   
  wypisz_argumenty
  wypisz_argumenty
wypisz_argumenty "$@"
  wypisz_argumenty raz dwa trzy cztery pięć
  wypisz_argumenty raz dwa trzy cztery pięć


Linia 202: Linia 205:
=== Status wyjścia ===
=== Status wyjścia ===


Ponieważ funkcja zachowuje się jak polecenie, to może też zwracać status wyjścia. Domyślnie zwracane jest zero. Aby wyjść z funkcji z zadanym statusem służy polecenie return.
Ponieważ funkcja zachowuje się jak polecenie, to może też zwracać status wyjścia. Domyślnie zwracane jest zero. Aby wyjść z funkcji z zadanym statusem służy polecenie <code>return</code>.
  pytanie_tak_nie ()
  pytanie_tak_nie ()
  {
  {
Linia 226: Linia 229:
== Podshelle ==
== Podshelle ==


Uruchamianie skryptów wiąże się z tworzeniem nowego procesu. Wszystko, co dostaje skrypt, dostaje też środowisko i argumenty. Tworząc nowy proces, możemy mu przekierować wejście i wyjście. Ponadto, jeśli uruchamiamy inny skrypt, to nie widzi on zmiennych, które zadeklarowaliśmy (chyba, że je wyeksportowaliśmy do środowiska). Po powrocie z wywołania innego skryptu bądź programu mamy pewność, że zadeklarowane zmienne nie zostały zmienione, podobnie jak argumenty skryptu. Wiemy też, że katalog bieżący jest niezmieniony.
Uruchamianie skryptów wiąże się z tworzeniem nowego procesu. Wszystko, co dostaje skrypt, to środowisko i argumenty. Tworząc nowy proces, możemy mu przekierować wejście i wyjście. Ponadto, jeśli uruchamiamy inny skrypt, to nie widzi on zmiennych, które zadeklarowaliśmy (chyba, że je wyeksportowaliśmy do środowiska). Po powrocie z wywołania innego skryptu bądź programu mamy pewność, że zadeklarowane zmienne nie zostały zmienione, podobnie jak argumenty skryptu. Wiemy też, że katalog bieżący jest niezmieniony.


Tego typu własności mogą być pożądane, gdy chcemy wykonać ciąg poleceń w skrypcie. Istnieje możliwość zrobienia tego bez pisania pliku z nowym skryptem. Można to zrobić w aktualnym skrypcie używając konstrukcji z nawiasami:
Tego typu własności mogą być pożądane, gdy chcemy wykonać ciąg poleceń w skrypcie. Istnieje możliwość zrobienia tego bez pisania pliku z nowym skryptem. Można to zrobić w aktualnym skrypcie używając konstrukcji z nawiasami:
Linia 238: Linia 241:
  /home/bashtest
  /home/bashtest
  bashtest@host:~$
  bashtest@host:~$
Konstrukcja '''( ... )''' przydaje się też do grupowania poleceń po to, aby używać przekierowań lub potoku do całej grupy instrukcji.
Konstrukcja <code>( ... )</code> przydaje się też do grupowania poleceń po to, aby używać przekierowań lub potoku do całej grupy instrukcji.
  (
  (
   if [ ! -d "$1" ]; then
   if [ ! -d "$1" ]; then
Linia 252: Linia 255:
   done
   done
  ) | less
  ) | less
Powyższy kawałek skryptu wyszuka w katalogu podanym w pierwszym argumencie wszystkie pliki o rozszerzeniu txt i dla każdego z nich wyświetli główkę oraz wszystkie linie zawierające tekst TODO. Wynik będzie można przejrzeć programem less.
Powyższy kawałek skryptu wyszuka w katalogu podanym w pierwszym argumencie wszystkie pliki o rozszerzeniu <code>txt</code> i dla każdego z nich wyświetli główkę oraz wszystkie linie zawierające tekst <code>TODO</code>. Wynik będzie można przejrzeć programem <code>less</code>.


Uruchamianie nowych procesów nie jest zbyt szybkie. Jeśli zależy nam na szybkości, należy unikać tworzenia nowych procesów. Na przykład
Uruchamianie nowych procesów nie jest zbyt szybkie. Jeśli zależy nam na szybkości, należy unikać tworzenia nowych procesów. Na przykład
  cat plik | grep wzorzec
  cat plik | grep wzorzec
można zastąpić poleceniem
można zastąpić poleceniem
  grep wzorzec plik,
  grep wzorzec plik
które uruchamia tylko jeden proces, a nie dwa, jak w poprzednim wywołaniu (dodatkowy proces to polecenie cat).
które uruchamia tylko jeden proces, a nie dwa, jak w poprzednim wywołaniu (dodatkowy proces to polecenie <code>cat</code>).


Warto wiedzieć, że wiele poleceń jest wbudowanych w Basha i nie są dla nich uruchamiane nowe procesy. Takie polecenia to na przykład: cd ,echo, test. Jeśli chcemy zgrupować polecenia, to zamiast używać konstrukcji '''( ... )''' można używać '''{ ... }'''. Różnica jest taka, że nie jest uruchamiany nowy shell, a co się z tym wiąże, wszelkie modyfikacje dokonane wewnątrz tego bloku są widoczne po wyjściu z tego bloku.
Warto wiedzieć, że wiele poleceń jest wbudowanych w Basha i nie są dla nich uruchamiane nowe procesy. Takie polecenia to na przykład: <code>cd</code>, <code>echo</code>, <code>test</code>. Jeśli chcemy zgrupować polecenia, to zamiast używać konstrukcji <code>( ... )</code> można używać <code>{ ... }</code>. Różnica jest taka, że nie jest uruchamiany nowy shell, a co się z tym wiąże, wszelkie modyfikacje dokonane wewnątrz tego bloku są widoczne po wyjściu z tego bloku.
  bashtest@host:~$ a=2; { echo $a; a=3; echo $a; }; echo $a;  
  bashtest@host:~$ a=2; { echo $a; a=3; echo $a; }; echo $a;  
  2
  2
Linia 269: Linia 272:
== Referencje ==
== Referencje ==


W jaki sposób dobrać się do zmiennej, której nazwa znajduje się na innej zmiennej. Otóż możemy użyć do tego polecenie '''eval''':
W jaki sposób dobrać się do zmiennej, której nazwa znajduje się na innej zmiennej. Otóż możemy użyć do tego polecenia <code>eval</code>:
  eval argumenty
  eval argumenty
Działa ono tak, że tworzona jest instrukcja składająca się z podanych argumentów. Niby nic w tym specjalnego, ale jest to w pewien sposób użyteczne. Na przykład, gdy w argumentach znajduje się wywołanie do zmiennej (np. '''$zmienna'''), wtedy wpierw wstawiana jest wartość zmiennej, potem tworzona jest instrukcja i dopiero na końcu jest wykonywana. Pozwala nam to tworzyć polecenia, w których skład wchodzą wartości zmiennych.
Działa ono tak, że tworzona jest instrukcja składająca się z podanych argumentów. Niby nic w tym specjalnego, ale jest to w pewien sposób użyteczne. Na przykład, gdy w argumentach znajduje się wywołanie do zmiennej (np. <code>$zmienna</code>), wtedy wpierw wstawiana jest wartość zmiennej, potem tworzona jest instrukcja i dopiero na końcu jest wykonywana. Pozwala nam to tworzyć polecenia, w których skład wchodzą wartości zmiennych.
  bashtest@host:~$ a=wartość
  bashtest@host:~$ a=wartość
  bashtest@host:~$ echo $a
  bashtest@host:~$ echo $a
Linia 295: Linia 298:
  echo "a=$a" # wypisze a=domy
  echo "a=$a" # wypisze a=domy
  echo "b=$b" # wypisze b=kąty
  echo "b=$b" # wypisze b=kąty
Widzimy, że tutaj odwołanie do zmiennej wymaga nawet użycia formy '''${...}'''. Zauważmy, że gdybyśmy mieli w czwartej linii taką instrukcję
Widzimy, że tutaj odwołanie do zmiennej wymaga nawet użycia formy <code>${...}</code>. Zauważmy, że gdybyśmy mieli w czwartej linii taką instrukcję
     eval $z=\$${z}y
     eval $z=\$${z}y
to nie byłoby to poprawne. Otóż dla '''z=a''' wartością '''$z''' jaki i '''${z}''' było by '''a''', więc polecenie utworzone przez eval do wykonania brzmiałoby '''a=$ay'''.
to nie byłoby to poprawne. Otóż dla <code>z=a</code> wartością <code>$z</code> jaki i <code>${z}</code> było by <code>a</code>, więc polecenie utworzone przez <code>eval</code> do wykonania brzmiałoby <code>a=$ay</code>.


== Pliki specjalne ==
== Pliki specjalne ==


W systemie Linux dostępne są tak zwane pliki specjalne. Na przykład '''/dev/null'''. Jest to taki plik, który zawsze, jak z niego czytamy, jest plikiem pustym, a wszystko, co do niego wpiszemy, znika. Ten plik jest przydatny gdy chcemy, aby wyjście polecenia nie zostało nigdzie wyświetlone.
W systemie Linux dostępne są tak zwane pliki specjalne. Na przykład <code>/dev/null</code>. Jest to taki plik, który zawsze, jak z niego czytamy, jest plikiem pustym, a wszystko, co do niego wpiszemy, znika. Ten plik jest przydatny gdy chcemy, aby wyjście polecenia nie zostało nigdzie wyświetlone.
  polecenie 2>/dev/null >/dev/null
  polecenie 2>/dev/null >/dev/null
Ignorowanie wyjścia jest przydatne, gdy interesuje nas tylko status wyjścia.
Ignorowanie wyjścia jest przydatne, gdy interesuje nas tylko status wyjścia.


Innym plikiem specjalnym jest '''/dev/zero'''. Wszystko co się wpisze do tego pliku, również znika, ale gdy czytamy z tego pliku otrzymujemy zawsze bajty, których wartość binarna wynosi 0. Ten plik można wykorzystać do utworzenia pliku o określonej wielkości. Na przykład, aby utworzyć plik '''1m.dat''' składający się dokładnie z jednego megabajta znaków możemy użyć polecenia
Innym plikiem specjalnym jest <code>/dev/zero</code>. Wszystko co się wpisze do tego pliku, również znika, ale gdy czytamy z tego pliku otrzymujemy zawsze bajty, których wartość binarna wynosi 0. Ten plik można wykorzystać do utworzenia pliku o określonej wielkości. Na przykład, aby utworzyć plik <code>1m.dat</code> składający się dokładnie z jednego megabajta znaków możemy użyć polecenia
  dd if=/dev/zero of=1m.dat bs=1M count=1
  dd if=/dev/zero of=1m.dat bs=1M count=1
Polecenie '''dd''' służy do kopiowania z jednego pliku do drugiego.
Polecenie <code>dd</code> służy do kopiowania z jednego pliku do drugiego określonej liczby bajtów.


Inne pliki specjalne znajdują się w katalogu '''/proc'''. W katalogu tym są najróżniejsze informacje o systemie, o procesach. Podamy tu dla przykładu dwa pliki: '''/proc/cpuinfo''', '''/proc/meminfo'''. Pierwszy wyświetla informacje o procesorach znajdujących się w danym komputerze, a drugi wyświetla informacje o dostępnej pamięci operacyjnej.
Inne pliki specjalne znajdują się w katalogu <code>/proc</code>. W katalogu tym są najróżniejsze informacje o systemie, o procesach. Podamy tu dla przykładu dwa pliki: <code>/proc/cpuinfo</code>, <code>/proc/meminfo</code>. Pierwszy wyświetla informacje o procesorach znajdujących się w danym komputerze, a drugi wyświetla informacje o dostępnej pamięci operacyjnej.

Aktualna wersja na dzień 10:09, 30 wrz 2006

Instrukcja wyboru

case jest instrukcją wyboru, która jest krótszą alternatywą dla instrukcji if, gdy testujemy jedną wartość. Jej składnia to:

case wartość in
  wzorzec1)
    instrukcje1
    ;;
  wzorzec2)
    instrukcje2
    ;;
  ...
esac

Wzorce mają formę podobną do wzorców plików, tzn. może to być konkretna wartość, a może też zawierać znaki * i ?. case dopasowuje wartość do jednego z wzorców. Dla pierwszego dopasowanego wzorca wykonywane są odpowiadające mu instrukcje. Jeśli nie uda się dopasować do żadnego z wzorców, to nie są wykonywane żadne instrukcje.

#!/bin/sh

case $1 in
  "")
    echo "Prawidłowe wywołanie to: $0 plik"
    exit 1
    ;;
  *.txt) # jeśli plik tekstowy, to go uruchamiamy edytor
    pico $1
    ;;
  *.sh)  # jeśli skrypt to go uruchamiamy
    ./$1
    ;;
  *)     # to oznacza wszystkie wartości; dalsze wzorce nie mają już sensu
    echo "Nieznany rodzaj pliku '$1'"
    ;;
esac

Pętle

while

Pętla while ma składnię

while warunek; do
  instrukcje
done

Interpreter tak długo wykonuje instrukcje, jak długo jest spełniony warunek. Podobnie, jak przy instrukcji if, warunek jest poleceniem, które jest uruchamiane przy każdym obrocie pętli. Jeśli status wyjścia jest równy zero, to wykonywane są instrukcje podane w bloku pętli.

Przykład.

zm=""
while [ "$zm" != koniec ]; do
  echo -n "Wpisz coś (słowo 'koniec' aby zakończyć): "
  read zm
  echo "Wpisałeś '$zm'"
done

until

Pętla until jest bardzo podobna do pętli while:

until warunek; do
  instrukcje
done

Różnica polega na tym, że pętla jest wykonywana tak długo, jak warunek jest nieprawdziwy (przeciwnie do tego jak ma się to w pętli while). Na przykład pętla z poprzedniego przykładu mogła by wyglądać tak:

until [ "$zm" == koniec ]; do
  ...
done

for

Pętla for ma dwie formy. Pierwsza forma służy wykonywania bloku instrukcji dla każdej wartości argumentów z listy, a druga forma ma bardziej złożoną składnię i jest zapożyczona z języka C.

for dla list

Ta wersja ma postać:

for zm in lista; do
  instrukcje
done

gdzie lista jest listą wartości. Listę podajemy w analogiczny sposób, jak argumenty poleceniu, czyli na przykład możemy używać wzorców nazw plików do podania wielu nazw plików naraz. Instrukcje w bloku są wykonywane dla każdej wartości znajdującej się na liście. W danym obrocie wartość z listy przypisywana jest na zmienną zm.

Przykład:

for f in *; do # * rozwija się do listy wszystkich plików/katalogów znajdujących się w bieżącym katalogu
  if [ -d "$f" ]; then
    echo "Katalog '$f'"
  elif [ -f "$f" ]; then
    echo "Plik '$f'"
  else
    echo "Inny typ '$f'"
  fi
done

Listą może być też ciąg wartości. Na przykład, aby wyświetlić kwadraty wybranych liczb, możemy te liczby umieścić na liście:

for i in 1 5 100 99; do
  echo "Kwadrat $i = $((i * i))"
done

Listą może też być wynik innego polecenia:

# Wyszukanie wszystkich tych plików o rozszerzeniu txt,
# dla których ostatnia linia zawiera napis "Autor: Jan Kowalski"
for f in `find . -name "*.txt" -type f`; do
  if [ "`tail -1 $f`" == "Autor: Jan Kowalski" ]; then
    echo "Plik '$f' posiada już podpis"
  fi
done

Można także w liście umieścić argumenty skryptu:

# Wypisywanie argumentów skryptu
n=1
for arg in "$@"; do
  echo "Argument $n: '$arg'"
  let "n++"
done

Listę można utworzyć przez połączenie dwóch innych list, na przykład 0 "$@" jest listą składającą się z elementu 0 oraz z wszystkich argumentów skryptu.

for w stylu C

Bardziej skomplikowana wersja pętli for w stylu C ma postać:

for ((inicjacja; warunek; post_modyfikacja)); do
  instrukcje
done

inicjacja, warunek i post_modyfikacja są wyrażeniami takimi jak wyrażenia używane w konstrukcjach $(( ... )) i (( ... )). Działanie jest następujące.

Na początku i tylko raz, jest uruchamiane wyrażenie inicjacja. To wyrażenie zazwyczaj ma za zadanie zainicjowanie zmiennych używanych do iterowania pętli.

Następnie przed każdą iteracją wyliczane jest wyrażenie warunek. Jeśli jest ono fałszywe, wykonywanie pętli kończy się. Jeśli jest ono prawdziwe, wykonywany jest blok instrukcje.

Po każdej iteracji wykonywane jest wyrażenie post_modyfikacja. To wyrażenie zazwyczaj ma za zadanie modyfikowanie zmiennych.

Przykład. Kilka sposobów wypisania liczb od 1 do 10:

i=1
while ((i <= 10)); do
  echo -n "$i "
  let "i++"
done
echo

for i in 1 2 3 4 5 6 7 8 9 10; do
  echo -n "$i "
done
echo

for i in `seq 1 10`; do
  echo -n "$i "
done
echo

for ((i = 1; i <= 10; i++)); do
  echo -n "$i "
done
echo

Polecenie seq służy do generowanie ciągów arytmetycznych.

break i continue

Wewnątrz pętli dostępne są dwa dodatkowe polecenia:

  • break,
  • continue.

break powoduje natychmiastowe przerwanie wykonywanej pętli. continue powoduje zakończenie aktualnej iteracji pętli i przejście do następnej iteracji.

while true; do
  read a
  if [ "$a" = "koniec" ]; then
    break
  elif [ "$a" = "dalej" ]; then
    continue
  fi
  echo "Wpisałeś '$a'"
done

Funkcje

Wewnątrz skryptu można pisać własne funkcje, które spełniają rolę podprogramu. Ma to miejsce na przykład wtedy, gdy pewną czynność chcemy wykonać wielokrotnie w różnych miejscach skryptu i chcemy uniknąć kopiowania kodu. Funkcję deklarujemy w skrypcie w następujący sposób:

nazwa_funkcji ()
{
  instrukcje
}

Wywoływanie funkcji i argumenty

Zadeklarowana funkcja dostępna jest dla potrzeb skryptu jak nowe polecenie. Wywołujemy ją używając jej nazwy. Możemy przekazywać argumenty w ten sam sposób, w jaki przekazujemy je poleceniom.

# Przykład pokazujący deklarację i wywołanie funkcji z parametrami

wypisz_argumenty ()
{
  echo -n "Jest $# argumentów:"
  for i in "$@"; do
    echo -n " '$i'"
  done
  echo
}

wypisz_argumenty
wypisz_argumenty "$@"
wypisz_argumenty raz dwa trzy cztery pięć

Zasięg deklaracji

Deklaracja funkcji jest instrukcją, w wyniku której dostępne staje się nowe polecenie. Czyli na przykład można deklarować funkcję wewnątrz bloków, a nie można używać funkcji, których deklaracja następuje później.

f    # błąd - funkcja nie jest zadeklarowana
if [ "$USER" = bashtest ]; then
  f ()
  {
    echo "Pierwsza wersja f"
  }
else
  f ()
  {
    echo "Druga wersja f"
  }
fi
f    # funkcja f może być zadeklarowana na dwa sposoby, zależnie od tego jaki użytkownik uruchomił skrypt

Status wyjścia

Ponieważ funkcja zachowuje się jak polecenie, to może też zwracać status wyjścia. Domyślnie zwracane jest zero. Aby wyjść z funkcji z zadanym statusem służy polecenie return.

pytanie_tak_nie ()
{
  while true; do
    if [ $# -ge 1 ]; then
      echo -n "$1 (tak/nie)? "
    fi
    read odp
    if [ "$odp" = tak ]; then
      return 0
    elif [ "$odp" = nie ]; then
      return 1
    fi
  done
}

if pytanie_tak_nie "Czy chcesz usłyszeć pytanie"; then
  until pytanie_tak_nie "Czy chcesz zakończyć ten skrypt"; do
    : # : jest wbudowaną pustą instrukcją
  done
fi

Podshelle

Uruchamianie skryptów wiąże się z tworzeniem nowego procesu. Wszystko, co dostaje skrypt, to środowisko i argumenty. Tworząc nowy proces, możemy mu przekierować wejście i wyjście. Ponadto, jeśli uruchamiamy inny skrypt, to nie widzi on zmiennych, które zadeklarowaliśmy (chyba, że je wyeksportowaliśmy do środowiska). Po powrocie z wywołania innego skryptu bądź programu mamy pewność, że zadeklarowane zmienne nie zostały zmienione, podobnie jak argumenty skryptu. Wiemy też, że katalog bieżący jest niezmieniony.

Tego typu własności mogą być pożądane, gdy chcemy wykonać ciąg poleceń w skrypcie. Istnieje możliwość zrobienia tego bez pisania pliku z nowym skryptem. Można to zrobić w aktualnym skrypcie używając konstrukcji z nawiasami:

( instrukcje )

Instrukcje między nawiasami są uruchomione w nowym shellu. Dziedziczy po naszym skrypcie wszystkie zmienne:

bashtest@host:~$ a=2; ( echo $a; a=3; echo $a ); echo $a
2
3
2
bashtest@host:~$ ( cd /tmp ); echo $PWD
/home/bashtest
bashtest@host:~$

Konstrukcja ( ... ) przydaje się też do grupowania poleceń po to, aby używać przekierowań lub potoku do całej grupy instrukcji.

(
  if [ ! -d "$1" ]; then
    echo "$1 nie jest katalogiem
    exit 1 # wychodzi tylko z podshella
  fi
  cd "$1"
  for i in *.txt; do
    if [ -f "$i" ]; then
      echo "==== Do zrobienia w pliku '$i' ===="
      grep TODO "$i"
    fi
  done
) | less

Powyższy kawałek skryptu wyszuka w katalogu podanym w pierwszym argumencie wszystkie pliki o rozszerzeniu txt i dla każdego z nich wyświetli główkę oraz wszystkie linie zawierające tekst TODO. Wynik będzie można przejrzeć programem less.

Uruchamianie nowych procesów nie jest zbyt szybkie. Jeśli zależy nam na szybkości, należy unikać tworzenia nowych procesów. Na przykład

cat plik | grep wzorzec

można zastąpić poleceniem

grep wzorzec plik

które uruchamia tylko jeden proces, a nie dwa, jak w poprzednim wywołaniu (dodatkowy proces to polecenie cat).

Warto wiedzieć, że wiele poleceń jest wbudowanych w Basha i nie są dla nich uruchamiane nowe procesy. Takie polecenia to na przykład: cd, echo, test. Jeśli chcemy zgrupować polecenia, to zamiast używać konstrukcji ( ... ) można używać { ... }. Różnica jest taka, że nie jest uruchamiany nowy shell, a co się z tym wiąże, wszelkie modyfikacje dokonane wewnątrz tego bloku są widoczne po wyjściu z tego bloku.

bashtest@host:~$ a=2; { echo $a; a=3; echo $a; }; echo $a; 
2
3
3
bashtest@host:~$

Referencje

W jaki sposób dobrać się do zmiennej, której nazwa znajduje się na innej zmiennej. Otóż możemy użyć do tego polecenia eval:

eval argumenty

Działa ono tak, że tworzona jest instrukcja składająca się z podanych argumentów. Niby nic w tym specjalnego, ale jest to w pewien sposób użyteczne. Na przykład, gdy w argumentach znajduje się wywołanie do zmiennej (np. $zmienna), wtedy wpierw wstawiana jest wartość zmiennej, potem tworzona jest instrukcja i dopiero na końcu jest wykonywana. Pozwala nam to tworzyć polecenia, w których skład wchodzą wartości zmiennych.

bashtest@host:~$ a=wartość
bashtest@host:~$ echo $a
wartość
bashtest@host:~$ b=a
bashtest@host:~$ echo $b
a
bashtest@host:~$ eval echo \$$b
wartość
bashtest@host:~$

Taki sposób przechowywania informacji o zmiennej (tzn. pamiętanie tylko nazwy zmiennej) nazywamy referencjami. W ten sposób możemy przekazywać zmienne funkcjom.

dopisz_y ()
{
  for z in "$@"; do
    eval $z=\$\{$z\}y
  done
}

a=dom
b=kąt
dopisz_y a b
echo "a=$a" # wypisze a=domy
echo "b=$b" # wypisze b=kąty

Widzimy, że tutaj odwołanie do zmiennej wymaga nawet użycia formy ${...}. Zauważmy, że gdybyśmy mieli w czwartej linii taką instrukcję

    eval $z=\$${z}y

to nie byłoby to poprawne. Otóż dla z=a wartością $z jaki i ${z} było by a, więc polecenie utworzone przez eval do wykonania brzmiałoby a=$ay.

Pliki specjalne

W systemie Linux dostępne są tak zwane pliki specjalne. Na przykład /dev/null. Jest to taki plik, który zawsze, jak z niego czytamy, jest plikiem pustym, a wszystko, co do niego wpiszemy, znika. Ten plik jest przydatny gdy chcemy, aby wyjście polecenia nie zostało nigdzie wyświetlone.

polecenie 2>/dev/null >/dev/null

Ignorowanie wyjścia jest przydatne, gdy interesuje nas tylko status wyjścia.

Innym plikiem specjalnym jest /dev/zero. Wszystko co się wpisze do tego pliku, również znika, ale gdy czytamy z tego pliku otrzymujemy zawsze bajty, których wartość binarna wynosi 0. Ten plik można wykorzystać do utworzenia pliku o określonej wielkości. Na przykład, aby utworzyć plik 1m.dat składający się dokładnie z jednego megabajta znaków możemy użyć polecenia

dd if=/dev/zero of=1m.dat bs=1M count=1

Polecenie dd służy do kopiowania z jednego pliku do drugiego określonej liczby bajtów.

Inne pliki specjalne znajdują się w katalogu /proc. W katalogu tym są najróżniejsze informacje o systemie, o procesach. Podamy tu dla przykładu dwa pliki: /proc/cpuinfo, /proc/meminfo. Pierwszy wyświetla informacje o procesorach znajdujących się w danym komputerze, a drugi wyświetla informacje o dostępnej pamięci operacyjnej.