Skrypt, który przyjmuje polecenia
Po tej lekcji: kursant pisze skrypty, które dostają dane w linijce wywołania (./skrypt.sh ania /etc), używa $1, $#, "$@", $0 i shift i wie, czemu "$@" prawie zawsze wygrywa z $*.
Wstęp
dwa terminale obok siebie. W lewym skrypt pyta „Które konto?”, „Który folder?”, a kursor mruga. W prawym jedna linijka ./kto_to.sh ania i od razu wynik
read z poprzedniej lekcji jest miły, ale ma jedną wadę: ktoś musi siedzieć przy klawiaturze. A skrypty admina najczęściej odpala nie człowiek, tylko budzik, czyli cron, inny skrypt albo narzędzie do automatyzacji. Budzik nie odpowie na pytanie „Jak masz na imię?”. Budzik umie tylko dzwonić.
Wszystkie prawdziwe komendy dostają dane od razu: ls -l /etc, cp a.txt b.txt, useradd ania. Dziś nasze skrypty będą działać dokładnie tak samo.
Argumenty, czyli numerki w kolejce
Wyobraź sobie okienko w urzędzie i kolejkę ludzi z numerkami. Urzędnik nie pyta „Kim pan jest?”. Woła: „Numer jeden!”, „Numer dwa!”. Tak samo skrypt widzi słowa podane po swojej nazwie. Pierwsze słowo siedzi w $1, drugie w $2, trzecie w $3. Fachowo to argumenty pozycyjne, bo liczy się ich pozycja.
#!/bin/bash
# argumenty.sh - pokazuje, co skrypt dostał w linijce
echo "Nazwa skryptu (\$0): $0"
echo "Pierwszy (\$1): $1"
echo "Drugi (\$2): $2"
echo "Ile ich jest (\$#): $#"
echo "Wszystkie (\"\$@\"):"
printf ' [%s]\n' "$@"\$ w tekście już znasz: to dolar dosłownie, żebyśmy widzieli nazwę zmiennej obok jej wartości. printf ' [%s]\n' wypisuje każdy argument w osobnej linijce, w nawiasach kwadratowych, żeby było widać, gdzie się zaczyna i kończy. Więcej o printf będzie w lekcji o ładnym wypisywaniu, na razie traktuj go jak lupę.
./argumenty.sh ania "Jan Kowalski" trzeciNazwa skryptu ($0): ./argumenty.sh
Pierwszy ($1): ania
Drugi ($2): Jan Kowalski
Ile ich jest ($#): 3
Wszystkie ("$@"):
[ania]
[Jan Kowalski]
[trzeci]Zobacz na „Jan Kowalski”. W cudzysłowie to jeden argument, choć ma spację. Koperta działa też tutaj: co jest w jednej kopercie, jest jednym numerkiem w kolejce.
A co, jak nic nie podasz?
Nazwa skryptu ($0): ./argumenty.sh
Pierwszy ($1):
Drugi ($2):
Ile ich jest ($#): 0
Wszystkie ("$@"):
[]Puste pudełka i zero. Bash nie protestuje, nie pyta. Sprawdzanie, czy użytkownik coś podał, to Twoja robota. Za dwa lekcje zrobimy to porządnie. Te puste nawiasy na końcu to żart printf, który wypisuje swój wzór przynajmniej raz, nawet bez danych.
$0, czyli jak się nazywam
$0 to nazwa, pod jaką skrypt został wywołany. Nie to, jak się nazywa plik, tylko to, co wpisałeś:
./argumenty.sh -> $0 = ./argumenty.sh
bash argumenty.sh -> $0 = argumenty.sh
/home/eryk/argumenty.sh -> $0 = /home/eryk/argumenty.shPo co to? Najczęściej do instrukcji obsługi: echo "Użycie: $0 LOGIN". Skrypt sam się przedstawia, nawet jeśli ktoś zmieni mu nazwę. Jak pracownik, który ma imię na plakietce, a nie wytatuowane na czole.
Dziesiąty w kolejce, czyli ${10}
Numerki jednocyfrowe można pisać bez klamr. Od dziesiątego klamry są obowiązkowe:
bash -c 'echo "$10 / ${10}"' x 1 2 3 4 5 6 7 8 9 DZIESIATY10 / DZIESIATY$10 Bash czyta jako $1 i doklejone zero. Pierwszym argumentem była jedynka, więc wyszło „10”. Dopiero ${10} to naprawdę dziesiąty. Klamry z poprzedniej lekcji wracają jak bumerang. A jeśli Twój skrypt ma dziesięć argumentów pozycyjnych, to i tak nikt nie zapamięta, który jest który. Wtedy przydają się opcje typu -d FOLDER, które zrobimy przy getopts.
bash -c 'polecenie' x a b to przy okazji świetny sposób na szybkie testy: słowo po poleceniu idzie do $0, a kolejne do $1, $2 i dalej.
"$@" kontra $*: wszyscy naraz
Czasem chcesz wziąć wszystkie argumenty naraz i przekazać dalej. Są na to dwa zapisy, $@ i $*, i różnica między nimi to klasyczne pytanie z rozmów kwalifikacyjnych.
#!/bin/bash
# gwiazdka_vs_malpa.sh - $* kontra "$@" kontra "$*"
echo 'Bez cudzysłowu $*:'
# shellcheck disable=SC2048,SC2086 # celowo zły przykład
printf ' [%s]\n' $*
echo 'W cudzysłowie "$*":'
printf ' [%s]\n' "$*"
echo 'W cudzysłowie "$@":'
printf ' [%s]\n' "$@"./gwiazdka_vs_malpa.sh "moje notatki.txt" raport.txtBez cudzysłowu $*:
[moje]
[notatki.txt]
[raport.txt]
W cudzysłowie "$*":
[moje notatki.txt raport.txt]
W cudzysłowie "$@":
[moje notatki.txt]
[raport.txt]Porządkujemy na przykładzie listuów. Masz dwie koperty: „moje notatki.txt” i „raport.txt”.
$* bez cudzysłowu rozrywa wszystkie koperty i sypie słowa luzem. Zrobiły się trzy kawałki, a „moje notatki.txt” przestało istnieć. To ta sama mina co w poprzedniej lekcji.
"$*" wkłada wszystko do jednej wielkiej koperty. Jeden argument, słowa sklejone spacjami. Przydaje się, kiedy chcesz zrobić z argumentów jedno zdanie, na przykład do logu.
"$@" przekazuje każdą kopertę osobno i nienaruszoną. Dokładnie tyle argumentów, ile użytkownik podał, dokładnie takich, jakie podał. To jest to, czego chcesz w dziewięćdziesięciu dziewięciu procentach przypadków.
Zasada na karteczkę: przekazujesz argumenty dalej? "$@", z cudzysłowem. Na przykład nakładka na ls, która zawsze pokazuje szczegóły:
#!/bin/bash
# ll.sh - ls -lh z dowolnymi argumentami
ls -lh "$@"Co użytkownik poda, to ls dostanie. Zero, jeden czy dziesięć plików, ze spacjami czy bez.
shift, czyli „następny proszę”
shift to urzędnik, który obsłużył numer jeden i woła „następny”. Pierwszy argument znika, a wszyscy przesuwają się o jedno miejsce do przodu. Dawny $2 staje się $1, dawny $3 staje się $2, a $# maleje o jeden.
#!/bin/bash
# przesun.sh - pokazuje działanie shift
echo "Na początku: $# argumentów, pierwszy to $1"
shift
echo "Po shift: $# argumentów, pierwszy to $1"
shift 2
echo "Po shift 2: $# argumentów, pierwszy to $1"
echo "Zostało: $*"./przesun.sh a b c d eNa początku: 5 argumentów, pierwszy to a
Po shift: 4 argumentów, pierwszy to b
Po shift 2: 2 argumentów, pierwszy to d
Zostało: d eshift 2 przesuwa o dwa. A tutaj $* w cudzysłowie jest w porządku, bo robimy z reszty jedno zdanie do wyświetlenia.
Po co to? Typowy wzorzec: pierwszy argument jest specjalny, a reszta to lista. Na przykład ./wyslij.sh adres@firma.pl plik1 plik2 plik3. Zapisujesz $1 do zmiennej adres, robisz shift i w "$@" masz już same pliki. Czysto, bez liczenia pozycji.
A gdybyś zrobił shift 5, a argumenty są tylko dwa? Nic się nie przesuwa, a shift kończy się kodem 1, czyli kciukiem w dół. Co to dokładnie znaczy, zobaczysz w następnej lekcji.
Skrypt z prawdziwego życia
#!/bin/bash
# kto_to.sh - pokazuje informacje o koncie podanym jako argument
echo "=== Konto: $1 ==="
getent passwd "$1"
id "$1"./kto_to.sh root=== Konto: root ===
root:x:0:0:root:/root:/bin/bash
uid=0(root) gid=0(root) groups=0(root)getent passwd pokazuje linijkę konta z bazy użytkowników, a id jego numer i grupy. Działa i na Ubuntu, i na RHEL-u, i nie obchodzi go, czy konto jest lokalne, czy z serwera katalogowego.
A teraz bez argumentu:
=== Konto: ===
id: '': no such user: No such file or directorySkrypt nie zrozumiał, że czegoś brakuje, tylko przekazał pustkę dalej. id dostał puste słowo i się obraził. To właśnie temat dwóch następnych lekcji: jak skrypt ma się dowiedzieć, że coś poszło źle, i jak ma na to zareagować.
Jak to działa pod spodem
Pamiętasz z lekcji 31, że jądro uruchamia skrypt jako /bin/bash ./skrypt.sh? Teraz pełny obraz: wywołanie ./kto_to.sh root zamienia się w
/bin/bash ./kto_to.sh rootNowy Bash bierze pierwsze słowo po swojej nazwie jako plik do czytania, wpisuje je do $0, a wszystko, co jest dalej, układa w ponumerowaną listę: $1, $2 i tak dalej. $# to po prostu długość tej listy.
Najważniejsze: dzielenie na argumenty robi Twój Bash w terminalu, jeszcze zanim skrypt wystartuje. Dlatego "Jan Kowalski" przyjechało jako jeden argument. Cudzysłowy zostały zdjęte po drodze, skrypt dostał czyste Jan Kowalski w jednym pudełku. Skrypt nigdy nie widzi cudzysłowów, które wpisałeś. Widzi tylko ich efekt.
A "$@" jest wyjątkiem od zasady „cudzysłów robi jedną przesyłkę”. To jedyne miejsce, gdzie cudzysłów daje wiele słów: każdy argument w osobnej kopercie. Bash ma na to specjalną regułę, bo inaczej nie dałoby się bezpiecznie przekazywać argumentów dalej.
Ściąga flag
| Zapis | Co zawiera | Uwagi |
|---|---|---|
$0 | nazwa, pod którą wywołano skrypt | do komunikatów „Użycie: $0 …” |
$1 … $9 | kolejne argumenty | puste, jeśli ich nie podano |
${10} | dziesiąty i dalsze | bez klamr $10 to $1 + „0” |
$# | liczba argumentów | nie liczy $0 |
"$@" | wszystkie, każdy osobno | prawie zawsze ten |
"$*" | wszystkie jako jeden napis | do logów i komunikatów |
$* / $@ bez cudzysłowu | wszystkie, pocięte na słowa | mina, unikaj |
shift | przesuwa argumenty o 1 | $# maleje |
shift N | przesuwa o N | gdy N > $#, nic nie robi i zwraca 1 |
set -- | a b c | ustawia nowe argumenty |
bash -c '…' | x a b | szybki test: $0=x, $1=a, $2=b |
⚠ Typowy błąd
Piszesz nakładkę na kopiowanie:
#!/bin/bash
# zla_kopia.sh - celowo zły przykład
cp $1 $2./zla_kopia.sh "moje notatki.txt" kopia.txtcp: target 'kopia.txt': No such file or directoryDziwny komunikat, prawda? Plik źródłowy istnieje, a cp narzeka na cel, który dopiero miał powstać. Ale $1 bez cudzysłowu zostało pocięte na moje i notatki.txt, więc cp dostał trzy argumenty: moje, notatki.txt i kopia.txt. A kiedy cp dostaje więcej niż dwa, zakłada, że ostatni jest folderem, do którego ma wrzucić resztę. Folderu kopia.txt nie ma. A jeśli plik kopia.txt akurat istnieje, usłyszysz Not a directory. Komunikat jest prawdziwy, tylko odpowiada na zupełnie inne pytanie, niż zadałeś.
Naprawa: cp -- "$1" "$2". Cudzysłowy, wiadomo. A podwójny myślnik mówi cp, że dalej są już tylko nazwy plików, a nie opcje. Bez niego plik nazwany -r zostałby wzięty za opcję. Tak, ktoś kiedyś nazwie tak plik. Zawsze ktoś nazwie.
ShellCheck oczywiście wytknie SC2086 przy obu zmiennych. Ten przykład jest zły specjalnie.
✎ Zadanie
Napisz skrypt przedstaw.sh, który jako pierwszy argument dostaje imię, a dalej dowolną liczbę zainteresowań. Skrypt zapisuje imię do zmiennej, robi shift, a potem wypisuje zdanie z imieniem i liczbą zainteresowań oraz każde zainteresowanie w osobnej linijce, w nawiasach kwadratowych. Wywołaj go tak:
./przedstaw.sh Ania rower "gotowanie zup" Linux„Gotowanie zup” ma się pojawić w jednych nawiasach. Sprawdź ShellCheckiem.
Pokaż rozwiązanie
#!/bin/bash
# przedstaw.sh - imię i lista zainteresowań z argumentów
imie=$1
shift
echo "Nazywam się $imie i mam $# zainteresowania:"
printf ' [%s]\n' "$@"./przedstaw.sh Ania rower "gotowanie zup" LinuxNazywam się Ania i mam 3 zainteresowania:
[rower]
[gotowanie zup]
[Linux]Imię ląduje w pudełku, zanim shift wyrzuci je z kolejki. Odwrotna kolejność to klasyk: najpierw shift, potem imie=$1 i nagle Ania nazywa się „rower”. $# po shift liczy już tylko zainteresowania, a "$@" trzyma „gotowanie zup” w jednej kopercie. Gramatyka „3 zainteresowania” przy pięciu się posypie, ale polska odmiana liczebników to temat na osobny kurs. Dla zaawansowanych.
Co dalej
Nasz kto_to.sh bez argumentu wypisał bzdury i udał, że wszystko gra. W następnej lekcji nauczymy się, jak każda komenda mówi „udało się” albo „nie udało się”: kody wyjścia, $?, exit i dwa magiczne znaczki && i ||.