Programowanie w Bash-u
Table of Contents
Skrypty powłoki
- Programowanie proceduralne. Proste typy danych, wszystko to ciąg znaków. Brak możliwości definiowania własnych typów/struktur/obiektów.
- Brak rozbudowanej biblioteki standardowej. Część funkcjonalności dostarczana przez funkcje wbudowane, część przez specjalne mechanizmy języka, część przez zewnętrzne programy.
- Bardzo niespójny język/środowisko programistyczne. Ubogość samego języka i funkcji wbudowanych historycznie spowodowała powstanie wielu niezależnych mechanizmów i narzędzi projektowanych i rozwijanych bez spójnej koncepcji. Wiele ułomnych rozwiązań, które trudno uogólnić lub zastosować do innych sytuacji.
Argumenty pozycyjne
Używając konstrukcji "$@" dostajemy listę argumentów podanych z linii poleceń.
Przy pomocy pętli for możemy przypisywać każdy z nich po kolei do zmiennej.
#!/bin/bash
echo "Podano $# argumentów."
i=1
for arg in "$@"; do
echo "Argument $i: $arg"
i=$((i+1))
doneAle uwaga, co wyświetlą poniższe skrypty?
#!/bin/bash
echo 'Przekazujemy argumenty używając $@'
./argumenty.sh $@
echo 'Przekazujemy argumenty używając $*'
./argumenty.sh $*
echo 'Przekazujemy argumenty używając "$*"'
./argumenty.sh "$*"A jaki wpływ na działanie ma odwołanie do argumentów już w pętli?
#!/bin/bash
echo "Podano $# argumentów."
i=1
for arg in "$@"; do
echo -e "\nWywołanie argumenty.sh dla $i argumentu: $arg"
./argumenty.sh $arg
i=$((i+1))
donePrzetwarzanie linii poleceń lub linii skryptu przed wykonaniem
Każda linia (lub polecenie wieloliniowe) w Bash-u jest przetwarzana, zanim zostanie wykonana.
Przy pracy interaktywnej, na początku rozwijane są odwołania do historii poleceń (np. !!), potem rozwijane są aliasy.
Domyślnie te dwa mechanizmy są wyłączone przy wykonywaniu skryptów.
Następnie stosowane są poniższe przekształcenia:
- Rozwijanie nawiasów klamrowych (brace expansion).
Przykładowo:ls plik{1,2,3}.txt→ls plik1.txt plik2.txt plik3.txt. - Rozwijanie tyld (tilde expansion).
Przykładowo:~gkowzan→/home/gkowzan. - Podstawianie wartości zmiennych i parametrów pozycyjnych (variable and parameter expansion).
Przykładowo:$HOME→/home/gkowzan. - Podstawianie wartości wyrażeń numerycznych (arithmetic expansion).
Przykładowo:$((12*3))→36. - Podstawianie wyjścia komend (command substitution).
Przykładowo:touch $(date +%Y-%m-%d_%H_%M_%S).txt→touch 2026-02-01_21_02_08.txt. - Dzielenie wyrazów (word splitting).
Omawiane powyżej przy okazji argumentów pozycyjnych. Tekst linii poleceń jest dzielony na argumenty. Każdy znak zmiennejIFSto separator. Domyślnie są to białe znaki (spacja, tabulacja, znak nowej linii). Patrz wyniki poleceń:
printf '%s' "$IFS" | od -An -t a -v
lub
printf '%s' "$IFS" | od -An -t u1 -v. - Rozwijanie wzorców ścieżek do plików (pathname expansion).
Argumenty zawierające znaki*,?,[,]są traktowane jako wzorce ścieżek do plików i rozwijane, jeśli pasują do jakiś plików. - Podmiana procesów (process substitution).
Bash może automatyczne podpiąć standardowe wejście lub wyjście komendy pod nazywany potok i podstawić ścieżkę do tego potoku jako argument polecenia. - Usuwanie znaków cytowania (quote removal).
Ostatecznie usuwane są znaki cytowania,',", które były użyte do zapobieżenia stosowania niektórych z powyższych mechanizmów.
Możemy nakazać bash-owi wyświetlanie linii poleceń po zastosowaniu wszystkich tych mechanizmów wywołując funkcję wbudowaną set -x.
(Wyłączamy wyświetlanie rozwiniętych linii komendą set +x.)
Podobnym mechanizmem przetwarzania kodu źródłowego przed jego wykonaniem są makra preprocesora w języku C.
Instrukcje warunkowe
Przetwarzanie tekstu przy podstawianiu wartości zmiennych
Tekst podstawianej zmiennej może być przetworzony przed podstawieniem używając specjalnej składni. Patrz:
- https://devhints.io/bash#parameter-expansions
- https://web.archive.org/web/20230408142504/https://wiki.bash-hackers.org/syntax/pe
Pętle for
Iteracja po liście argumentów:
echo " 1"
for arg in 1 2 3 4 5 6; do
echo "$arg"
done
echo " 2"
for arg in 1 "2 3" 4 5 6; do
echo "$arg"
done
echo " 3"
# lista argumentów jest przetwarzana przed wykonaniem
for arg in {1..6}; do
echo "$arg"
done
echo " 4"
args="1 2 3 4 5 6"
for arg in $args; do
echo $arg
done
echo " 5"
# to samo co for arg in "$@"; do
for arg; do
echo "$arg"
doneIteracja po plikach:
# iteracja po plikach bieżącym katalogu
# używamy pathname expansion
for f in *; do
echo "$f"
doneCo jeśli katalog jest pusty?
Co z plikami zaczynającymi się od kropki?
Niektóre aspekty działania powłoki możemy kontrolować poleceniem shopt.
Patrz w szczególności opcje z “glob” w nazwie.
Iteracja przez konstrukcję w trybie arytmetycznym. Pierwsze polecenie jest wykonywane przed rozpoczęciem pętli, wartość logiczna drugiego jest sprawdzana przed każdą iteracją, ostatnie jest wykonywane po każdej iteracji.
for (( i=1; i<=6; i++ )); do
echo $i
donePętla while
Wykonuj kod tak długo jako warunek jest spełniony. Warunek jest sprawdzany przed każdym wejściem do pętli.
echo " 1"
i=1
while ((i<=10)); do
echo $i
((i++))
done
echo " 2"
i=1
while [[ $i -le 10 ]]; do
echo $i
i=$((i+1))
done
echo " 3"
i=1
while true; do
if ((i>10)); then
# wyjdź z pętli
break
fi
echo $i
((i++))
done
echo " 4"
i=1
while ((i<=10)); do
if ((i<=5)); then
((i++))
# od razu przejdź do następnej iteracji
# nie wykonuj poleceń między "fi" a "done"
continue
fi
echo $i
((i++))
doneZwróć uwagę, że $((...)) to nie to samo co ((...)).
Jak iterować po pliku, strumieniu, potoku?
Używamy funkcji wbudowanej read, która wczytuje zawartość linia po linia, dzieli linię na słowa i przypisuje je do podanych zmiennych.
read zwraca wartość nie-zerową (fałsz logiczny), gdy strumień się kończy.
while read -r line; do
echo "${line^^}"
doneCo jeśli w pętli też wczytamy dane ze strumienia?
while read -r line; do
echo "czytanie while: ${line}"
read -r line
echo "czytanie w pętli: ${line}"
doneUżywamy tego samego strumienia. Zachłanny proces w pętli może nam popsuć iterację.
while read -r line; do
echo "czytanie while: ${line}"
cat > pusty
donecat podpina się pod standardowe wejście.
Co jeśli zduplikujemy standardowe wejście?
exec 8<&0
exec 0< /dev/null
while read -u 8 -r line; do
echo "czytanie while: ${line}"
cat > pusty
done
exec 0<&8
exec 8<&-Tablice
Co zrobić jeśli chcemy zapisać wszystkie podane argumenty pozycyjne do zmiennej na później?
for arg in "$@"; do
echo "$arg"
done
echo
echo "zapisz argumenty do zmiennej i iteruj po nich później"
argumenty="$@"
for arg in "$argumenty"; do
echo "$arg"
doneTablice:
tablica=() # pusta tablica
tablica=(pierwszy "drugi i trzeci" czwarty)
echo $tablica
echo $tablica[0] # ups
echo ${tablica[0]}
echo ${tablica[1]}
echo "${tablica[@]}" # ta sama różnica co z pozycyjnymi
echo "${tablica[*]}"
echo "${#tablica}"
echo "${#tablica}"
echo "${#tablica[@]}"
echo "${#tablica[*]}"
tablica[70]=siedemdziesiąt
tablica[warzywo]=pomidor
echo "${!tablica[*]}" # indeksy
declare -p tablicaSłowniki:
declare -A tablica
tablica=(["warzywo"]="pomidor" ["owoc"]="czereśnia")
echo "${tablica[warzywo]}"Pozostała obsługa analogicznie do zwykłych tablic.
Grupowanie operacji
Podpowłoki - ( .. )
Polecenia złożone - { .. }
Funkcje - func() { .. }
- zmienne lokalne
Ćwiczenia
Warunki
Napisz skrypt, który przyjmuje jeden argument z linii poleceń, traktuje go jako ścieżkę do pliku i sprawdza czy podany plik jest zwykłym plikiem, katalogiem lub innego rodzaju plikiem. Dla każdego z tych wariantów wyświetla stosowny komunikat.
- Dla zwykłego pliku sprawdzać dodatkowo czy plik jest możliwy do odczytu, zapisu lub wykonania.
- Sprawdzić czy skrypt otrzymał wymaganą ilość argumentów, jeśli nie to zakończyć działanie (poleceniem
exit 1). Jeśli otrzymano argument, to sprawdzić czy ścieżka prowadzi do jakiegokolwiek pliku.
Dopasowanie do wzorca
Napisz skrypt, który przyjmuje jeden argument z linii poleceń, traktuje go jako ścieżkę do pliku.
Sprawdź czy prawa dostępu podane pliku pozwalają na odczyt przez innych użytkowników (tzn. klasie o przy zapisie symbolicznym uprawnień).
Użyj polecenia stat z odpowiednim formatem wyjściowych, aby wygenerować ciąg znaków maksymalnie ułatwiający to zadanie.
Aby dopasować się do otrzymanego ciągu znaków:
- użyj testu wbudowanego w Bash, tj.
[[ $STRING =~ $PATTERN ]]. - użyj grep z cichą opcją.
Kopiuj pod nowym rozszerzeniem
Napisz skrypt który bierze dwa argumenty z linii poleceń:
- ścieżkę do pliku,
- rozszerzenie,
i kopiuje plik do bieżącego katalogu z nowym rozszerzeniem. Stare rozszerzenie jest usuwane.
Rozpakuj
Napisz skrypt unpack.sh, który sprawdza czy podany plik to archiwum zip, tar.gz lub tar.bz2 i rozpakowuje odpowiednim poleceniem lub pisze, że nie potrafi obsłużyć archiwum.
Archiwizuj
Napisz skrypt, który archiwizuje w formacie tar i kompresuje w formacie gzip (plik .tar.gz) wszystkie pliki w katalogu domowym użytkownika, które zostały zmodyfikowane w ciągu ostatnich 24 godzin. Nazwa pliku końcowego to home-backup-<rok>-<miesiąc>-<dzień>_<godzina>-<minuta>.tar.gz, przykładowo home-backup-2025-11-21_12-57.tar.gz.
-
Wariant
findUżyj polecenia
findz odpowiednim warunkiem, aby znaleźć wszystkie pliki i sukcesywnie dodawać je do archiwum.
-
Wariant
bashUżyj możliwości rekurencyjnej iteracji po strukturze katalogów wbudowanej w bash-a, którą należy włączyć poleceniem
shopt, oraz testów basha do znalezienia odpowiednich plików.
Obserwuj
Napisz skrypt, który przyjmuje jako pierwszy argument ID procesu, jako drugi argument odstęp czasu. Skrypt sprawdza co zadany odstęp czasu czy proces jest uruchomiony. Gdy wykryje, że proces nie jest uruchomiony, to wypisuje informację o tym i kończy działanie.
Kalkulator
Napisz skrypt pytający użytkownika jaką operację chce wykonać (dodawanie, odejmowanie, mnożenie lub dzielenie), proszący o argumenty i podający wynik działania.
Po wyświetleniu wyniku, pytaj ponownie aż użytkownika nie poda ciągu znaków exit.
Podpowiedź: Użyj read z opcją -p.
Parsowanie plików i słowniki
Użyj read z odpowiednią wartością zmiennej środowiskowej IFS, aby odczytać zawartość pliku /etc/passwd podzielonego na pola.
W słowniku login_shells zapisz jako pary klucz-wartość nazwy użytkowników i ich powłoki logowania.
Usuwanie komentarzy
Komentarze w skryptach powłoki to wszystkie linie zaczynające się od dowolnej ilości białych znaków, znaku #, a następnie dowolnych innych znaków.
Wyjątkiem jest pierwsza linia skryptu, która może się zaczynać od #!.
Napisz skrypt, którego pierwszym argumentem jest plik ze skryptem w bashu, drugi argument to plik wynikowy.
Skrypt kopiuje zawartość pierwszego pliku do drugiego, ale usuwa linie z komentarzami.
Użyj pętli while i funkcji read.
Najdłuższa linia
Napisz skrypt znajdujący najdłuższą linie w podanym pliku tekstowym.
Materiały
- Mark G. Sobell, A Practical Guide to Linux Commands, Editors, and Shell Programming, Wydanie 3, Pearson, 2012. - spójne, poprawne, przystępne wytłumaczenie zawiłości użytkowania powłoki i narzędzi uniksowych,
- Bash Guide - Greg’s Wiki - jak uniknąć typowych błędów,
- Bash scripting cheatsheet - ściąga najważniejszych mechanizmów,
- Advanced Bash-Scripting Guide - dużo przykładów, niekoniecznie niezawodnie napisanych,
- ShellCheck - sprawdza błędy i styl.