Zmienne i cudzysłowy
Po tej lekcji: kursant zapisuje dane w zmiennych, wie, kiedy użyć cudzysłowu podwójnego, a kiedy pojedynczego, i umie zapytać użytkownika o dane przez read, także o hasło.
Wstęp
Twój raport z poprzedniej lekcji ma wszystko wpisane na sztywno. To jak formularz w urzędzie, na którym ktoś z góry wpisał „Jan Kowalski”. Działa świetnie, pod warunkiem że jesteś Janem Kowalskim.
Dziś nauczymy skrypt zapamiętywać rzeczy i pytać o nie. A po drodze rozbroimy najczęstszą minę w całym Bashu: brak cudzysłowu. Ta mina wysadziła więcej skryptów niż wszystkie inne błędy razem wzięte.
Zmienna, czyli pudełko z napisem
Wyobraź sobie pudełko po butach. Na boku piszesz markerem „imie”, a do środka wkładasz karteczkę „Eryk”. Kiedy potrzebujesz imienia, nie pamiętasz go, tylko sięgasz do pudełka z napisem „imie”.
To jest zmienna. Nazwa na zewnątrz, zawartość w środku.
imie=Eryk
echo "Cześć, $imie"Cześć, ErykDwie zasady, które musisz wbić sobie w pamięć od razu:
Przy wkładaniu do pudełka piszesz samą nazwę, bez dolara. Przy wyjmowaniu piszesz dolar przed nazwą. imie=Eryk wkłada, $imie wyjmuje.
I żadnych spacji wokół znaku równości. Nigdy. Bash nie jest szkolnym zeszytem od matematyki, gdzie spacje wokół „=” wyglądają ładnie. O tym, co się dzieje, kiedy je dodasz, w typowym błędzie, bo to klasyk.
Nazwy zmiennych: litery, cyfry i podkreślnik, nie mogą zaczynać się od cyfry. Polskie litery w nazwach odpadają, więc imie, a nie imię. I dobra rada: swoje zmienne pisz małymi literami. Wielkimi literami system ma już swoje zmienne, takie jak PATH, HOME czy USER. Jeśli w skrypcie napiszesz PATH=/home/ania/zdjecia, to właśnie wymazałeś Bashowi mapę, gdzie szukać komend, i nagle nie działa nawet ls.
Zmienna, której nie ma, nie powoduje błędu. Bash po prostu wstawia pustkę:
echo "[$nieistnieje]"[]Puste pudełko. Żadnego ostrzeżenia. Brzmi wygodnie, a jest źródłem bardzo ciekawych awarii, do których wrócimy przy set -u. Pudełko wyrzucasz przez unset imie.
Klamry: gdzie kończy się nazwa
plik=raport
echo "$plik_kopia.txt"
echo "${plik}_kopia.txt".txt
raport_kopia.txtCo się stało w pierwszej linijce? Bash czyta nazwę zmiennej tak długo, jak długo widzi litery, cyfry i podkreślniki. Więc szuka pudełka o nazwie plik_kopia. Takiego nie ma, wstawia pustkę, zostaje samo .txt.
Klamry ${plik} mówią wprost: nazwa jest dokładnie tutaj, ani litery dalej. Jak taśma klejąca na pudełku, która pokazuje, gdzie kończy się napis. Przy kropce klamry nie są potrzebne, bo kropki nie ma w nazwach zmiennych, więc $plik.txt da raport.txt. Ale jeśli nie jesteś pewien, dawaj klamry. Nie kosztują nic.
Cudzysłowy, czyli koperty
Teraz najważniejsza część lekcji. Cudzysłów to koperta. To, co włożysz do koperty, Bash przenosi jako jedną przesyłkę. Ale są dwa rodzaje kopert.
Cudzysłów podwójny "..." to koperta z okienkiem. Bash zagląda przez okienko i podmienia zmienne na ich zawartość. Reszty nie rusza.
Apostrof, czyli pojedynczy cudzysłów '...', to koperta zaklejona na głucho. Bash do środka nie zagląda. Co wpisałeś, to dostajesz, znak w znak.
cena=100
echo "Kosztuje $cena zł"
echo 'Kosztuje $cena zł'
echo "Kosztuje \$cena zł"Kosztuje 100 zł
Kosztuje $cena zł
Kosztuje $cena złTrzecia linijka pokazuje trzeci sposób: ukośnik wsteczny przed dolarem. Mówi „ten jeden znak potraktuj dosłownie”. Jak naklejka „nie otwierać” na jednym znaku.
A co z cudzysłowem w środku cudzysłowu? Prosta zasada: w podwójnych możesz trzymać apostrofy, w apostrofach możesz trzymać podwójne.
echo "Ma 'apostrof' w środku"
echo 'Ma "cudzysłów" w środku'Apostrofu wewnątrz apostrofów nie da się wsadzić wcale. Zaklejona koperta to zaklejona koperta. Wtedy używasz podwójnych.
Kiedy który? Apostrofy, kiedy chcesz tekst dokładnie tak, jak go napisałeś: wzorce dla grep, hasła w przykładach, wszystko z dolarami, które mają zostać dolarami. Podwójne, kiedy w środku są zmienne. I tu złota zasada całego kursu: każdą zmienną bierz w podwójny cudzysłów. "$plik", a nie $plik. Zawsze. Chyba że dokładnie wiesz, dlaczego nie.
Dlaczego „zawsze”? Mina z dzieleniem na słowa
nazwa="moje notatki.txt"
touch "$nazwa"
ls -l $nazwa
ls -l "$nazwa"ls: cannot access 'moje': No such file or directory
ls: cannot access 'notatki.txt': No such file or directory
-rw-r--r-- 1 eryk eryk 0 Nov 3 16:29 moje notatki.txtBez koperty Bash wyjął z pudełka „moje notatki.txt”, zobaczył spację i pociął to na dwa słowa. ls dostał dwa osobne pliki do pokazania, żadnego nie ma. W kopercie cała nazwa pojechała jako jedna przesyłka.
Teraz wyobraź sobie, że zamiast ls jest rm. I że w folderze akurat leży plik moje. Właśnie tak powstają historie, które admini opowiadają sobie przy piwie łamiącym się głosem.
I druga mina, jeszcze zabawniejsza:
gwiazdka="*"
echo $gwiazdka
echo "$gwiazdka"a.txt b.txt notatki.md
*Bez cudzysłowu Bash po wyjęciu gwiazdki z pudełka potraktował ją jak wzorzec z lekcji piątego i rozwinął w listę plików. Zamawiałeś gwiazdkę, dostałeś cały folder. Z cudzysłowem gwiazdka została gwiazdką.
To samo z wieloma spacjami: echo $tekst ściśnie dużo spacji do dużo spacji, a echo "$tekst" zostawi wszystkie.
ShellCheck łapie to od ręki:
In zle.sh line 4:
ls -l $nazwa
^----^ SC2086 (info): Double quote to prevent globbing and word splitting.Ten plik jest specjalnie zły, żebyś zobaczył ostrzeżenie. Kiedy ShellCheck mówi SC2086, to nie sugestia, to prośba.
read: skrypt zadaje pytanie
Pudełka możesz napełniać sam, w skrypcie. Ale możesz też poprosić użytkownika, żeby coś do nich włożył. Jak urzędnik, który podsuwa formularz i czeka, aż go wypełnisz.
#!/bin/bash
# powitanie.sh - pyta o dane i wita użytkownika
read -r -p "Jak masz na imię? " imie
read -r -s -p "Podaj hasło (nie będzie widać): " haslo
echo
echo "Cześć, ${imie}!"
echo "Hasło dla testu (nigdy tak nie rób): $haslo"Jak masz na imię? Ania
Podaj hasło (nie będzie widać):
Cześć, Ania!
Hasło dla testu (nigdy tak nie rób): tajne123read czeka, aż użytkownik coś wpisze i wciśnie Enter, a potem wkłada to do pudełka o podanej nazwie. Bez dolara, bo wkładamy, a nie wyjmujemy.
-p "tekst" to pytanie wyświetlane przed kursorem. Bez tego użytkownik patrzy w migający kursor i nie wie, czy komputer na niego czeka, czy się zawiesił.
-s to tryb tajny, od „silent”. Wpisywane znaki nie pojawiają się na ekranie, nawet gwiazdki. Do haseł. Dlatego po nim jest puste echo: Enter wciśnięty w trybie tajnym też się nie wyświetlił i bez tego następny tekst przykleiłby się do pytania.
-r to „raw”, czyli surowo. Bez niego read traktuje ukośniki wsteczne jako znaki specjalne i je zjada:
read sciezka <<< 'C:\Users\ania'
echo "$sciezka"
read -r sciezka <<< 'C:\Users\ania'
echo "$sciezka"C:Usersania
C:\Users\aniaTrzy strzałki <<< podają tekst na wejście zamiast klawiatury, żeby dało się to pokazać bez pisania. Dokładnie omówimy je przy here-stringach. Wniosek: -r dawaj zawsze. ShellCheck i tak się o to upomni (SC2162).
Dwa bonusy. read może napełnić kilka pudełek naraz, dzieli wtedy po spacjach, a ostatnie pudełko dostaje całą resztę:
read -r imie nazwisko <<< "Jan Maria Kowalski"
echo "imie=[$imie] nazwisko=[$nazwisko]"imie=[Jan] nazwisko=[Maria Kowalski]A jeśli nie podasz żadnej nazwy, odpowiedź ląduje w pudełku REPLY.
Jak to działa pod spodem
Zmienne mieszkają w pamięci procesu Basha. Każdy Bash ma swoją szafkę z pudełkami. Pamiętasz z poprzedniej lekcji, że skrypt to osobny proces? Więc zmienne z Twojego terminala nie są widoczne w skrypcie, a zmienne ze skryptu znikają, kiedy się skończy.
imie=Ola
bash -c 'echo "[$imie]"'
export imie
bash -c 'echo "[$imie]"'[]
[Ola]Zwykła zmienna to pudełko w Twojej szafce. export robi z niego zmienną środowiskową, czyli wkłada kopię do paczki, którą każdy proces-dziecko dostaje na start. Kopię, nie oryginał. Dziecko może ją zmienić, ale rodzic tego nie zobaczy. Jak kopia klucza: dorabiasz ją synowi, ale jak on ją zgubi, Twój klucz zostaje w kieszeni.
A kiedy Bash podmienia $imie na zawartość? Zanim uruchomi komendę. Z lekcji dziesiątego wiesz, że Bash najpierw przerabia całą linijkę, a dopiero potem wykonuje. Kolejność jest taka: najpierw rozwija zmienne, potem dzieli wynik na słowa po spacjach, potem rozwija gwiazdki, a na końcu zdejmuje cudzysłowy. Cudzysłów wyłącza dwa środkowe kroki. Dlatego echo $gwiazdka dostało listę plików: zmienna została rozwinięta, a potem gwiazdka trafiła na krok rozwijania wzorców. Program echo nigdy nie zobaczył gwiazdki. Zobaczył już gotową listę.
Ściąga flag
| Komenda / składnia | Flaga / forma | Co robi |
|---|---|---|
nazwa=wartość | bez spacji | wkłada wartość do zmiennej |
$nazwa | — | wyjmuje wartość |
${nazwa} | klamry | wyjmuje wartość, jasno kończąc nazwę |
"..." | podwójny | jedna przesyłka, zmienne są rozwijane |
'...' | pojedynczy | jedna przesyłka, wszystko dosłownie |
\ | przed znakiem | ten jeden znak dosłownie |
read | -r | nie zjada ukośników wstecznych (dawaj zawsze) |
read | -p "tekst" | wyświetla pytanie |
read | -s | nie pokazuje wpisywanych znaków (hasła) |
read | -t N | czeka najwyżej N sekund |
read | -n N | czyta N znaków bez czekania na Enter |
export | nazwa | przekazuje zmienną procesom-dzieciom |
unset | nazwa | usuwa zmienną |
declare | -p nazwa | pokazuje zmienną i jej atrybuty |
⚠ Typowy błąd
imie = Erykbash: imie: command not foundCzemu Bash szuka komendy imie? Bo dla niego spacja oddziela komendę od argumentów. Widzi więc: komenda imie, argumenty = i Eryk. Zupełnie jak ls -l /etc. Takiej komendy nie ma, więc błąd.
A wersja ze spacją tylko po równości?
imie= Erykbash: Eryk: command not foundTo jeszcze sprytniejsze. Bash rozumie to jako „ustaw imie na pusto, ale tylko na czas uruchomienia komendy Eryk”. To ta sama składnia, co LC_ALL=C lscpu z poprzedniej lekcji. Komendy Eryk nie ma. Na razie. Może w przyszłym roku.
ShellCheck od razu ostrzega:
imie = Eryk
^-- SC2283 (error): Remove spaces around = to assign (or use [ ] to compare, or quote '=' if literal).Naprawa: imie=Eryk. Jeśli w wartości ma być spacja, cudzysłów: imie="Jan Maria".
✎ Zadanie
Napisz skrypt wizytowka.sh, który pyta przez read -r -p o Twoje imię i o nazwę folderu. Potem wypisuje powitanie z imieniem, pokazuje zawartość tego folderu (ls) i jego rozmiar (du -sh). Na końcu ma wypisać dosłownie zdanie: Zmienne nazywały się $imie i $folder., z dolarami, a nie z wartościami. Przetestuj na folderze, który ma spację w nazwie, na przykład moje pliki. Jeśli działa, usuń jeden cudzysłów przy ls i zobacz, co się stanie.
Pokaż rozwiązanie
#!/bin/bash
# wizytowka.sh - pyta o imię i folder, pokazuje folder i jego rozmiar
read -r -p "Jak masz na imię? " imie
read -r -p "Który folder pokazać? " folder
echo "Cześć, ${imie}! Oto folder \"$folder\":"
ls "$folder"
du -sh "$folder"
# shellcheck disable=SC2016 # celowo: chcemy wypisać dosłownie $imie
echo 'Zmienne nazywały się $imie i $folder.'Jak masz na imię? Ania
Który folder pokazać? moje pliki
Cześć, Ania! Oto folder "moje pliki":
x
y
4.0K moje pliki
Zmienne nazywały się $imie i $folder.Ostatnie zdanie jest w apostrofach, bo chcemy dolary dosłownie. ShellCheck grzecznie zapyta, czy na pewno (SC2016), więc linijka wyżej jest komentarz # shellcheck disable=SC2016 z wyjaśnieniem. To sposób, żeby powiedzieć: „Wiem, co robię, nie zaczepiaj”. Używaj oszczędnie, jak zwolnienia lekarskiego.
A po usunięciu cudzysłowu przy ls:
ls: cannot access 'moje': No such file or directory
ls: cannot access 'pliki': No such file or directoryNożyczki Basha w akcji. Teraz już wiesz, skąd się biorą.
Co dalej
read jest miłe, ale skrypt, który ciągle o coś pyta, nie nadaje się do automatu ani do crona. Prawdziwe komendy dostają dane od razu w linijce: ls -l /etc. W następnej lekcji nauczymy nasze skrypty przyjmować argumenty: $1, $#, "$@" i shift.