Opcje jak w prawdziwych programach
Po tej lekcji: kursant dodaje do swoich skryptów opcje w stylu -v, -d FOLDER, -h i --help przez getopts, pisze porządny ekran pomocy i sprawdza dane od użytkownika, zanim skrypt cokolwiek zrobi.
Wstęp
ls --help, długa ściana opcji przewija się przez ekran
Wpisz ls --help. Albo grep --help. Każdy porządny program zachowuje się tak samo: ma opcje z myślnikiem, ma pomoc, a jak wpiszesz bzdurę, mówi dokładnie, co jest nie tak.
$ ls --zle
ls: unrecognized option '--zle'
Try 'ls --help' for more information.A nasze skrypty? ./skrypt.sh /var/log 30 tak. Co to jest „tak”? Co znaczy 30? A jak zamienisz kolejność, to skrypt usunie pliki z folderu „30” starsze niż „/var/log” dni? Za pół roku nawet ty nie będziesz pamiętał, który argument był który. Twój kolega na pewno nie, i zadzwoni do ciebie w niedzielę.
Dziś skrypt dostanie opcje jak prawdziwy program. Na koniec będziemy mieli narzędzie, które sprząta stare pliki, domyślnie niczego nie kasuje, a zapytane o pomoc odpowiada jak dobrze wychowany urzędnik.
Argumenty po kolei kontra opcje
Argumenty pozycyjne, $1, $2, $3, to jak formularz, w którym nie ma podpisanych pól. Musisz pamiętać, że w trzeciej kratce wpisuje się PESEL, a w piątej nazwisko panieńskie matki. Pomylisz kratki i wniosek idzie do kosza. Albo gorzej, zostaje rozpatrzony.
Opcje to formularz z podpisami przy polach: -d to folder, -n to liczba dni. Kolejność nie ma znaczenia, część pól jest nieobowiązkowa, a do części wpisujesz wartość.
Umowa w świecie Uniksa wygląda tak:
-v— przełącznik, włączony albo nie.-d FOLDER— opcja z wartością.-vu— kilka przełączników sklejonych razem, to samo co-v -u.--— koniec opcji, dalej są już zwykłe argumenty.-hi--help— pomoc.
Do czytania tego w Bashu jest wbudowana komenda getopts.
getopts krok po kroku
getopts to urzędnik, który bierze z kupki argumentów po jednej opcji naraz. Wołasz go w pętli while. Za każdym razem mówi, jaką opcję znalazł, a jeśli opcja ma wartość, kładzie ją do zmiennej OPTARG. Kiedy dojdzie do czegoś, co nie zaczyna się od myślnika, kończy.
#!/bin/bash
while getopts "vd:" opcja; do
echo "opcja=$opcja OPTARG=${OPTARG:-} OPTIND=$OPTIND"
done
shift $((OPTIND - 1))
echo "zostało argumentów: $# -> $*"Napis "vd:" to spis dozwolonych opcji. Każda litera to opcja. Dwukropek po literze znaczy: „ta opcja wymaga wartości”. Czyli v to przełącznik, a d: to opcja z wartością.
./demo1.sh -v -d /tmp plik1 plik2opcja=v OPTARG= OPTIND=2
opcja=d OPTARG=/tmp OPTIND=4
zostało argumentów: 2 -> plik1 plik2OPTIND to numer następnej kartki do przeczytania. Po pętli shift $((OPTIND - 1)) zrzuca ze stosu wszystkie przeczytane opcje i zostają same zwykłe argumenty, czyli pliki. Znasz shift z lekcji o argumentach. Tu przesuwa o tyle, ile trzeba, jednym ruchem.
Sklejanie też działa:
./demo1.sh -vd/tmp plik1opcja=v OPTARG= OPTIND=1
opcja=d OPTARG=/tmp OPTIND=2
zostało argumentów: 1 -> plik1-vd/tmp to dla getopts -v i -d /tmp. Zauważ, że po v OPTIND dalej wynosi 1, bo urzędnik jeszcze nie skończył tej kartki.
A to ważne: opcje muszą być przed zwykłymi argumentami.
./demo1.sh plik1 -vzostało argumentów: 2 -> plik1 -vgetopts zobaczył plik1, uznał, że opcje się skończyły, i poszedł do domu. -v zostało zwykłym argumentem. [dla zaawansowanych: programy GNU, jak ls, przestawiają argumenty i przyjmują opcje także na końcu. getopts w Bashu trzyma się starszej, POSIX-owej zasady: pierwszy argument bez myślnika kończy opcje.]
Gdy użytkownik poda bzdurę
Domyślnie getopts sam wypisuje błędy po angielsku:
demo1.sh: illegal option -- x
demo1.sh: option requires an argument -- dDziała, ale wygląda jak komunikat z lat osiemdziesiątych. Lepiej przejąć obsługę błędów. Robi się to dwukropkiem na początku spisu opcji: ":vd:". To tak zwany tryb cichy. getopts przestaje sam krzyczeć i zamiast tego zostawia ci znaki:
?w zmiennej opcji i nieznaną literę wOPTARG, kiedy opcji nie ma na liście,:w zmiennej opcji i literę wOPTARG, kiedy opcji brakuje wartości.
Klasyczna obsługa to case z lekcji o menu:
#!/bin/bash
while getopts ":vd:" opcja; do
case $opcja in
v) echo "gadatliwie" ;;
d) echo "folder: $OPTARG" ;;
:) echo "Opcja -$OPTARG wymaga wartości." >&2; exit 2 ;;
\?) echo "Nieznana opcja: -$OPTARG" >&2; exit 2 ;;
esac
done$ ./demo2.sh -x
Nieznana opcja: -x
kod 2
$ ./demo2.sh -d
Opcja -d wymaga wartości.
kod 2\? ma backslash, bo sam ? w case to wzorzec „dowolny jeden znak” i złapałby wszystko. Kod 2 to umowa: błąd w sposobie użycia. Zobacz, co zwracają porządne programy, kiedy podasz złą opcję:
$ bash -c 'cd -Z'
bash: line 1: cd: -Z: invalid option
cd: usage: cd [-L|[-P [-e]] [-@]] [dir]
cd kod 2
ls kod 2
grep kod 2Wszystkie dwójki. Jest się kogo trzymać. Konwencjom kodów wyjścia poświęcimy kawałek lekcji 54.
Dwie pułapki getopts
Pierwsza: opcja z wartością zjada wszystko, co stoi po niej.
$ ./demo2.sh -d -v
folder: -v
kod 0Użytkownik zapomniał podać folderu, a getopts uznał, że folder nazywa się -v. Skrypt poszedł dalej. Dlatego wartości trzeba potem sprawdzić, o tym za chwilę.
Druga: getopts nie zna długich opcji.
$ ./demo2.sh --help
Nieznana opcja: --
kod 2Dla getopts --help to opcja o nazwie -, a za nią sklejone h, e, l, p. Pierwszy minus jest nieznany, więc koniec. Długie opcje trzeba obsłużyć ręcznie. Dla samego --help wystarczy prosta pętla przed getopts:
for arg in "$@"; do
if [[ $arg == "--help" ]]; then
pomoc
exit 0
fi
doneEkran pomocy
Pomoc piszemy jako funkcję z here-dokiem z lekcji 44. To najwygodniejszy sposób na kilkanaście linijek tekstu:
PROGRAM=${0##*/}
pomoc() {
cat <<KONIEC
Użycie: $PROGRAM -d FOLDER [-n DNI] [-u] [-v]
Wypisuje pliki w FOLDERZE starsze niż DNI dni.
Domyślnie niczego nie usuwa.
Opcje:
-d FOLDER gdzie szukać (wymagane)
-n DNI ile dni (domyślnie: 30)
-u naprawdę usuń znalezione pliki
-v pokaż rozmiar i datę każdego pliku
-h, --help ta pomoc
KONIEC
}${0##*/} to nożyczki z lekcji 41: obcinają ścieżkę i zostaje sama nazwa skryptu, bez ./ czy /usr/local/bin/. Nawiasy kwadratowe w linijce „Użycie” to umowa: w kwadratowych to, co nieobowiązkowe. WIELKIE litery to miejsce na twoją wartość. Ta sama umowa, co w man i w każdym --help.
Jeszcze jedna zasada: pomoc, o którą ktoś poprosił, idzie na standardowe wyjście i kończy się kodem 0. Komunikat o błędzie idzie na wyjście błędów i kończy się kodem 2. Ktoś, kto zrobi ./stare.sh --help | less, chce przewijać pomoc, a nie dostać ją rozlaną po ekranie.
Sprawdzanie danych
Pamiętasz bramkarza z lekcji o if? Teraz stawiamy całą ochronę przy wejściu. Zasada: sprawdź wszystko, zanim zrobisz cokolwiek. Skrypt, który usunie połowę plików, a potem odkryje, że liczba dni była bez sensu, jest gorszy niż skrypt, który od razu odmówi.
blad() {
echo "$PROGRAM: $1" >&2
echo "Spróbuj: $PROGRAM -h" >&2
exit 2
}
(( $# == 0 )) || blad "niepotrzebne argumenty: $*"
[[ -n $folder ]] || blad "brak wymaganej opcji -d FOLDER"
[[ -d $folder ]] || blad "nie ma takiego folderu: $folder"
[[ $dni =~ ^[0-9]+$ ]] || blad "-n musi być liczbą całkowitą, a jest: $dni"
dni=$((10#$dni)) # 08 to osiem, a nie błąd w systemie ósemkowym
(( dni >= 1 )) || blad "-n musi być co najmniej 1"Każda linijka to jeden bramkarz: „sprawdzam, a jak nie pasuje, wylatujesz”. Wzór ^[0-9]+$ to wyrażenie regularne z lekcji 45: od początku do końca same cyfry, co najmniej jedna. Bez minusów, bez przecinków, bez „trzydzieści”.
Co robi 10#, wyjaśnię w typowym błędzie. Bo ta linijka powstała dokładnie z tego błędu.
Całe narzędzie
#!/bin/bash
# stare.sh — wypisuje (albo usuwa) pliki starsze niż N dni
set -euo pipefail
PROGRAM=${0##*/}
folder=""
dni=30
usuwaj=0
gadatliwie=0
pomoc() {
cat <<KONIEC
Użycie: $PROGRAM -d FOLDER [-n DNI] [-u] [-v]
Wypisuje pliki w FOLDERZE starsze niż DNI dni.
Domyślnie niczego nie usuwa.
Opcje:
-d FOLDER gdzie szukać (wymagane)
-n DNI ile dni (domyślnie: 30)
-u naprawdę usuń znalezione pliki
-v pokaż rozmiar i datę każdego pliku
-h, --help ta pomoc
KONIEC
}
blad() {
echo "$PROGRAM: $1" >&2
echo "Spróbuj: $PROGRAM -h" >&2
exit 2
}
# --help nie jest dla getopts, łapiemy je ręcznie
for arg in "$@"; do
if [[ $arg == "--help" ]]; then
pomoc
exit 0
fi
done
while getopts ":d:n:uvh" opcja; do
case $opcja in
d) folder=$OPTARG ;;
n) dni=$OPTARG ;;
u) usuwaj=1 ;;
v) gadatliwie=1 ;;
h) pomoc; exit 0 ;;
:) blad "opcja -$OPTARG wymaga wartości" ;;
\?) blad "nieznana opcja -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
# sprawdzanie danych
(( $# == 0 )) || blad "niepotrzebne argumenty: $*"
[[ -n $folder ]] || blad "brak wymaganej opcji -d FOLDER"
[[ -d $folder ]] || blad "nie ma takiego folderu: $folder"
[[ $dni =~ ^[0-9]+$ ]] || blad "-n musi być liczbą całkowitą, a jest: $dni"
dni=$((10#$dni)) # 08 to osiem, a nie błąd w systemie ósemkowym
(( dni >= 1 )) || blad "-n musi być co najmniej 1"
licznik=0
while IFS= read -r -d '' plik; do
if (( gadatliwie )); then
printf '%s %8s B %s\n' "$(date -r "$plik" +%F)" "$(stat -c %s -- "$plik")" "$plik"
else
printf '%s\n' "$plik"
fi
if (( usuwaj )); then
rm -f -- "$plik"
fi
licznik=$((licznik + 1))
done < <(find "$folder" -type f -mtime +"$dni" -print0)
if (( usuwaj )); then
echo "Usunięto plików: $licznik"
else
echo "Znaleziono plików: $licznik (nic nie usunięto, dodaj -u)"
fiZbiera wszystko z modułu: set -euo pipefail, pętlę z -print0 odporną na spacje, cudzysłowy, -- przed nazwami plików. ShellCheck: cisza.
Zwróć uwagę na projekt: niebezpieczna akcja wymaga jawnej opcji. Bez -u skrypt tylko pokazuje. Dokładnie tak, jak w rozwiązaniu zadania z lekcji 49: najpierw patrzysz, potem kasujesz. Teraz ta ostrożność jest wbudowana w narzędzie.
ostrzeżenie: „Opcję -u testuj najpierw na folderze ze śmieciami testowymi albo na maszynie wirtualnej.”
Przygotowałem folder testowy smietnik z plikami w różnym wieku, także w podfolderze ze spacją. Uruchamiamy:
$ ./stare.sh -d smietnik
smietnik/duzy.log
smietnik/a.log
smietnik/stare logi/b c.log
Znaleziono plików: 3 (nic nie usunięto, dodaj -u)
$ ./stare.sh -vd smietnik
2026-07-26 2048 B smietnik/duzy.log
2026-09-24 0 B smietnik/a.log
2026-09-24 0 B smietnik/stare logi/b c.log
Znaleziono plików: 3 (nic nie usunięto, dodaj -u)
$ ./stare.sh -u -d smietnik -n 60
smietnik/duzy.log
Usunięto plików: 1A teraz próbujemy go zepsuć:
$ ./stare.sh
stare.sh: brak wymaganej opcji -d FOLDER
Spróbuj: stare.sh -h
$ ./stare.sh -d /nie/ma
stare.sh: nie ma takiego folderu: /nie/ma
Spróbuj: stare.sh -h
$ ./stare.sh -d smietnik -n trzydzieści
stare.sh: -n musi być liczbą całkowitą, a jest: trzydzieści
Spróbuj: stare.sh -h
$ ./stare.sh -d smietnik -x
stare.sh: nieznana opcja -x
Spróbuj: stare.sh -h
$ ./stare.sh -d smietnik extra
stare.sh: niepotrzebne argumenty: extra
Spróbuj: stare.sh -hZa każdym razem kod 2, komunikat mówi dokładnie, co nie tak, i podpowiada, gdzie szukać pomocy. Nie usunięto ani jednego pliku. Tak wygląda skrypt, który można dać koledze.
Jak to działa pod spodem
getopts to komenda wbudowana w Basha, a nie osobny program. Musi taka być, bo zmienia stan powłoki: ustawia zmienną opcji, OPTARG i OPTIND. Osobny program nie mógłby zmienić zmiennych Basha, tak jak kucharz w kuchni nie przestawi ci krzeseł na sali.
Przy każdym wywołaniu getopts patrzy na argument numer OPTIND. Jeśli zaczyna się od myślnika, bierze kolejną literę i szuka jej w spisie. Jeśli po literze w spisie stoi dwukropek, wartością jest reszta tej samej kartki (-d/tmp) albo cała następna kartka (-d /tmp). Kiedy kartka się skończy, OPTIND rośnie o jeden. Kiedy trafi na argument bez myślnika albo na --, zwraca kod różny od zera i pętla while się kończy.
OPTIND na starcie skryptu wynosi 1. Jeśli używasz getopts w funkcji, zadeklaruj w niej local OPTIND. Inaczej drugie wywołanie funkcji zacznie czytać od miejsca, w którym skończyło pierwsze, i nic nie znajdzie.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
getopts "vd:" opcja | — | v przełącznik, d: opcja z wartością |
getopts ":vd:" opcja | : na początku | tryb cichy: sam obsługujesz błędy |
$OPTARG | — | wartość opcji albo litera błędnej opcji |
$OPTIND | — | numer następnego argumentu do przeczytania |
shift $((OPTIND - 1)) | — | zostawia same zwykłe argumenty |
case | \?) | nieznana opcja (w trybie cichym) |
case | :) | brak wartości opcji (w trybie cichym) |
local OPTIND | — | getopts w funkcji bez śmieci z poprzedniego wywołania |
${0##*/} | — | sama nazwa skryptu do komunikatów |
$((10#$n)) | — | liczba dziesiętna, nawet z zerem na początku |
exit | 0 / 2 | pomoc na życzenie / błąd użycia |
getopt | --long | zewnętrzny program GNU z długimi opcjami (nieprzenośny) |
⚠ Typowy błąd
Użytkownik podaje -n 08, bo przecież „osiem dni” można zapisać z zerem, jak w kalendarzu:
$ ./stare.sh -d smietnik -n 08
stare.sh: line 59: ((: 08: value too great for base (error token is "08")
stare.sh: -n musi być co najmniej 1
Spróbuj: stare.sh -hBash w arytmetyce traktuje liczbę z zerem na początku jako liczbę w systemie ósemkowym. A w systemie ósemkowym cyfry 8 nie ma. Jak próba zapłacenia w Biedronce banknotem 8 złotych. Wyrażenie się wywala, (( )) zwraca porażkę i bramkarz wyrzuca użytkownika z kompletnie mylącym komunikatem „co najmniej 1”. Przy -n 07 byłoby jeszcze podstępniej: wszystko by zadziałało, bo 7 ósemkowo to też 7. Błąd czekałby do pierwszego 08 albo 09.
Naprawa: po sprawdzeniu, że są same cyfry, wymuś system dziesiętny: dni=$((10#$dni)). 10# znaczy „czytaj to dziesiętnie”. Ten sam problem trafia skrypty, które biorą godzinę albo dzień miesiąca z date +%H czy date +%d, bo tam zera na początku są zawsze.
✎ Zadanie
Dopisz do skryptu sprawdz.sh z lekcji 35 opcje -v (pokazuje dodatkowo rozmiar i właściciela) i -h (pomoc) przez getopts.
Pokaż rozwiązanie
#!/bin/bash
# sprawdz.sh — mówi, czy ścieżka to plik, folder, czy nic
uzycie() {
echo "Użycie: $0 [-v] [-h] ŚCIEŻKA"
echo " -v pokaż też rozmiar i właściciela"
echo " -h ta pomoc"
}
gadatliwy=0
while getopts ":vh" opcja; do
case "$opcja" in
v) gadatliwy=1 ;;
h) uzycie; exit 0 ;;
'?') echo "Nieznana opcja: -$OPTARG" >&2; uzycie >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -ne 1 ]]; then
uzycie >&2
exit 1
fi
sciezka=$1
if [[ -f "$sciezka" ]]; then
echo "$sciezka to plik"
elif [[ -d "$sciezka" ]]; then
echo "$sciezka to folder"
else
echo "$sciezka nie istnieje"
exit 1
fi
if (( gadatliwy )); then
stat -c 'rozmiar: %s B, właściciel: %U' -- "$sciezka"
fi$ ./sprawdz.sh -v /etc/hostname
/etc/hostname to plik
rozmiar: 3 B, właściciel: root
$ ./sprawdz.sh -x /etc
Nieznana opcja: -x
Użycie: ./sprawdz.sh [-v] [-h] ŚCIEŻKA
-v pokaż też rozmiar i właściciela
-h ta pomocDwie linijki robią tu całą robotę. getopts ":vh" mówi: znam opcje v i h, a dwukropek na początku znaczy „błędy zgłaszam sam, po swojemu”. A shift $((OPTIND - 1)) zdejmuje z taśmy wszystkie przerobione opcje, więc po nim $1 to już ścieżka, a nie -v. Bez tego shift skrypt sprawdzałby, czy istnieje plik o nazwie -v, i uczciwie odpowiadał, że nie istnieje. Technicznie prawda, praktycznie bezużyteczne.
stat -c wyciąga z pliku dokładnie te pola, które chcesz: %s to rozmiar w bajtach, %U to nazwa właściciela. A -- przed ścieżką mówi „tu kończą się opcje”, na wypadek gdyby ktoś nazwał plik -v. Zawsze znajdzie się ktoś taki.
macOS: tamtejszy stat jest z rodziny BSD i zamiast -c ma -f z innymi literkami. Na Linuksie i w kursie używamy wersji GNU.
Co dalej
Nasze skrypty mają już opcje, pomoc i bramkarzy. Ale prędzej czy później któryś i tak zrobi coś dziwnego, a ty nie będziesz wiedział, w której linijce. W następnej lekcji szukamy błędów jak detektyw: bash -n, bash -x, set -x z własnym PS4, ShellCheck na serio i krótki rzut oka na strace, czyli podsłuchiwanie, o co program prosi jądro.