3.3 Lekcja 2
Certyfikat: |
Linux Essentials |
---|---|
Wersja: |
1.6 |
Temat: |
3 Siła wiersza poleceń |
Cel nauki: |
3.3 Przekształcanie poleceń w skrypt |
Lekcja: |
2 z 2 |
Wstęp
W ostatniej sekcji użyliśmy tego prostego przykładu, aby zademonstrować działanie skryptów Bash:
#!/bin/bash # A simple script to greet a single user. if [ $# -eq 1 ] then username=$1 echo "Hello $username!" else echo "Please enter only one argument." fi echo "Number of arguments: $#."
-
Wszystkie skrypty powinny zaczynać się od shebang, który określa ścieżkę do interpretera.
-
Wszystkie skrypty powinny zawierać komentarze opisujące ich użycie.
-
Ten konkretny skrypt działa z argumentem, który jest przekazywany do skryptu, gdy jest wywoływany.
-
Skrypt ten zawiera instrukcję if, która sprawdza warunki wbudowanej zmiennej
$#
. Ta zmienna jest ustawiana na liczbę argumentów. -
Jeśli liczba argumentów przekazanych do skryptu wynosi 1, to wartość pierwszego argumentu jest przekazywana do nowej zmiennej o nazwie
username
, a skrypt wysyła powitanie do użytkownika. W przeciwnym razie zostanie wyświetlony komunikat o błędzie. -
Na koniec skrypt wyświetla liczbę argumentów. Jest to przydatne do rozwiązywania problemów (debugowania).
To jest przydatny przykład wyjaśniający niektóre cechy skryptów Bash.
Kody zakończenia (Exit Codes)
Zauważysz, że nasz skrypt ma dwa możliwe stany: albo wypisze "Hello <user>!"
, albo wyświetli komunikat o błędzie. Jest to całkiem normalne w przypadku wielu naszych podstawowych narzędzi. Pomyśl o poleceniu cat
, z którym bez wątpienia dobrze się zapoznasz.
Porównajmy pomyślne użycie argumentu cat
z sytuacją, w której to się nie powiedzie. Przypomnij sobie, że powyższy przykład to skrypt o nazwie new_script.sh
.
$ cat -n new_script.sh 1 #!/bin/bash 2 3 # A simple script to greet a single user. 4 5 if [ $# -eq 1 ] 6 then 7 username=$1 8 9 echo "Hello $username!" 10 else 11 echo "Please enter only one argument." 12 fi 13 echo "Number of arguments: $#."
To polecenie się powiedzie i zauważysz, że flaga -n
wypisuje również numery wierszy, Numery te są bardzo pomocne podczas debugowania skryptów i szukania w nich błędów, ale należy pamiętać, że nie są one częścią skryptu.
Teraz sprawdzimy wartość nowej wbudowanej zmiennej $?
. Na razie zwróć uwagę na wynik:
$ echo $? 0
Rozważmy teraz sytuację, w której polecenie cat
zawiedzie. Najpierw otrzymujemy komunikat o błędzie, a następnie sprawdzimy wartość $?
.
$ cat -n dummyfile.sh cat: dummyfile.sh: No such file or directory $ echo $? 1
Wyjaśnienie tego zachowania jest następujące: każde wykonanie narzędzia cat
zwróci exit code (kod zakończenia). Exit code informuje nas, czy polecenie się powiodło, czy też wystąpił błąd. Exit code zero oznacza, że polecenie zostało wykonane pomyślnie, co jest prawdą dla prawie każdego polecenia Linuxa, z którym będziesz pracować. Każdy inny exit code będzie wskazywał na jakiś błąd. Kod zakończenia ostatniego polecenia do uruchomienia jest przechowywany w zmiennej $?
.
Exit codes zwykle nie są widoczne dla użytkowników, ale są bardzo przydatne podczas pisania skryptów. Wyobraź sobie skrypt, w którym kopiujemy pliki na zdalny dysk sieciowy. Istnieje wiele przyczyn, dlaczego zadanie kopiowania mogło się nie powieść. Na przykład nasz komputer lokalny może nie być połączony do sieci lub dysk zdalny może być pełny. Sprawdzając exit codes naszego programu kopiującego, możemy ostrzec użytkownika o wszelkich problemach podczas uruchamiania skryptu.
Bardzo dobrą praktyką jest wdrażanie kodów zakończenia, zatem zrobimy to teraz. W naszym skrypcie mamy dwie ścieżki: sukces i porażkę. Użyjmy “zera” do wskazania sukcesu, a “jeden” do wskazania niepowodzenia.
1 #!/bin/bash 2 3 # A simple script to greet a single user. 4 5 if [ $# -eq 1 ] 6 then 7 username=$1 8 9 echo "Hello $username!" 10 exit 0 11 else 12 echo "Please enter only one argument." 13 exit 1 14 fi 15 echo "Number of arguments: $#."
$ ./new_script.sh Carol Hello Carol! $ echo $? 0
Zauważ, że polecenie echo
w linii 15. zostało całkowicie zignorowane. Stało sie tak dlatego, ponieważ użycie exit
natychmiast kończy działanie skryptu, więc ta linia nigdy nie zostanie napotkana.
Obsługa wielu argumentów
Jak dotąd nasz skrypt może przetwarzać tylko jedną nazwę użytkownika na raz, a dowolna liczba argumentów, z wyjątkiem jednego, spowoduje błąd. Zobaczmy, jak możemy uczynić ten skrypt bardziej wszechstronnym.
Pierwszym odruchem użytkownika może być użycie większej liczby zmiennych pozycyjnych, takich jak $2
, $3
i tak dalej. Niestety nie możemy przewidzieć liczby argumentów, których będzie potrzebował użytkownik. Aby rozwiązać ten problem, pomocne będzie wprowadzenie większej liczby zmiennych wbudowanych.
Zmodyfikujmy logikę naszego skryptu. Zerowa liczba argumentów powinna spowodować błąd, lecz każda inna liczba argumentów powinna zakończyć się sukcesem. Nowy skrypt nazwijmy friendly2.sh
.
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 echo "Hello $@!" 11 exit 0 12 fi
$ ./friendly2.sh Carol Dave Henry Hello Carol Dave Henry!
Istnieją dwie wbudowane zmienne, które zawierają wszystkie argumenty przekazane do skryptu: $@
oraz $*
. W większości przypadków oba argumenty zachowują się tak samo. Bash przeanalizuje (parse) argumenty i oddzieli każdy argument, gdy napotka spację między nimi. W efekcie zawartość $@
wygląda następująco:
|
|
|
|
|
|
Jeśli znasz inne języki programowania, możesz rozpoznać ten typ zmiennej jako tablicę (array). Tablice w Bash można łatwo tworzyć, po prostu umieszczając spacje między elementami, takimi jak zmienna FILES
w skrypcie arraytest
poniżej:
FILES="/usr/sbin/accept /usr/sbin/pwck/ usr/sbin/chroot"
Zawarta jest tutaj lista kilku pozycji. Jak dotąd nie jest to zbyt pomocne, ponieważ nie wprowadziliśmy jeszcze żadnego sposobu na indywidualne traktowanie tych elementów.
Pętle FOR
Wróćmy do przedstawionego wcześniej przykładu arraytest
. Jeśli pamiętasz, w tym przykładzie tworzymy własną tablicę o nazwie FILES
. To, czego potrzebujemy, to sposóbu “rozpakowania” tej zmiennej i uzyskania dostępu do poszczególnych wartości jedna po drugiej. Aby to zrobić, użyjemy struktury zwanej pętlą for, która jest dostępna we wszystkich językach programowania. Mamy dostęp do dwóch zmiennych, do których będziemy się odwoływać: jedna to zakres, a druga to indywidualna wartość, nad którą aktualnie pracujemy. Oto cały skrypt:
#!/bin/bash FILES="/usr/sbin/accept /usr/sbin/pwck/ usr/sbin/chroot" for file in $FILES do ls -lh $file done
$ ./arraytest lrwxrwxrwx 1 root root 10 Apr 24 11:02 /usr/sbin/accept -> cupsaccept -rwxr-xr-x 1 root root 54K Mar 22 14:32 /usr/sbin/pwck -rwxr-xr-x 1 root root 43K Jan 14 07:17 /usr/sbin/chroot
W powyższym przykładzie w skrypcie friendly2.sh
widać, że pracujemy z zakresem wartości zawartych w jednej zmiennej $@
. Ze względu na przejrzystość, tę drugą zmienną nazwiemy username
. Nasz skrypt wygląda teraz następująco:
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 for username in $@ 11 do 12 echo "Hello $username!" 13 done 14 exit 0 15 fi
Pamiętaj, że zmienna, którą tutaj zdefiniujesz, może mieć dowolną nazwę i że wszystkie wiersze w strukturze do… done
będą wykonywane raz dla każdego elementu tablicy. Przyjrzyjmy się wynikom naszego skryptu:
$ ./friendly2.sh Carol Dave Henry Hello Carol! Hello Dave! Hello Henry!
Załóżmy teraz, że chcemy, aby nasze wyniki wydawały się nieco bardziej “ludzkie” i dlatego umieszczamy nasze powitanie w jednej linii.
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 echo -n "Hello $1" 11 shift 12 for username in $@ 13 do 14 echo -n ", and $username" 15 done 16 echo "!" 17 exit 0 18 fi
Kilka uwag:
-
Użycie
-n
z poleceniemecho
spowoduje zablokowanie nowej linii i pominie podział wiersza po wyświetleniu. Oznacza to, że wszystkie “echa” będą wyświetlone w tej samej linii, a nowa linia zostanie wyświetlona dopiero po znaku!
w linii numer 16. -
Polecenie
shift
usunie pierwszy element naszej tablicy, dzięki czemu:
|
|
|
|
|
|
Zostanie przekształcone do:
|
|
|
|
Spójrzmy na wynik:
$ ./friendly2.sh Carol Hello Carol! $ ./friendly2.sh Carol Dave Henry Hello Carol, and Dave, and Henry!
Używanie wyrażeń regularnych do sprawdzania błędów
Powiedzmy, że chcemy zweryfikować wszystkie argumenty, które wprowadza użytkownik. Na przykład, chcemy się upewnić, że wszystkie nazwy przekazywane do skryptu friendly2.sh
zawierają tylko litery, a wszelkie znaki specjalne lub cyfry spowodują błąd. Do takiego sprawdzania błędów użyjemy polecenia grep
.
Przypomnij sobie, że z poleceniem grep
możemy używać wyrażeń regularnych.
$ echo Animal | grep "^[A-Za-z]*$" Animal $ echo $? 0
$ echo 4n1ml | grep "^[A-Za-z]*$" $ echo $? 1
Znaki ^
oraz $
oznaczają odpowiednio początek i koniec wiersza. Zakres [A-Za-z]
oznacza zakres liter, zarówno dużych jak i małych. Znak *
to tzw. quantifier i modyfikuje nasz zakres liter od zera do (dowolnych) wielu liter. Podsumowując, nasz grep
powiedzie się, jeśli dane wejściowe będą zawierały tylko litery. W przeciwnym przypadku grep
się nie powiedzie.
Kolejną rzeczą wartą zapamiętania jest to, że polecenie grep
zwraca exit codes w zależności od tego, czy było dopasowanie, czy nie. Pozytywne dopasowanie zwraca 0
, a brak dopasowania zwraca 1
. Właściwości tej możemy użyć do przetestowania argumentów w naszym skrypcie.
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 for username in $@ 11 do 12 echo $username | grep "^[A-Za-z]*$" > /dev/null 13 if [ $? -eq 1 ] 14 then 15 echo "ERROR: Names must only contains letters." 16 exit 2 17 else 18 echo "Hello $username!" 19 fi 20 done 21 exit 0 22 fi
W linii 12. przekierowujemy standardowe wyjście do /dev/null
, co jest łatwym sposobem na jego zablokowanie, ponieważ nie chcemy wyświetlać żadnych danych wyjściowych polecenia grep
. Chcemy po prostu przetestować jego exit code i zobaczyć, co się dzieje w linii 13. Zauważmy również, że używamy exit code 2
do wyświetlania nieprawidłowego argumentu. Zasadniczo dobrą praktyką jest używanie różnych exit codes w celu wskazania różnych błędów. W ten sposób doświadczony użytkownik może wykorzystać te exit code do rozwiązywania problemów.
$ ./friendly2.sh Carol Dave Henry Hello Carol! Hello Dave! Hello Henry! $ ./friendly2.sh 42 Carol Dave Henry ERROR: Names must only contains letters. $ echo $? 2
Ćwiczenia z przewodnikiem
-
Przeczytaj poniższy plik
script1.sh
:#!/bin/bash if [ $# -lt 1 ] then echo "This script requires at least 1 argument." exit 1 fi echo $1 | grep "^[A-Z]*$" > /dev/null if [ $? -ne 0 ] then echo "no cake for you!" exit 2 fi echo "here's your cake!" exit 0
Jaki jest wynik następujących poleceń?
-
./script1.sh
-
echo $?
-
./script1.sh cake
-
echo $?
-
./script1.sh CAKE
-
echo $?
-
-
Przeczytaj poniższy skrypt
script2.sh
:for filename in $1/*.txt do cp $filename $filename.bak done
Opisz cel tego skryptu, tak jak go rozumiesz.
Ćwiczenia eksploracyjne
-
Utwórz skrypt, który pobierze dowolną liczbę argumentów od użytkownika i wypisze tylko te argumenty, które są liczbami większymi od 10.
Podsumowanie
W tej sekcji nauczyłeś się:
-
Jakie są exit codes, co oznaczają i jak je wdrożyć
-
Jak sprawdzić exit codes polecenia
-
Czym są pętle
for
i jak ich używać z tablicami -
Jak używać polecenia
grep
, wyrażeń regularnych i exit codes do sprawdzania danych wprowadzanych przez użytkownika w skryptach.
Komendy wykorzystywane w ćwiczeniach:
shift
-
Usuwa pierwszy element tablicy.
Zmienne specjalne:
$?
-
Zawiera exit code ostatniego wykonanego polecenia.
$@
,$*
-
Zawierają wszystkie argumenty przekazane do skryptu w postaci tablicy.
Odpowiedzi do ćwiczeń z przewodnikiem
-
Przeczytaj poniższy plik
script1.sh
:#!/bin/bash if [ $# -lt 1 ] then echo "This script requires at least 1 argument." exit 1 fi echo $1 | grep "^[A-Z]*$" > /dev/null if [ $? -ne 0 ] then echo "no cake for you!" exit 2 fi echo "here's your cake!" exit 0
Jaki jest wynik następujących poleceń?
-
Polecenie:
./script1.sh
Wynik:
This script requires at least 1 argument.
-
Polecenie:
echo $?
Wynik:
1
-
Polecenie:
./script1.sh cake
Wynik:
no cake for you!
-
Polecenie:
echo $?
Wynik:
2
-
Polecenie:
./script1.sh CAKE
Wynik:
here’s your cake!
-
Polecenie:
echo $?
Wynik:
0
-
-
Przeczytaj poniższy skrypt
script2.sh
:for filename in $1/*.txt do cp $filename $filename.bak done
Opisz cel tego skryptu, tak jak go rozumiesz.
Ten skrypt tworzy kopie zapasowe wszystkich plików kończących się na
.txt
w podkatalogu zdefiniowanym w pierwszym argumencie.
Odpowiedzi do ćwiczeń eksploracyjnych
-
Utwórz skrypt, który pobierze dowolną liczbę argumentów od użytkownika i wypisze tylko te argumenty, które są liczbami większymi od 10.
#!/bin/bash for i in $@ do echo $i | grep "^[0-9]*$" > /dev/null if [ $? -eq 0 ] then if [ $i -gt 10 ] then echo -n "$i " fi fi done echo ""