Tech Handbook Null Yard

Programowanie w shellu

Powiązane tematy: Debian - shell, FreeBSD - shell, Shelle w Debianie i Shelle w FreeBSD.

Shell to nie tylko miejsce do wpisywania poleceń. Jest też językiem programowania, w którym można pisać małe narzędzia administracyjne, automatyzować powtarzalne czynności, pytać użytkownika o dane, reagować na jego wybory, sprawdzać wyniki poleceń i wyświetlać status operacji.

Ten materiał skupia się na skryptach uruchamianych w terminalu na Debianie i FreeBSD.


1. Jaki shell wybrać do skryptów?

Najważniejsze rozróżnienie:

  • sh - podstawowy, przenośny shell zgodny mniej więcej ze standardem POSIX,
  • bash - rozbudowany shell popularny w Linuksie,
  • zsh - rozbudowany shell używany często interaktywnie,
  • csh / tcsh - spotykany szczególnie w świecie BSD, ale obecnie raczej nie jest najlepszym wyborem do nowych skryptów.

Dla prostych skryptów administracyjnych najlepszym punktem startowym jest:

#!/bin/sh

Taki skrypt będzie zwykle działał zarówno na Debianie, jak i FreeBSD.

W Debianie:

/bin/sh -> dash

Najczęściej nie jest to Bash.

Na FreeBSD /bin/sh jest systemowym shellem FreeBSD.

Jeżeli używasz funkcji charakterystycznych dla Basha, używaj:

#!/usr/bin/env bash

To pozwala znaleźć bash przez PATH.

Na Debianie Bash jest zwykle dostępny domyślnie.

Na FreeBSD może być konieczna instalacja:

pkg install bash

Dla większości małych narzędzi:

zacznij od sh; przechodź na Bash dopiero wtedy, gdy rzeczywiście potrzebujesz funkcji Basha.


2. Pierwszy skrypt

Tworzymy plik:

vim hello

Zawartość:

#!/bin/sh

echo "Cześć!"
echo "To jest mój pierwszy skrypt."

Pierwsza linia:

#!/bin/sh

to shebang.

Informuje system, jakim interpreterem ma zostać uruchomiony plik.

Nadajemy prawa wykonywania:

chmod +x hello

Uruchomienie:

./hello

Dlaczego trzeba pisać ./?

Ponieważ bieżący katalog zwykle nie znajduje się w zmiennej PATH.


3. echo i printf - wypisywanie informacji

Najprostsze:

echo "Uruchamiam program..."

Kilka komunikatów:

echo "Sprawdzam konfigurację..."
echo "Łączę z serwerem..."
echo "Gotowe."

Do prostych komunikatów echo jest wystarczające.

Bardziej przewidywalny jest jednak:

printf '%s\n' "Uruchamiam program..."

Można też formatować dane:

NAME="user"
printf 'Użytkownik: %s\n' "$NAME"

Przykład:

COUNT=5
printf 'Znaleziono %s plików.\n' "$COUNT"

4. Zmienne

Przypisanie:

NAME="user"

Bardzo ważne:

NAME="user"

jest poprawne.

To:

NAME = "user"

jest błędne.

W shellu nie daje się spacji wokół = przy przypisaniu.

Odczyt zmiennej:

echo "$NAME"

Najlepiej prawie zawsze pisać:

"$NAME"

zamiast:

$NAME

Cudzysłowy chronią przed problemami ze spacjami i znakami specjalnymi.

Przykład:

FILE="/home/user/Moje Pliki/test.txt"

cat "$FILE"

5. Zmienne środowiskowe

Shell posiada wiele gotowych zmiennych.

Przykłady:

echo "$HOME"
echo "$USER"
echo "$PATH"
echo "$SHELL"

HOME:

/home/user

PATH zawiera katalogi, w których shell szuka programów.

Przykładowo:

/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin

Jeśli wpiszesz:

vim

shell przeszukuje po kolei katalogi zapisane w PATH.


6. Pobieranie danych od użytkownika

Do pobierania danych służy read.

Przykład:

printf 'Jak masz na imię? '
read NAME

echo "Cześć, $NAME."

Można też zrobić:

echo "Podaj nazwę pliku:"
read FILE

echo "Wybrałeś: $FILE"

Bezpieczniej:

IFS= read -r FILE

-r powoduje, że backslash \ nie jest traktowany specjalnie.


7. Pytania typu tak / nie

Przykład:

printf 'Kontynuować? [t/N] '
read ANSWER

Następnie sprawdzamy odpowiedź:

if [ "$ANSWER" = "t" ]; then
    echo "Kontynuuję."
else
    echo "Przerwano."
fi

8. Instrukcja if

Podstawowa składnia:

if warunek; then
    polecenia
fi

Przykład:

if [ "$NAME" = "user" ]; then
    echo "Witaj."
fi

Pełna wersja:

if [ "$ANSWER" = "1" ]; then
    echo "Wybrałeś opcję pierwszą."
elif [ "$ANSWER" = "2" ]; then
    echo "Wybrałeś opcję drugą."
else
    echo "Nieznana opcja."
fi

9. Polecenie test i nawiasy [ ]

To:

[ "$NAME" = "user" ]

jest tak naprawdę formą polecenia test.

Można napisać:

test "$NAME" = "user"

albo:

[ "$NAME" = "user" ]

Druga forma jest znacznie częściej używana.

Uwaga na spacje:

[ "$NAME" = "user" ]

jest poprawne.

To:

["$NAME"="user"]

jest błędne.


10. Najważniejsze testy

Czy plik istnieje?

if [ -f "$FILE" ]; then
    echo "Plik istnieje."
fi

Czy katalog istnieje?

if [ -d "$DIR" ]; then
    echo "Katalog istnieje."
fi

Czy coś istnieje niezależnie od typu?

if [ -e "$PATHNAME" ]; then
    echo "Istnieje."
fi

Czy plik jest wykonywalny?

if [ -x "$FILE" ]; then
    echo "Plik jest wykonywalny."
fi

Czy plik jest zapisywalny?

if [ -w "$FILE" ]; then
    echo "Można zapisywać."
fi

Czy zmienna jest pusta?

if [ -z "$NAME" ]; then
    echo "Brak nazwy."
fi

Czy zmienna nie jest pusta?

if [ -n "$NAME" ]; then
    echo "Podano nazwę."
fi

11. Porównywanie liczb

Dla liczb nie używa się zwykle =.

Przykłady:

[ "$A" -eq "$B" ]

równe

[ "$A" -ne "$B" ]

różne

[ "$A" -gt "$B" ]

większe

[ "$A" -lt "$B" ]

mniejsze

[ "$A" -ge "$B" ]

większe lub równe

[ "$A" -le "$B" ]

mniejsze lub równe

Przykład:

if [ "$COUNT" -gt 10 ]; then
    echo "Dużo wyników."
fi

12. case - idealne do menu

Jeśli użytkownik ma wybrać jedną z kilku opcji, case jest często wygodniejsze niż wiele if.

echo "1 - aktualizacja"
echo "2 - informacje o systemie"
echo "3 - wyjście"

printf 'Wybór: '
read CHOICE

case "$CHOICE" in
    1)
        echo "Uruchamiam aktualizację..."
        ;;
    2)
        echo "Pokazuję informacje..."
        ;;
    3)
        echo "Koniec."
        exit 0
        ;;
    *)
        echo "Nieznana opcja."
        ;;
esac

*) oznacza wszystko, czego nie dopasowały wcześniejsze przypadki.


13. Reagowanie na wynik polecenia

Każde polecenie zwraca kod wyjścia.

Standard:

0       sukces
inne    błąd lub inny stan

Kod ostatniego polecenia:

echo "$?"

Przykład:

ping -c 1 1.1.1.1
echo "$?"

Jeśli ping się uda, zwykle zobaczysz:

0

14. Najlepszy sposób sprawdzania sukcesu polecenia

Zamiast:

command
if [ "$?" -eq 0 ]; then

lepiej:

if command; then
    echo "Sukces."
else
    echo "Błąd."
fi

Przykład:

if ping -c 1 1.1.1.1 >/dev/null 2>&1; then
    echo "Internet działa."
else
    echo "Brak odpowiedzi."
fi

15. && i ||

&& oznacza:

wykonaj następne polecenie tylko wtedy, gdy poprzednie się udało.

mkdir backup && echo "Katalog utworzony."

|| oznacza:

wykonaj następne polecenie tylko wtedy, gdy poprzednie się nie udało.

mkdir backup || echo "Nie udało się utworzyć katalogu."

Można połączyć:

command && echo "OK" || echo "BŁĄD"

Ale przy bardziej rozbudowanej logice lepiej używać if.


16. Przekierowanie wyjścia

Standardowe wyjście programu:

stdout

Błędy:

stderr

Zapis stdout do pliku:

command > output.txt

Dopisanie:

command >> output.txt

Błędy do pliku:

command 2> errors.txt

Wszystko do jednego pliku:

command > output.txt 2>&1

Ukrycie całego wyjścia:

command >/dev/null 2>&1

17. Statusy na ekranie

Prosty schemat:

echo "[INFO] Sprawdzam połączenie..."

if ping -c 1 1.1.1.1 >/dev/null 2>&1; then
    echo "[OK] Sieć działa."
else
    echo "[ERROR] Brak połączenia."
fi

Można przyjąć własną konwencję:

[INFO]
[OK]
[WARN]
[ERROR]

Dzięki temu skrypt jest znacznie czytelniejszy.


18. Funkcje

Jeśli jakaś czynność powtarza się kilka razy, warto zrobić funkcję.

show_status() {
    echo "[INFO] $1"
}

Wywołanie:

show_status "Uruchamiam aktualizację"

$1 oznacza pierwszy argument funkcji.

Przykład:

success() {
    echo "[OK] $1"
}

error() {
    echo "[ERROR] $1"
}

Użycie:

success "Operacja zakończona."
error "Nie udało się połączyć."

19. Argumenty przekazane do skryptu

Jeżeli uruchomisz:

myscript test plik.txt

wewnątrz skryptu:

$0    nazwa skryptu
$1    pierwszy argument
$2    drugi argument
$3    trzeci argument

Przykład:

#!/bin/sh

echo "Skrypt: $0"
echo "Argument 1: $1"
echo "Argument 2: $2"

Liczba argumentów:

echo "$#"

Wszystkie argumenty:

echo "$@"

20. Sprawdzanie wymaganych argumentów

Przykład:

if [ "$#" -lt 1 ]; then
    echo "Użycie: $0 nazwa_pliku"
    exit 1
fi

Potem:

FILE="$1"

Pełny przykład:

#!/bin/sh

if [ "$#" -lt 1 ]; then
    echo "Użycie: $0 plik"
    exit 1
fi

FILE="$1"

if [ -f "$FILE" ]; then
    echo "Plik istnieje."
else
    echo "Pliku nie znaleziono."
    exit 1
fi

21. exit - kończenie skryptu

Poprawne zakończenie:

exit 0

Błąd:

exit 1

Można używać innych wartości, ale najczęściej wystarcza:

0 = OK
1 = błąd

Przykład:

if [ ! -f "$CONFIG" ]; then
    echo "Brak pliku konfiguracji."
    exit 1
fi

22. Negacja !

Warunek:

[ -f "$FILE" ]

oznacza:

plik istnieje.

Natomiast:

[ ! -f "$FILE" ]

oznacza:

plik nie istnieje.

Przykład:

if [ ! -d "$HOME/backup" ]; then
    mkdir "$HOME/backup"
fi

23. Pętle for

Przykład:

for FILE in *.txt; do
    echo "Plik: $FILE"
done

Lista wartości:

for ITEM in one two three; do
    echo "$ITEM"
done

Argumenty skryptu:

for ARG in "$@"; do
    echo "Argument: $ARG"
done

24. Pętla while

Przykład:

COUNT=1

while [ "$COUNT" -le 5 ]; do
    echo "$COUNT"
    COUNT=$((COUNT + 1))
done

25. Pętla menu

Bardzo przydatny wzorzec:

#!/bin/sh

while true; do
    echo
    echo "1 - pokaż uptime"
    echo "2 - pokaż dyski"
    echo "3 - pokaż pamięć"
    echo "q - wyjście"

    printf 'Wybór: '
    read CHOICE

    case "$CHOICE" in
        1)
            uptime
            ;;
        2)
            df -h
            ;;
        3)
            free -h
            ;;
        q|Q)
            exit 0
            ;;
        *)
            echo "Nieznana opcja."
            ;;
    esac
done

Uwaga:

free -h

jest typowe dla Linuksa.

Na FreeBSD pamięć można sprawdzać np.:

sysctl hw.physmem

lub:

top

Dlatego w skryptach działających na obu systemach czasem trzeba rozpoznać system.


26. Rozpoznawanie systemu

Polecenie:

uname -s

Na Debianie zwróci:

Linux

Na FreeBSD:

FreeBSD

Przykład:

OS="$(uname -s)"

case "$OS" in
    Linux)
        echo "To jest Linux."
        ;;
    FreeBSD)
        echo "To jest FreeBSD."
        ;;
    *)
        echo "Nieznany system: $OS"
        ;;
esac

27. Skrypt Debian + FreeBSD

Przykład aktualizacji pakietów:

#!/bin/sh

OS="$(uname -s)"

case "$OS" in
    Linux)
        echo "[INFO] Aktualizacja Debiana..."
        sudo apt update
        sudo apt upgrade
        ;;
    FreeBSD)
        echo "[INFO] Aktualizacja pakietów FreeBSD..."
        sudo pkg update
        sudo pkg upgrade
        ;;
    *)
        echo "[ERROR] Nieobsługiwany system: $OS"
        exit 1
        ;;
esac

To prosty przykład pokazujący, jak jeden skrypt może wykonać inne polecenia zależnie od systemu.


28. Podstawianie wyniku polecenia do zmiennej

Składnia:

VARIABLE="$(command)"

Przykład:

HOSTNAME="$(hostname)"

Potem:

echo "Host: $HOSTNAME"

Inny przykład:

KERNEL="$(uname -r)"

echo "Kernel: $KERNEL"

29. Operacje matematyczne

POSIX shell:

A=5
B=3

RESULT=$((A + B))

echo "$RESULT"

Inne przykłady:

COUNT=$((COUNT + 1))
RESULT=$((A * B))
RESULT=$((A / B))

30. Domyślna wartość zmiennej

Przydatny zapis:

NAME="${1:-world}"

Jeżeli podano pierwszy argument:

script user

NAME będzie:

user

Jeżeli nie:

script

NAME będzie:

world

31. Sprawdzanie, czy program istnieje

Bardzo przydatny wzorzec:

if command -v curl >/dev/null 2>&1; then
    echo "curl jest zainstalowany."
else
    echo "Brak curl."
fi

command -v jest bardziej przenośne niż which.

Można zrobić:

require_command() {
    if ! command -v "$1" >/dev/null 2>&1; then
        echo "[ERROR] Brak programu: $1"
        exit 1
    fi
}

Użycie:

require_command curl
require_command git

32. Bezpieczniejsze skrypty

W Bashu często spotkasz:

set -euo pipefail

Znaczenie:

-e       zakończ skrypt po błędzie polecenia
-u       błąd przy użyciu niezdefiniowanej zmiennej
-o pipefail
         pipeline uznawany jest za błędny, jeśli którykolwiek element zawiedzie

pipefail nie jest przenośnym elementem POSIX sh.

Jeżeli piszesz:

#!/usr/bin/env bash

możesz użyć:

set -euo pipefail

Jeżeli piszesz przenośny:

#!/bin/sh

często stosuje się:

set -eu

ale trzeba rozumieć ich działanie, ponieważ mogą zakończyć skrypt w miejscu, którego początkowo się nie spodziewasz.


33. Komentarze

Komentarz zaczyna się od #.

# Sprawdzamy połączenie z internetem

ping -c 1 1.1.1.1

Wyjątkiem jest pierwsza linia:

#!/bin/sh

która jest interpretowana przez system jako shebang.


34. Czytelna struktura skryptu

Dobry skrypt często wygląda mniej więcej tak:

#!/bin/sh

# Konfiguracja

CONFIG="$HOME/.config/mytool.conf"

# Funkcje

info() {
    echo "[INFO] $1"
}

error() {
    echo "[ERROR] $1" >&2
}

# Walidacja

if [ ! -f "$CONFIG" ]; then
    error "Brak konfiguracji."
    exit 1
fi

# Główna część programu

info "Uruchamiam."

# ...

info "Gotowe."

exit 0

35. stderr - komunikaty błędów

Normalny komunikat:

echo "Gotowe."

Komunikat błędu warto wysłać do stderr:

echo "Błąd!" >&2

Dzięki temu użytkownik może osobno przekierować normalne wyniki i błędy.

Przykład:

mytool >output.txt 2>errors.txt

36. Jak uruchamiać skrypt bez ./

Załóżmy, że masz:

/home/user/scripts/workvpn

i próbujesz:

workvpn

Shell znajdzie program tylko wtedy, gdy jego katalog znajduje się w PATH.

Sprawdź:

echo "$PATH"

37. Najlepsze miejsce na własne skrypty użytkownika

Dobrym rozwiązaniem jest:

~/.local/bin

Utwórz katalog:

mkdir -p "$HOME/.local/bin"

Przenieś skrypt:

mv myscript "$HOME/.local/bin/"

Nadaj prawa:

chmod +x "$HOME/.local/bin/myscript"

Teraz trzeba upewnić się, że katalog jest w PATH.


38. Dodanie ~/.local/bin do PATH

Dla sh lub Bash:

PATH="$HOME/.local/bin:$PATH"
export PATH

Można też krócej:

export PATH="$HOME/.local/bin:$PATH"

Aby działało po ponownym logowaniu, wpis trzeba umieścić w pliku startowym shella.


39. Gdzie ustawić PATH

To zależy od shella.

Bash

Najczęściej:

~/.bashrc

lub przy logowaniu:

~/.profile

Bezpieczna konfiguracja w ~/.profile:

if [ -d "$HOME/.local/bin" ]; then
    PATH="$HOME/.local/bin:$PATH"
fi

export PATH

POSIX sh

Najczęściej:

~/.profile

Zsh

Najczęściej:

~/.zshrc

tcsh / csh

Składnia jest inna.

Przykładowo w tcsh:

setenv PATH "$HOME/.local/bin:$PATH"

Typowy plik:

~/.tcshrc

40. Wczytanie zmian bez wylogowania

Jeżeli zmieniłeś:

~/.profile

możesz wykonać:

. "$HOME/.profile"

Kropka oznacza polecenie source.

W Bashu można też:

source ~/.bashrc

POSIX-owa wersja:

. ~/.profile

41. Sprawdzenie, skąd uruchamia się skrypt

Użyj:

command -v myscript

Przykład:

/home/user/.local/bin/myscript

Możesz też:

type myscript

Wynik pokaże, czy jest to:

  • alias,
  • funkcja,
  • builtin,
  • plik wykonywalny.

42. Skrypty dostępne dla wszystkich użytkowników

Jeżeli skrypt ma być dostępny systemowo:

/usr/local/bin

To zazwyczaj właściwe miejsce na ręcznie instalowane narzędzia administratora.

Przykład:

sudo cp myscript /usr/local/bin/myscript
sudo chmod 755 /usr/local/bin/myscript

Potem:

myscript

powinno działać z każdego katalogu.

/usr/local/bin jest dobrym wyborem zarówno na Debianie, jak i FreeBSD.

Nie wrzucaj własnych skryptów bez potrzeby do:

/bin
/usr/bin

Są to katalogi należące do systemu i menedżera pakietów.


43. Nazwa pliku skryptu

Skrypt nie musi kończyć się:

.sh

Dla narzędzi CLI często lepiej użyć po prostu:

workvpn
backup
servercheck
web-monitor

zamiast:

workvpn.sh
backup.sh
servercheck.sh

Jeżeli plik ma poprawny shebang i prawa wykonywania, rozszerzenie nie jest potrzebne.


44. Aliasy a skrypty

Alias:

alias ll='ls -la'

jest dobry dla prostego skrótu.

Skrypt jest lepszy, gdy potrzebujesz:

  • warunków,
  • zmiennych,
  • pytań,
  • funkcji,
  • wielu poleceń,
  • obsługi błędów.

Zamiast tworzyć ogromny alias, lepiej napisać program w shellu.


45. Prosty przykład praktyczny - sprawdzanie serwera

#!/bin/sh

HOST="${1:-1.1.1.1}"

echo "[INFO] Sprawdzam host: $HOST"

if ping -c 1 "$HOST" >/dev/null 2>&1; then
    echo "[OK] Host odpowiada."
    exit 0
else
    echo "[ERROR] Host nie odpowiada." >&2
    exit 1
fi

Użycie:

checkhost

albo:

checkhost 192.168.1.1

46. Przykład praktyczny - menu administracyjne

#!/bin/sh

while true; do
    echo
    echo "=== MENU ==="
    echo "1. Uptime"
    echo "2. Miejsce na dyskach"
    echo "3. Adresy IP"
    echo "4. Kernel"
    echo "q. Wyjście"

    printf 'Wybór: '
    read CHOICE

    case "$CHOICE" in
        1)
            uptime
            ;;
        2)
            df -h
            ;;
        3)
            if command -v ip >/dev/null 2>&1; then
                ip addr
            else
                ifconfig
            fi
            ;;
        4)
            uname -a
            ;;
        q|Q)
            echo "Koniec."
            exit 0
            ;;
        *)
            echo "Nieznana opcja."
            ;;
    esac
done

Ten przykład działa sensownie zarówno w Linuksie, jak i BSD dzięki sprawdzeniu obecności polecenia ip.


47. Przykład praktyczny - pytanie przed wykonaniem operacji

#!/bin/sh

printf 'Usunąć pliki tymczasowe? [t/N] '
read ANSWER

case "$ANSWER" in
    t|T|tak|TAK|Tak)
        echo "[INFO] Usuwam..."
        rm -rf "$HOME/tmp/test"
        echo "[OK] Gotowe."
        ;;
    *)
        echo "Anulowano."
        ;;
esac

48. Przykład praktyczny - skrypt aktualizujący system

#!/bin/sh

OS="$(uname -s)"

echo "[INFO] Wykryty system: $OS"

case "$OS" in
    Linux)
        echo "[INFO] Aktualizuję listę pakietów..."
        sudo apt update || exit 1

        printf 'Uruchomić apt upgrade? [t/N] '
        read ANSWER

        case "$ANSWER" in
            t|T|tak|TAK|Tak)
                sudo apt upgrade
                ;;
            *)
                echo "Pominięto upgrade."
                ;;
        esac
        ;;

    FreeBSD)
        echo "[INFO] Aktualizuję repozytoria pkg..."
        sudo pkg update || exit 1

        printf 'Uruchomić pkg upgrade? [t/N] '
        read ANSWER

        case "$ANSWER" in
            t|T|tak|TAK|Tak)
                sudo pkg upgrade
                ;;
            *)
                echo "Pominięto upgrade."
                ;;
        esac
        ;;

    *)
        echo "[ERROR] Nieobsługiwany system: $OS" >&2
        exit 1
        ;;
esac

49. Przykład praktyczny - wykonywanie serii poleceń ze statusami

#!/bin/sh

step() {
    echo
    echo "[INFO] $1"
}

success() {
    echo "[OK] $1"
}

error() {
    echo "[ERROR] $1" >&2
}

step "Sprawdzam DNS"

if ping -c 1 1.1.1.1 >/dev/null 2>&1; then
    success "Połączenie działa."
else
    error "Brak połączenia."
fi

step "Sprawdzam miejsce na dysku"

df -h

step "Sprawdzam kernel"

uname -a

success "Diagnostyka zakończona."

50. Przykład praktyczny - narzędzie przyjmujące polecenia

#!/bin/sh

COMMAND="${1:-}"

case "$COMMAND" in
    status)
        uptime
        df -h
        ;;
    network)
        if command -v ip >/dev/null 2>&1; then
            ip addr
        else
            ifconfig
        fi
        ;;
    kernel)
        uname -a
        ;;
    "")
        echo "Użycie:"
        echo "  syscheck status"
        echo "  syscheck network"
        echo "  syscheck kernel"
        ;;
    *)
        echo "Nieznane polecenie: $COMMAND" >&2
        exit 1
        ;;
esac

Użycie:

syscheck status
syscheck network
syscheck kernel

To jest już początek normalnego programu CLI.


51. Bardziej rozbudowany przykład

#!/bin/sh

info() {
    echo "[INFO] $1"
}

success() {
    echo "[OK] $1"
}

error() {
    echo "[ERROR] $1" >&2
}

check_network() {
    info "Sprawdzam sieć..."

    if ping -c 1 1.1.1.1 >/dev/null 2>&1; then
        success "Sieć działa."
        return 0
    else
        error "Brak odpowiedzi."
        return 1
    fi
}

show_system() {
    echo
    echo "System:"
    uname -a

    echo
    echo "Uptime:"
    uptime

    echo
    echo "Dyski:"
    df -h
}

main_menu() {
    while true; do
        echo
        echo "1. Sprawdź sieć"
        echo "2. Informacje o systemie"
        echo "q. Wyjście"

        printf '> '
        read CHOICE

        case "$CHOICE" in
            1)
                check_network
                ;;
            2)
                show_system
                ;;
            q|Q)
                exit 0
                ;;
            *)
                error "Nieznana opcja."
                ;;
        esac
    done
}

main_menu

Tutaj program jest już podzielony na funkcje.

To bardzo dobry sposób organizacji większych skryptów.


52. return kontra exit

exit kończy cały skrypt.

exit 1

return kończy funkcję.

my_function() {
    if coś; then
        return 1
    fi

    return 0
}

Przykład:

check_network() {
    ping -c 1 1.1.1.1 >/dev/null 2>&1
}

Możemy potem:

if check_network; then
    echo "Sieć działa."
fi

Funkcja zwraca status ostatniego polecenia.


53. Pipeline

Operator:

|

przekazuje wyjście jednego programu na wejście drugiego.

Przykład:

ps aux | grep nginx

Albo:

dmesg | grep -i error

Albo:

ls -la | less

Pipeline jest jedną z najważniejszych idei Uniksa:

programy powinny robić małe rzeczy i dawać się łatwo łączyć.


54. Wyszukiwanie tekstu

Przykład:

grep "error" logfile.txt

Bez rozróżniania wielkości liter:

grep -i "error" logfile.txt

Rekurencyjnie:

grep -R "TODO" .

W skrypcie:

if grep -q "enabled=true" config.txt; then
    echo "Funkcja jest włączona."
fi

Opcja:

-q

oznacza quiet - bez wypisywania wyniku.


55. Obsługa Ctrl+C i trap

Można reagować na sygnały.

Przykład:

cleanup() {
    echo
    echo "Sprzątam..."
    rm -f /tmp/mytool.lock
}

trap cleanup EXIT

Funkcja wykona się przy zakończeniu skryptu.

Można też:

trap 'echo "Przerwano."; exit 1' INT

INT odpowiada m.in. Ctrl+C.

Bardziej praktyczny przykład:

TMPFILE="$(mktemp)"

cleanup() {
    rm -f "$TMPFILE"
}

trap cleanup EXIT

Dzięki temu plik tymczasowy zostanie usunięty nawet po błędzie.


56. Pliki tymczasowe

Nie twórz przewidywalnych nazw typu:

/tmp/test.txt

gdy skrypt może być uruchamiany równolegle.

Lepiej:

TMPFILE="$(mktemp)"

Potem:

echo "dane" > "$TMPFILE"

Na końcu:

rm -f "$TMPFILE"

Najlepiej razem z trap.


57. Debugowanie skryptu

Uruchom:

sh -x script

Shell będzie pokazywał wykonywane polecenia.

Dla Basha:

bash -x script

Można też chwilowo dodać:

set -x

a potem wyłączyć:

set +x

Przykład:

set -x
command1
command2
set +x

58. Sprawdzanie składni

Dla sh:

sh -n script

Dla Basha:

bash -n script

Opcja -n sprawdza składnię bez wykonywania skryptu.


59. ShellCheck

Bardzo przydatne narzędzie do sprawdzania skryptów.

Debian:

sudo apt install shellcheck

FreeBSD:

sudo pkg install hs-ShellCheck

Sprawdzenie:

shellcheck script

ShellCheck wykrywa m.in.:

  • brakujące cudzysłowy,
  • niebezpieczne konstrukcje,
  • błędy składni,
  • błędne użycie zmiennych,
  • problemy z przenośnością.

Dla nauki shell scriptingu jest niezwykle przydatny.


60. shellcheck + Vim

Dobry prosty workflow:

vim myscript

potem:

sh -n myscript

następnie:

shellcheck myscript

i dopiero:

./myscript

61. Uprawnienia

Sprawdzenie:

ls -l myscript

Możesz zobaczyć:

-rwxr-xr-x

Litera x oznacza prawo wykonywania.

Dodanie:

chmod +x myscript

Typowe prawa dla systemowego narzędzia:

chmod 755 myscript

czyli:

właściciel: rwx
grupa:     r-x
inni:      r-x

62. Skrypt wymagający roota

Można sprawdzić UID:

id -u

Root ma UID:

0

Przykład:

if [ "$(id -u)" -ne 0 ]; then
    echo "Ten skrypt musi być uruchomiony jako root." >&2
    exit 1
fi

Uruchomienie:

sudo myscript

Nie każdy skrypt administracyjny powinien być uruchamiany cały jako root.

Często bezpieczniej uruchamiać tylko konkretne polecenia przez:

sudo command

63. Konfiguracja skryptu

Zamiast wpisywać wszystko na stałe:

SERVER="192.168.1.10"
PORT="8080"

możesz użyć pliku konfiguracji.

Przykład:

~/.config/mytool/config

Zawartość:

SERVER="192.168.1.10"
PORT="8080"

Wczytanie:

. "$HOME/.config/mytool/config"

Uwaga:

plik wczytany przez . jest wykonywany jak kod shellowy.

Dlatego nie wolno wczytywać w ten sposób niezaufanych plików.


64. Katalog konfiguracji użytkownika

Dobry układ:

~/.config/mytool/

np.:

~/.config/mytool/config

Program:

~/.local/bin/mytool

Dane:

~/.local/share/mytool/

Logi można trzymać np.:

~/.local/state/mytool/

To dobrze współgra z konwencjami XDG stosowanymi w nowoczesnych systemach uniksowych.


65. Kolory - opcjonalnie

Można użyć kodów ANSI.

Przykład:

printf '\033[32m[OK]\033[0m Gotowe\n'

Czerwony:

printf '\033[31m[ERROR]\033[0m Błąd\n'

Żółty:

printf '\033[33m[WARN]\033[0m Ostrzeżenie\n'

Warto jednak traktować kolory jako dodatek. Skrypt powinien być czytelny także bez nich.


66. Podstawowy szablon własnego narzędzia

Dobry start dla prostego programu:

#!/bin/sh

info() {
    echo "[INFO] $1"
}

success() {
    echo "[OK] $1"
}

error() {
    echo "[ERROR] $1" >&2
}

main() {
    info "Uruchamiam."

    printf 'Podaj nazwę hosta: '
    IFS= read -r HOST

    if [ -z "$HOST" ]; then
        error "Nie podano hosta."
        exit 1
    fi

    info "Sprawdzam $HOST..."

    if ping -c 1 "$HOST" >/dev/null 2>&1; then
        success "$HOST odpowiada."
    else
        error "$HOST nie odpowiada."
        exit 1
    fi
}

main "$@"

To dobry fundament do dalszego rozbudowywania.


67. Przykład instalacji własnego skryptu

Masz plik:

servercheck

Nadajesz prawa:

chmod +x servercheck

Tworzysz lokalny katalog programów:

mkdir -p "$HOME/.local/bin"

Przenosisz:

mv servercheck "$HOME/.local/bin/"

Dodajesz do ~/.profile:

if [ -d "$HOME/.local/bin" ]; then
    PATH="$HOME/.local/bin:$PATH"
fi

export PATH

Wczytujesz:

. "$HOME/.profile"

Sprawdzasz:

command -v servercheck

Powinno zwrócić coś w rodzaju:

/home/user/.local/bin/servercheck

Od tej chwili możesz być w dowolnym katalogu:

cd /

i nadal uruchomić:

servercheck

68. Gdy skrypt ma być naprawdę „zainstalowany”

Dla prywatnego narzędzia użytkownika:

~/.local/bin

Dla narzędzia dostępnego globalnie:

/usr/local/bin

Przykład:

sudo install -m 755 servercheck /usr/local/bin/servercheck

Polecenie install jest wygodne, bo jednocześnie kopiuje plik i ustawia uprawnienia.

Sprawdzenie:

command -v servercheck

69. Czego nauczyć się w następnej kolejności

Jeżeli opanujesz materiał z tego dokumentu, kolejne przydatne tematy to:

  1. sed
  2. awk
  3. bardziej zaawansowane grep
  4. wyrażenia regularne
  5. find
  6. xargs
  7. getopts
  8. logowanie
  9. lockfile
  10. sygnały i trap
  11. cron
  12. systemd timers na Debianie
  13. rc.d na FreeBSD

70. getopts - pierwszy krok do porządnego CLI

Z czasem zamiast:

myscript host port

możesz chcieć:

myscript -h server.example.com -p 8080

Do tego w POSIX shellu służy:

getopts

Przykład:

#!/bin/sh

while getopts "h:p:" OPT; do
    case "$OPT" in
        h)
            HOST="$OPTARG"
            ;;
        p)
            PORT="$OPTARG"
            ;;
        *)
            exit 1
            ;;
    esac
done

echo "Host: $HOST"
echo "Port: $PORT"

Nie trzeba znać getopts na początku, ale jest to naturalny następny krok, gdy skrypt zaczyna przypominać prawdziwe narzędzie CLI.


71. Najważniejsze zasady, które warto zapamiętać

Cytuj zmienne

Zamiast:

rm $FILE

pisz:

rm "$FILE"

Sprawdzaj błędy

Zamiast:

command
echo "Gotowe"

lepiej:

if command; then
    echo "[OK] Gotowe"
else
    echo "[ERROR] Operacja nieudana" >&2
    exit 1
fi

Używaj funkcji

Zamiast jednego skryptu mającego 300 linii bez struktury, podziel go na:

check_config
check_network
do_backup
show_status
main

Trzymaj własne programy w odpowiednim miejscu

Prywatne:

~/.local/bin

Systemowe:

/usr/local/bin

Używaj command -v

Zamiast:

which curl

preferuj:

command -v curl

Sprawdzaj skrypt

sh -n script

i:

shellcheck script

72. Minimalny zestaw wiedzy potrzebny do pisania użytecznych skryptów

Jeżeli potrafisz używać:

#!/bin/sh

echo
printf
read

VARIABLE=value
"$VARIABLE"

if
elif
else

[ ... ]

case

for
while

$1
$2
$@
$#

exit

$?

&&
||

>

>>

2>

|

$(command)

functions

command -v

to potrafisz już stworzyć bardzo dużą część praktycznych skryptów administracyjnych.

Reszta to przede wszystkim poznawanie kolejnych poleceń systemowych i łączenie ich w sensowną logikę.


73. Jak myśleć o skrypcie shellowym

Najprostszy model:

INPUT
  ↓
sprawdzenie danych
  ↓
decyzja
  ↓
wykonanie poleceń
  ↓
sprawdzenie wyniku
  ↓
STATUS / OUTPUT

Przykład:

użytkownik podaje host
        ↓
czy host jest pusty?
        ↓
       nie
        ↓
uruchamiamy ping
        ↓
czy ping zwrócił 0?
       ↙   ↘
     tak   nie
      ↓     ↓
    [OK]  [ERROR]

Właśnie na tym polega większość prostych programów shellowych.


74. Mały projekt do samodzielnego zrobienia

Dobrym pierwszym własnym programem byłby:

sysinfo

Uruchamiany:

sysinfo

Menu:

1. System
2. Sieć
3. Dyski
4. Procesy
5. Aktualizacje
q. Wyjście

Skrypt może sam rozpoznawać:

Linux
FreeBSD

i używać właściwych poleceń dla danego systemu.

To mały projekt, ale wykorzystuje praktycznie wszystko:

  • zmienne,
  • case,
  • funkcje,
  • read,
  • sprawdzanie systemu,
  • polecenia zewnętrzne,
  • kody błędów,
  • menu,
  • PATH.

Po napisaniu takiego narzędzia podstawy shell scriptingu przestają być teorią.


Ściąga

Początek skryptu

#!/bin/sh

Komunikat

echo "Tekst"

Zmienna

NAME="user"
echo "$NAME"

Input

read NAME

Warunek

if [ "$A" = "$B" ]; then
    echo "Tak"
fi
case "$CHOICE" in
    1)
        command
        ;;
    *)
        echo "Błąd"
        ;;
esac

Funkcja

hello() {
    echo "Hello"
}

Wynik polecenia

if command; then
    echo "OK"
else
    echo "Błąd"
fi

Argument

FILE="$1"

Wynik polecenia do zmiennej

HOST="$(hostname)"

Sprawdzenie programu

command -v git

Udostępnienie z każdego katalogu

mkdir -p ~/.local/bin
mv script ~/.local/bin/
chmod +x ~/.local/bin/script

oraz w ~/.profile:

export PATH="$HOME/.local/bin:$PATH"

Sprawdzenie

command -v script

Podsumowanie

Do tworzenia praktycznych programów shellowych nie trzeba poznawać ogromnego języka.

Najważniejsze jest opanowanie kilku elementów:

polecenia
zmienne
input
warunki
case
pętle
funkcje
argumenty
kody wyjścia
przekierowania
pipeline
PATH

Shell jest szczególnie dobry wtedy, gdy program przede wszystkim:

  • uruchamia inne programy,
  • zarządza plikami,
  • konfiguruje system,
  • sprawdza usługi,
  • przetwarza tekst,
  • automatyzuje administrację.

Jeżeli logika programu zaczyna dominować nad wykonywaniem poleceń systemowych, kod robi się bardzo duży albo potrzebujesz skomplikowanych struktur danych, wtedy zwykle lepiej przejść do Pythona, Go lub innego pełnego języka programowania.

Do małych narzędzi systemowych shell pozostaje jednak jednym z najszybszych i najbardziej naturalnych rozwiązań.

Oficjalne źródła

  • POSIX Shell Command Language: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html
  • Debian Reference - shell programming: https://www.debian.org/doc/manuals/debian-reference/ch12
  • FreeBSD sh(1): https://man.freebsd.org/cgi/man.cgi?query=sh&sektion=1