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))
done

Ale 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))
done

Przetwarzanie 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:

  1. Rozwijanie nawiasów klamrowych (brace expansion).
    Przykładowo: ls plik{1,2,3}.txtls plik1.txt plik2.txt plik3.txt.
  2. Rozwijanie tyld (tilde expansion).
    Przykładowo: ~gkowzan/home/gkowzan.
  3. Podstawianie wartości zmiennych i parametrów pozycyjnych (variable and parameter expansion).
    Przykładowo: $HOME/home/gkowzan.
  4. Podstawianie wartości wyrażeń numerycznych (arithmetic expansion).
    Przykładowo: $((12*3))36.
  5. Podstawianie wyjścia komend (command substitution).
    Przykładowo: touch $(date +%Y-%m-%d_%H_%M_%S).txttouch 2026-02-01_21_02_08.txt.
  6. Dzielenie wyrazów (word splitting).
    Omawiane powyżej przy okazji argumentów pozycyjnych. Tekst linii poleceń jest dzielony na argumenty. Każdy znak zmiennej IFS to 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.
  7. 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.
  8. 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.
  9. 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:

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"
done

Iteracja po plikach:

# iteracja po plikach bieżącym katalogu
# używamy pathname expansion
for f in *; do
    echo "$f"
done

Co 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
done

Pę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++))
done

Zwróć 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^^}"
done

Co 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}"
done

Uż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
done

cat 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"
done

Tablice:

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 tablica

Sł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ń:

  1. ścieżkę do pliku,
  2. 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 find

    Użyj polecenia find z odpowiednim warunkiem, aby znaleźć wszystkie pliki i sukcesywnie dodawać je do archiwum.

  • Wariant bash

    Uż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.