Środowisko programisty/Wprowadzenie do Basha

From Studia Informatyczne

Spis treści

Czym jest Bash?

Bash (Bourne-Again Shell) jest najpopularniejszą odmianą shell'a. Innymi znanymi są np. ksh (Korn Shell), csh (C Shell).

Shell możemy kojarzyć z linią poleceń, dzięki której możemy wpisywać komendy z klawiatury. Służą one na ogół do uruchamiania innych programów, poleceń lub wyświetlania informacji. Shell służy też do uruchamiania własnoręcznie napisanych skryptów.

Najprostsze skrypty są po prostu ciągiem poleceń, ale możemy też pisać bardziej skomplikowane mechanizmy, które w istocie rzeczy można nazwać wręcz programami. Składnia skryptów nie jest skomplikowana. Oprócz różnych programów użytkowych i systemowych jest zaledwie kilka reguł, których wystarczy się nauczyć, żeby swobodnie pisać skrypty składające poznane programy w celu wykonania zaplanowanych działań.

Shell jest podstawowym narzędziem pod system typu UNIX, ale nie tylko. Pod Windowsem mamy np. interpretator poleceń, a skryptami są np. pliki o rozszerzeniu bat. Niemniej możliwości i funkcjonalność takich skryptów pod Windowsem są znikome (w porównania do Basha pod Unixem) ze względu na ograniczenia składniowe, jak i dostępny zbiór poleceń użytkowych. Aby uzyskać możliwość uruchamiania skryptów napisanych w Bashu pod Windowsem możemy sobie zainstalować pakiet Cygwin.

Do czego się przydaje?

Skrypty pisze się, gdy chce się zautomatyzować lub uprościć jakąś czynność powtarzalną bądź nie. Czynności te, to

  • kompilacja, budowanie aplikacji,
  • przetwarzanie plików (tworzenie, usuwanie, szukanie, itp.),
  • prosta obróbka (np. tekstowa) plików,
  • administracja systemem (np. konfiguracja, uruchamianie demonów),
  • i wiele innych...

Do czego się nie nadaje?

Bash jest interpreterem. Skryptów napisanych w bashu się nie kompiluje. Nie ma też żadnych skomplikowanych struktur danych (jak tablice wielowymiarowe, czy drzewa), ani konstrukcji znanych z języków wyższych poziomów (jak rekordy, klasy). W związku z tym skrypty nie nadają się na przykład do:

  • zadań trudnych obliczeniowo, wymagających szybkiego działania,
  • operacji skomplikowanych algorytmicznie lub matematycznie,
  • operacji niskopoziomowych, jak dostęp do sprzętu.

Zaczynamy pracować

Zapoznanie z Linuxem

Po uruchomieniu systemu Linux powinno pojawić się okno logowania. Należy wpisać swój login i hasło. W przypadku problemów należy poprosić administratora o przydzielenie konta. Po zalogowaniu uruchamia się menedżer okien w środowisku graficznym. W zależności od dystrybucji i konfiguracji może to być jeden z wielu dostępnych menedżerów. Najpopularniejszymi są KDE i GNOME.

Podobnie jak w systemach z rodziny Windows możemy uruchamiać programy z dostępnego menu. Oprócz uruchamiania programów takich jak przeglądarki, czy edytory, nas najbardziej będzie interesować konsola/shell. W KDE jest to program konsole i możemy go wybrać albo z paska, albo z menu. Po uruchomieniu konsoli pojawia nam się okienko z linią komend. Migający kursor zaprasza nas do wpisywania komend. Wpiszmy komendę pwd.

bashtest@host:~$ pwd
/home/bashtest
bashtest@host:~$ 

W wyniku wypisał się nasz katalog domowy /home/bashtest i z powrotem pojawiła się linia z kursorem zachęcającym do wpisania następnej komendy. Katalogi w Linuxie oddziela się znakiem /. Oznacza to, że nasz katalog domowy w stosunku do katalogu głównego / znajduję się w podkatalogu home, a następnie w katalogu bashtest. Komenda pwd wypisuje aktualny katalog, a ponieważ zaraz po uruchomieniu shella zaczynamy w katalogu domowym, więc w tym wypadku zwraca ona nasz katalog domowy.

Konsola z linią komend jest podstawowym narzędziem pracy do wykonywania poleceń basha. Ponadto do tworzenia skryptów, bądź też innych plików tekstowych będziemy używać edytorów. Najprostszymi edytorami, których można używać bezpośrednio z linii poleceń, są mcedit, joe, pico. Można używać też bardziej skomplikowanych edytorów jak emacs, vim, kate. Na przykład, aby edytować plik test.txt wpisujemy komendę:

bashtest@host:~$ mcedit test.txt

Wpisujemy jakiś tekst, wciskamy F10 i potwierdzamy zapis. Aby wyświetlić wprowadzony tekst, piszemy:

bashtest@host:~$ cat test.txt

Przykład możliwości Basha

Shell z linii komend umożliwia nam wykonywanie nawet bardzo skomplikowanych czynności. Spójrzmy na następujący przykład, który jest już bardziej skomplikowany. Załóżmy, że mamy projekt o nazwie ecmnet, którego źródła znajdują sie w katalogu o tej samej nazwie. Chcemy wysłać sobie mailem archiwum ze źródłami tego projektu. Chcemy również, aby w tym archiwum nie znalazły się pliki o rozszerzeniu bak, czy kończące się znakiem ~. Ponadto nazwa archiwum powinna zawierać aktualną datę z dokładnością co do sekundy. Oto jak może wyglądać przykładowa sesja w bashu realizująca te zadanie .

<flash>file=SrodowiskoProgramisty-bash01.swf|width=484|height=316|quality=low|loop=false</flash>

Niektóre komendy mogą być w tym momencie niezrozumiałe, niemniej warto być świadomym możliwości, że tego typu czynności można wykonywać bardzo sprawnie. Skomentujmy po krótce przeznaczenie użytych poleceń, o których więcej szczegółów zostanie podanych w trakcie kursu. Zwróćmy uwagę również na możliwości edycyjne basha. Przy edycji komendy możemy używać klawiszy intuicyjnie tak, jak ma się to w zwykłych edytorach. Jednak w bashu są również typowe dla niego funkcje. Nie będziemy omawiać tutaj szczegółów. Pokażemy tylko parę przykładów, a zainteresowanych odsyłamy do dokumentacji basha.

Omówmy nasz przykład.

bashtest@host:~$ cp -a ecmnet/ /tmp/

cp -a kopiuje rekurencyjnie katalog ecment do katalogu /tmp/ (jest to standardowy katalog, w którym trzyma się pliki tymczasowe). Zamiast pisać pełną nazwę katalogu wpisujemy tylko ec i wciskamy Tab, co powoduję automatyczne rozwinięcie do ecmnet/. Tab służy do rozwijania komend oraz nazw plików.

bashtest@host:~$ cd /tmp/

Zmienia aktualny katalog na /tmp.

bashtest@host:/tmp$ find ecmnet/ -name "*.bak" -o -name "*~"
bashtest@host:/tmp$ find ecmnet/ -name "*.bak" -o -name "*~" -execdir rm -f {} +

find potrafi wyszukiwać pliki rekurencyjnie w rozmaity sposób i nie tylko. W tym przypadku użyliśmy go do znalezienia plików o rozszerzeniu bak lub kończących się znakiem ~. W drugiej linii zmodyfikowaliśmy komendę tak, aby te znalezione pliki zostały od razu usunięte.

Aby wpisać drugą komendę wystarczy wcisnąć strzałkę do góry i dopisać brakujący kawałek. Za pomocą strzałek w górę i w dół możemy przeglądać historię wpisywanych poleceń.

bashtest@host:/tmp$ date +%Y%m%d%H%M%S

date służy do wyświetlania aktualnej daty/godziny lub jej zmieniania. W tym przypadku wyświetlamy datę w odpowiednim formacie.

bashtest@host:/tmp$ tar cvz ecmnet/ | uuencode ecmnet-`date +%Y%m%d%H%M%S`.tgz | mail me@somehost

Tutaj mamy przykład potoku. Komenda tar służy do składania plików w jedno archiwum z kompresją lub bez. To, co wyprodukuje ta komenda, jest przekazywane komendzie uuencode, która służy do kodowania plików binarnych tak, aby mogły być użyte w mediach tekstowych. Wynik wywołania komendy date jest użyty do nazwania pliku archiwum. Ostateczny wynik dostaje komenda mail, która wysyła go na adres me@somehost.

Żeby nie pisać ponownie komendy date wraz z trudnym do wpisania formatem daty, wciskamy strzałkę do góry, aby uzyskać przed chwilą wpisaną komendę date. Następnie przechodzimy na początek komendy (Ctrl-A lub Home) i wciskamy Ctrl-K. Powoduje to usunięcie wszystkich znaków od pozycji kursora do końca linii i umieszczenie ich w buforze. Później, przy edycji, gdy ponownie dochodzimy do momentu wpisania komendy date, wciskamy Ctrl-Y i zostaje umieszczone to, co jest aktualnie w buforze.

bashtest@host:/tmp$ rm -rf ecmnet/

Usuwa rekurencyjnie katalog ecmnet.

bashtest@host:/tmp$ cd

Zmienia katalog z powrotem na domowy katalog użytkownika.

Dokumentacja

W powyższym przykładzie widzimy, że główną siłą była znajomość komend i opcji, z jakimi trzeba ich użyć. Jeśli chodzi o uzyskiwanie informacji na temat opcji danej komendy, to z pomocą przychodzą nam dwa polecenia:

  1. man
  2. info

Na przykład, żeby dowiedzieć się co oznacza magiczne +%Y%m%d%H%M%S przy poleceniu date możemy napisać :

<flash>file=SrodowiskoProgramisty-bash02.swf|width=484|height=316|quality=low|loop=false</flash>

bashtest@host:~$ man date

Dostajemy tekstowy opis komendy date wraz z wszystkimi opcjami, który możemy sobie spokojnie poprzeglądać. Komenda info jest podobna do komendy man z tą różnicą, że daje jeszcze większą wygodę poruszania się i na ogół jest znacznie więcej informacji. W niektórych dystrybucjach linuxa większość informacji o dostępnych komendach użytkowych można uzyskać na przykład poprzez polecenie:

bashtest@host:~$ info coreutils

Dokładną dokumentację Basha uzyskujemy przez:

bashtest@host:~$ info bash

Przydatne jest też wywołanie

bashtest@host:~$ man -k słowo

które wyświetla wszystkie polecenia związane z danym słowem. Więcej o czytaniu dokumentacji:

man man
man info

Gdzie można znaleźć informacje o tym jakie w ogóle są komendy? Wiele z nich wymienionych jest w info coreutils. Innych komend można samemu spróbować poszukać w liście zainstalowanych pakietów danej dystrybucji. Takie szukanie jest jednak czasochłonne. Tak naprawdę lista wszystkich poleceń nie istnieje, gdyż co chwilę powstają nowe programy dające nowe możliwości lub ułatwiające życie.

W tym kursie przedstawimy podstawowe narzędzia, które powinny w praktyce wystarczyć do większości celów. Przy potrzebie bardziej wysublimowanych komend trzeba jednak będzie znaleźć odpowiednie narzędzie, lub też samemu napisać, używając bardziej zaawansowanych języków jak Perl, Python, C, czy Java.

Podstawowe mechanizmy

Przekierowanie wejścia-wyjścia

Każdy program ma trzy podstawowe strumienie wejścia-wyjścia:

  1. standardowe wejście,
  2. standardowe wyjście,
  3. standardowe wyjście diagnostyczne (strumień błędów).

Zazwyczaj zaraz po uruchomieniu strumienie te połączone są z terminalem, co dla wejścia oznacza, że wczytywane jest ono z klawiatury, a dla wyjścia oznacza, że wypisywane jest ono na ekran. Strumienie te możemy jednak przekierowywać do plików za pomocą symboli <, >, lub >>. Na przykład, aby przekierować wynik wywołania komendy ls -R do pliku wynik.txt piszemy:

bashtest@host:~$ ls -R > wynik.txt

Znaczenie tych symboli przedstawione jest w poniższej tabeli.

Symbol Znaczenie
< plik podstawienie pod standardowe wejścia pliku
> plik wypisywanie wyjścia do pliku; jeśli plik istniał wcześniej to jest nadpisywany
>> plik wypisywanie wyjścia do pliku; jeśli plik istniał wcześniej to wyjście jest dopisywane na jego końcu

Aby przekierować standardowe wyjście diagnostyczne używamy notacji 2> lub 2>>. Na przykład

rm "nie ma takiego pliku" 2>plik

Zobaczmy, jak można wykorzystywać przekierowania. Polecenie cat uruchamiane bez argumentów po prostu kopiuje wejście na wyjście. Przekierowując wyjście do pliku, możemy wprowadzić treść tego pliku z klawiatury. Aby zakończyć strumień wejścia wprowadzamy znacznik końca pliku (^D) z klawiatury wciskając Ctrl-D.

bashtest@host:~$ cat >test.txt
To
jest
^D
bashtest@host:~$ cat >>test.txt
test
^D
bashtest@host:~$ cat <test.txt 
To
jest
test
bashtest@host:~$ 

W ostatnim poleceniu taki sam wynik otrzymamy po prostu podając poleceniu cat nazwę pliku jako argument:

bashtest@host:~$ cat test.txt 
To
jest
test
bashtest@host:~$

Potoki

W sytuacji gdy chcemy, aby wyjście jednego programu było zarazem wejściem dla drugiego, używając przekierowań, możemy użyć pliku tymczasowego. Można to zrobić znacznie prościej, używając potoków. Gdy połączymy dwa programy znakiem |, standardowe wyjście pierwszego programu będzie dostarczone w standardowym wejściu drugiego programu.

Aby wyświetlić plik w aktualnym katalogu, który został ostatnio zmodyfikowany, możemy posłużyć się komendą:

bashtest@host:~$ ls -t | head -1
test.txt

ls służy do wyświetlania plików. Argument -t powoduje, że wynik jest sortowany po dacie modyfikacji poczynając od najnowszego. head wyświetla tylko pierwsze linie wejścia, w tym przypadku jest to tylko jedna linia ze względu na opcję -1.

Potoki mogą łączyć więcej niż dwa programy. Na przykład:

bashtest@host:~$ tr j t <test.txt | uniq
To
test
bashtest@host:~$ tr j t <test.txt | uniq | tr '\n' ' '
To test bashtest@host:~$ 

tr x y konwertuje wszystkie znaki x na y. uniq usuwa powtarzające się linie. \n oznacza znak końca linii. W wyniku ostatniej komendy otrzymujemy napis 'To test ' bez znaku końca linii przez co tekst zachęty pojawia się zaraz za nim.

Ciąg poleceń

Polecenia mogą być też uruchamiane jedno po drugim. Do rozdzielenia poleceń służy znak ;.

bashtest@host:~$ echo "test.txt: start"; cat test.txt; echo "test.txt: end"
test.txt: start
To
jest
test
test.txt: end
bashtest@host:~$

Komenda echo służy do wypisywania tekstu podanego w argumencie.

Oczywiście inny sposób na wykonanie kilku poleceń pod rząd to osobne wprowadzanie każdego z nich. Ciągi poleceń są głównie wykorzystywane w linii komend. W skryptach dla czytelności przeważnie każde polecenie piszemy w osobnej linii.

Wzorce nazw plików

W argumentach polecenia, gdy odwołujemy się do plików, możemy używać wzorców do określenia o jakie pliki nam chodzi. Służą do tego znaki:

  • * - kojarzy dowolny ciąg znaków (być może pusty),
  • ? - kojarzy dokładnie jeden dowolny znak.

Jeśli w nazwie pliku pojawia się znak * lub ?, interpreter przegląda aktualny katalog w celu znalezienia wszystkich plików, które odpowiadają danemu wzorcowi. Na przykład

$ echo *.txt

Wyświetli nazwy plików z aktualnego katalogu kończące się na .txt.

$ cat *.tx?

Wypisze zawartość wszystkich plików kończących się na .tx plus dowolny znak.

A oto jeszcze jeden przykład:

bashtest@host:~$ mkdir wzorce_test
bashtest@host:~$ cd wzorce_test/
bashtest@host:~/wzorce_test$ touch a b ab abcd bbdd
bashtest@host:~/wzorce_test$ ls
a  ab  abcd  b  bbdd
bashtest@host:~/wzorce_test$ echo ?
a b
bashtest@host:~/wzorce_test$ echo b*
b bbdd
bashtest@host:~/wzorce_test$ echo *b*
ab abcd b bbdd
bashtest@host:~/wzorce_test$ echo *b?*
abcd bbdd
bashtest@host:~/wzorce_test$ echo ?b?d
abcd bbdd
bashtest@host:~/wzorce_test$ echo a*
a ab abcd
bashtest@host:~/wzorce_test$ echo a?
ab
bashtest@host:~/wzorce_test$

Komenda touch tworzy plik, jeśli plik nie istnieje, w przeciwnym razie (jeśli plik istnieje), ustawia jego datę modyfikacji na aktualną datę systemową.

Znaki specjalne, apostrofy i cudzysłowy

Poznaliśmy niektóre znaki, które są interpretowane w specjalny sposób przez Basha (np. < > ; | * ?). Poznamy ich jeszcze znacznie więcej. Powstaje pytanie, co zrobić, jeśli chcemy użyć jednego z tych znaków w argumentach polecenia, np. w nazwie pliku. Są trzy sposoby na zrobienie tego.

Backslash (\)

Aby uzyskać dany znak poprzedzamy go znakiem \.

bashtest@host:~$ echo \aa\*\?\|\<\>\\
aa*?|<>\
bashtest@host:~$

W ten sposób możemy użyć spacji w nazwie pliku, która normalnie służy do rozdzielania argumentów.

bashtest@host:~$ touch To\ jest\ jeden\ plik
bashtest@host:~$ ls -l *\ *
-rw-r--r-- 1 bashtest users 0 2006-08-05 10:44 To jest jeden plik
bashtest@host:~$ rm To\ jest\ jeden\ plik 
bashtest@host:~$ ls -l *\ *
ls: * *: Nie ma takiego pliku ani katalogu
bashtest@host:~$

Apostrof (')

Wygodniejszym sposobem na wprowadzanie napisów zawierających znaki specjalne jest otoczenie danego ciągu znaków apostrofami.

bashtest@host:~$ echo 'aa*?|<>\'
aa*?|<>\
bashtest@host:~$

Jedyny znak, który nie może się pojawić pomiędzy dwoma apostrofami jest apostrof, gdyż oznaczałby on wcześniejsze skończenie nieinterpretowanego łańcucha. Między dwoma znakami może się nawet pojawić znak końca linii (enter).

bashtest@host:~$ echo 'Pierwsza linia
> Druga linia'
Pierwsza linia
Druga linia
bashtest@host:~$

Cudzysłów (")

Cudzysłów działa analogicznie jak apostrof, z tą różnicą, że pomiędzy cudzysłowami niektóre znaki interpretowane są w specjalny sposób. Tymi znakami są $ ` \. Znaczenie znaków $ ` jeszcze poznamy. Znaki, które są specjalne pomiędzy cudzysłowami możemy wprowadzić używając \. Apostrof ma zwykłe znaczenie pomiędzy cudzysłowami.

bashtest@host:~$ echo "Znaki, które trzeba poprzedzić znakiem \\: '\$' '\`' '\"' '\\'"
Znaki, które trzeba poprzedzić znakiem \: '$' '`' '"' '\'
bashtest@host:~$

Znak \, postawiony przed innymi znakami niż wyżej wymienione, nadaje jemu zwykłe znaczenie.

bashtest@host:~$ echo "\a\b\c"
\a\b\c
bashtest@host:~$

Możemy tworzyć też dłuższe łańcuchy łącząc każdą z powyższych trzech metod.

bashtest@host:~$ echo "To 'słowo', "a\ to\ 'też "słowo"'
To 'słowo', a to też "słowo"
bashtest@host:~$

Zmienne

Bash umożliwia zapamiętywanie łańcuchów znakowych na zmiennej. Identyfikator zmiennej powinien zaczynać się z litery alfabetu angielskiego, a następnie z ciągu składającego się z liter, cyfr i znaku podkreślenia. Zmiennej przypisujemy wartość używając znaku =. Przy czym trzeba pamiętać, aby nie używać odstępów.

Do zmiennej odwołujemy się poprzedzając identyfikator znakiem $.

bashtest@host:~$ zm=wart
bashtest@host:~$ echo $zm
wart
bashtest@host:~$ echo zm
zm
bashtest@host:~$ zm=słowo1 słowo2
bash: słowo2: command not found
bashtest@host:~$ zm="słowo1 słowo2"
bashtest@host:~$ echo $zm
słowo1 słowo2
bashtest@host:~$

Używając zmiennych możemy uprościć przykład z sekcji Ciąg poleceń.

bashtest@host:~$ p=test.txt; echo "$p: start"; cat $p; echo "$p: end"

W ten sposób nazwa pliku podana jest tylko w jednym miejscu i wystarczy wykonać jedną zmianę, aby przykład działał dla innej nazwy pliku. Zauważmy, że możemy także odwoływać się do zmiennej wewnątrz cudzysłowów. Wewnątrz apostrofów nie jest to możliwe.

Alternatywną formą odwoływania się do zmiennej jest ${zmienna}. Jest ona przydatna na przykład wtedy, gdy po wartości zmiennej chcemy dopisać inne znaki, które mogły by wejść w skład nazwy zmiennej.

bashtest@host:~$ zm=but
bashtest@host:~$ echo $zmy

bashtest@host:~$ echo ${zm}y
buty
bashtest@host:~$

Ciapki

Argumenty polecenia możemy tworzyć także poprzez inne polecenia. Do tego celu służą ciapki `...`. Polecenie podane w bloku otoczonym ciapkami jest uruchamiane i wynik tego polecenia (tzn., to co polecenie wypisało na standardowe wyjście) zastępuje dany blok. Po wykonaniu tych podmian, oryginalne polecenie jest interpretowane i uruchamiane. Oto prosty przykład utworzenia komendy whoami przez wywołanie trzy razy polecenia echo z różnymi argumentami.

bashtest@host:~$ `echo who``echo am``echo i`
bashtest
bashtest@host:~$ whoami
bashtest
bashtest@host:~$

Inny przykład.

bashtest@host:~$ cp "`ls *.txt | head -1`" /tmp

Powyższe polecenie przekopiuje pierwszy alfabetycznie plik o rozszerzeniu txt do katalogu /tmp. Trzeba pamiętać o użyciu apostrofów, gdyż może się okazać, że nazwa pliku zawiera spację, a wtedy w wyniku podmiany będzie utworzonych więcej argumentów. Spójrzmy na przykład.

bashtest@host:~$ cp `echo a b` c
cp: cel `c' nie jest katalogiem

Tutaj polecenie cp dostało trzy argumenty a b c, zatem próbuje ono przekopiować wszystkie pliki/katalogi podane w wszystkich argumentach oprócz ostatniego do katalogu podanego w ostatnim argumencie.

bashtest@host:~$ cp "`echo a b`" c
cp: nie można wykonać stat na `a b': Nie ma takiego pliku ani katalogu

Tutaj cp ma dwa argumenty i szuka pliku o nazwie 'a b', który nie istnieje.

Używając ciapek możemy inicjować zmienne wynikiem wykonania polecenia. Na przykład możemy zmiennej przypisać zawartość pliku:

bashtest@host:~$ zm=`cat test.txt`

Bash udostępnia też alternatywną formę wstawiania wyniku wywołania polecenia $( ... ). Czyli zamiast pisać

`polecenie`

możemy też napisać

$(polecenie)

Ta forma jest o tyle wygodniejsza od ciapek, że umożliwia w prosty sposób zagnieżdżanie, na przykład

bashtest@host:~$ zm=$(cat $(echo test).txt)