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:
sedawk- bardziej zaawansowane
grep - wyrażenia regularne
findxargsgetopts- logowanie
- lockfile
- sygnały i
trap - cron
- systemd timers na Debianie
- 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
Menu
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