Jak Bash czyta twoją komendę
Po tej lekcji: kursant wie, w jakiej kolejności Bash przerabia linijkę (słowa, aliasy, rozwinięcia, przekierowania), jak szuka komendy (funkcja, wbudowana, PATH) i co znaczy kod 127, więc dziwne błędy przestają być magią.
Wstęp
imie="Jan Kowalski"
touch $imieChcesz zrobić plik Jan Kowalski. Robisz ls i masz dwa pliki: Jan i Kowalski. Dopisujesz cudzysłów i nagle działa. Dlaczego? Nikt Ci nie powie, „bo tak”.
Albo: wpisujesz ehco cos, literówka, i dostajesz command not found. W skrypcie kod 127. Skąd akurat 127? Dlaczego nie 1?
Dziś zaglądamy urzędnikowi przez ramię. Zobaczysz, co Bash robi z Twoją linijką od naciśnięcia Entera do startu programu. Po tej lekcji połowa „magicznych” błędów w skryptach staje się oczywista.
Taśma w urzędzie: siedem stanowisk
Wyobraź sobie, że Twój wniosek nie trafia do jednego urzędnika, tylko przechodzi przez kilka stanowisk w ustalonej kolejności. Jedno rozcina kartkę na słowa. Drugie sprawdza skróty. Trzecie podmienia wszystkie „wpisać tutaj” na prawdziwe dane. Dopiero ostatnie wysyła wniosek do wykonania.
Podręcznik Basha, czyli oficjalna instrukcja GNU, opisuje to tak:
- Czyta linijkę: z klawiatury, ze skryptu albo z napisu po
bash -c. - Dzieli ją na słowa i operatory. Słowa oddzielają spacje i znaki specjalne, takie jak
|,>,;,&. Tutaj też działają cudzysłowy: to, co w kopercie, jest jednym kawałkiem. I tu podmieniane są aliasy. - Składa słowa w komendy: zwykłe, rury, pętle.
- Robi rozwinięcia. To ten etap, gdzie
*,~,$zmiennazamieniają się w prawdziwe wartości. - Ustawia przekierowania, czyli
>,<,2>, i wyrzuca je z listy argumentów. - Uruchamia komendę.
- Czeka, aż się skończy, i zapisuje kod wyjścia.
taśma z siedmioma stanowiskami, po taśmie jedzie kartka ls *.txt > lista
Aliasy: pieczątki ze skrótem
Alias to pieczątka. Zamiast pisać cały długi tekst, przybijasz krótką, a urzędnik na stanowisku drugim wie, co pod nią jest.
alias ll='ls -alF'
type llll is aliased to `ls -alF'Na Ubuntu kilka pieczątek dostajesz od razu, w pliku ~/.bashrc, na przykład alias ls='ls --color=auto'. Stąd kolorki w ls. Sprawdź u siebie type ls. [RHEL] Na Red Hacie też zobaczysz alias, tylko ustawiony gdzie indziej.
Ważne: alias działa tylko na pierwszym słowie komendy i tylko w terminalu, w którym piszesz. W skryptach aliasy są domyślnie wyłączone. A jak chcesz ominąć pieczątkę i dostać gołe ls, dopisz odwrotny ukośnik: \ls. Szczegóły aliasów, czyli jak je zapisać na stałe, w lekcji o ustawieniach Basha.
Rozwinięcia: kolejność ma znaczenie
To najważniejsza część lekcji. Bash ma kilka rodzajów rozwinięć i robi je w ustalonej kolejności. Podręcznik mówi:
- Nawiasy klamrowe:
{a,b},{1..3}. Znasz z lekcji 5. - Potem razem, w jednym przejściu od lewej do prawej:
- tylda:
~zamienia się w Twój folder domowy, - zmienne:
$HOME,$USER,$imie, - arytmetyka:
$((2+2))daje4, - podstawienie komendy:
$(date +%Y)wstawia wynik innej komendy.
- tylda:
- Dzielenie na słowa: wynik rozwinięć bez cudzysłowu jest cięty na spacjach.
- Wzorce:
*,?,[...]zamieniają się na nazwy plików. - Usuwanie cudzysłowów: znikają koperty, zostaje sama treść.
Wszystko naraz w jednej linijce:
x=3
echo ~ $USER $((2+2)) $(date +%Y) {a,b} *.txt "$x"/home/eryk eryk 4 2026 a b raport1.txt raport2.txt 3Jest sposób, żeby podejrzeć, co Bash naprawdę uruchamia. set -x każe mu wypisywać każdą komendę po rozwinięciach, z plusem na początku:
set -x
echo {1..3} ~ $((x*2)) *.txt
set +x+ echo 1 2 3 /home/eryk 6 raport1.txt raport2.txt
1 2 3 /home/eryk 6 raport1.txt raport2.txtLinijka z plusem to dokładnie to, co dostaje echo. Ani gwiazdki, ani dolara, ani klamry. echo w ogóle nie wie, że coś takiego wpisałeś. To ten sam trik co z gwiazdką w lekcji 5, tylko teraz widać go dla wszystkich rozwinięć naraz. set -x to najlepszy przyjaciel kogoś, kto debuguje skrypt o drugiej w nocy.
Dowód, że kolejność istnieje
a=1
echo {$a..3}Spodziewasz się 1 2 3, prawda?
{1..3}Klamry są pierwsze. W chwili, gdy Bash je sprawdza, w środku nie ma jeszcze jedynki, tylko napis $a. A {$a..3} to nie jest poprawny zakres, więc Bash zostawia klamry w spokoju. Dopiero potem, krok dalej, podmienia $a na 1. Za późno. Pociąg z klamrami już odjechał. Do zakresów ze zmienną użyjesz pętli, przyjdzie na nie czas w module o skryptach.
Dzielenie na słowa, czyli zagadka z haka
imie="Jan Kowalski"
printf '[%s]\n' $imie
printf '[%s]\n' "$imie"[Jan]
[Kowalski]
[Jan Kowalski]printf z [%s]\n drukuje każdy argument w osobnych nawiasach, więc widać, ile kawałków dostał. Bez cudzysłowu Bash podmienił zmienną, a potem rozciął wynik na spacji: dwa argumenty. W cudzysłowie zmienna jest w kopercie. Koperta przechodzi przez stanowisko z nożyczkami nienaruszona: jeden argument.
Dlatego touch $imie zrobiło dwa pliki. I dlatego zasada numer jeden w Bashu brzmi: zmienne zawsze w podwójnym cudzysłowie. "$imie", nie $imie. Wrócimy do tego w lekcji o zmiennych i cudzysłowach, ale zapamiętaj już teraz.
To samo z gwiazdką ukrytą w zmiennej:
v='*.txt'
echo $v
echo "$v"raport1.txt raport2.txt
*.txtBez cudzysłowu zawartość zmiennej przeszła jeszcze przez stanowisko z wzorcami i gwiazdka zamieniła się w pliki. W cudzysłowie została gwiazdką.
Przekierowania przychodzą na końcu
Przekierowania z lekcji 6 Bash robi po rozwinięciach, tuż przed uruchomieniem. Widać to po takim teście:
echo * > lista.txt
cat lista.txt
ls > lista2.txt
cat lista2.txtraport1.txt raport2.txt
lista.txt
lista2.txt
raport1.txt
raport2.txtPierwszy przypadek: gwiazdka rozwinęła się, zanim powstał lista.txt, więc go nie ma na liście. Drugi: Bash najpierw założył lista2.txt, a dopiero potem uruchomił ls. A ls widzi już wszystko, łącznie z plikiem, do którego właśnie pisze. Jak fotograf, który robi zdjęcie grupowe i sam wskakuje w kadr.
Gdzie Bash szuka komendy
Rozwinięcia gotowe, pierwsze słowo to nazwa komendy. Teraz urzędnik musi ustalić, kto ma to zrobić. Sprawdza w tej kolejności:
- Funkcja: przycisk w windzie, który ktoś sam zaprogramował w tej powłoce.
- Komenda wbudowana: coś, co urzędnik umie zrobić sam przy biurku, na przykład
cd,echo,type. - Program na dysku: Bash przegląda po kolei foldery ze zmiennej
PATH.
Wcześniej, na etapie słów, był jeszcze alias. Więc pełna kolejka to: alias, funkcja, wbudowana, PATH. Wyjątek: jeśli nazwa ma w sobie ukośnik, jak ./skrypt.sh albo /usr/bin/ls, Bash nie szuka niczego, tylko idzie pod wskazany adres.
echo $PATH
type -a echo/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echoPATH to lista folderów oddzielonych dwukropkami. Lista adresów, pod którymi urzędnik szuka fachowców. type -a pokazuje wszystkich kandydatów, a wygrywa pierwszy. echo jest wbudowany, więc program /usr/bin/echo czeka w kolejce i nigdy nie dostanie roboty. Dwie ścieżki do tego samego pliku to znowu drogowskaz /bin z lekcji 8.
Kolejność widać, kiedy zrobisz funkcję o tej samej nazwie:
echo() { builtin echo "[funkcja] $@"; }
echo test
unset -f echo[funkcja] testFunkcja wygrała z komendą wbudowaną. Tak, możesz w ten sposób zepsuć sobie echo. Nie rób tego koledze, kiedy odejdzie od komputera. A przynajmniej nie na produkcji.
Kod 127: nie ma takiego fachowca
ehco cos
echo "kod: $?"bash: ehco: command not found
kod: 127Urzędnik przeszukał wszystko: aliasy, funkcje, swoje umiejętności, wszystkie adresy z PATH. Nikogo. Zwraca 127. To umówiony numer i oznacza dokładnie jedno: komendy nie znaleziono. $? to kod wyjścia ostatniej komendy, kciuk w górę albo w dół. Zero znaczy „udało się”, cokolwiek innego to problem. Przyjrzymy mu się dokładnie w module o skryptach.
[Ubuntu] Na Ubuntu zamiast suchego komunikatu często zobaczysz podpowiedź od pakietu command-not-found, w stylu Command 'ehco' not found, did you mean: command 'echo' from deb coreutils. To funkcja command_not_found_handle, którą Bash woła, zanim się podda. [RHEL] Na minimalnym RHEL-u zwykle dostaniesz tylko krótkie command not found.
Kuzyn 127 to kod 126: plik znaleziony, ale nie da się go uruchomić.
printf '#!/bin/bash\necho hej\n' > skrypt.sh
./skrypt.sh
echo "kod: $?"bash: ./skrypt.sh: Permission denied
kod: 126Fachowiec jest, ale nie ma przepustki. Po chmod +x skrypt.sh działa. Ściągawka dla skryptów: 127 to „nie znalazłem”, 126 to „znalazłem, ale nie wolno”.
Jak to działa pod spodem: fork i exec
Bash uruchamia program w dwóch ruchach. Najpierw fork: robi kopię samego siebie, jak urzędnik, który klonuje się na chwilę. Potem ten klon robi exec: zamienia się w zupełnie inny program, na przykład w ls. Oryginał czeka przy okienku, aż klon skończy, i odbiera od niego kod wyjścia.
Da się to podejrzeć narzędziem strace, które pokazuje rozmowy programu z jądrem:
strace -f -e trace=clone,execve,wait4 bash -c 'ls raport1.txt; echo koniec'execve("/usr/bin/bash", ["bash", "-c", "ls raport1.txt; echo koniec"], ...) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 6017
[pid 6017] execve("/usr/bin/ls", ["ls", "raport1.txt"], ...) = 0
raport1.txt
[pid 6017] +++ exited with 0 +++
<... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 6017
koniecclone to współczesna wersja forka. Proces 6017 robi execve i staje się ls. Kończy z zerem, Bash odbiera wynik przez wait4. A echo koniec? Żadnego klonowania. echo jest wbudowany, urzędnik zrobił to sam.
I tu odpowiedź na pytanie, czemu cd musi być wbudowane. Gdyby cd było osobnym programem, zmieniłoby folder klonowi. Klon by skończył pracę i zniknął, a Ty zostałbyś tam, gdzie byłeś. Jak wysłanie kolegi, żeby za Ciebie poszedł na siłownię.
strace zainstalujesz przez sudo apt install strace [Ubuntu] albo sudo dnf install strace [RHEL]. O procesach, forkach i exec będzie cała lekcja w module o procesach.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
type | (brak) | mówi, czym jest nazwa: aliasem, funkcją, wbudowaną czy plikiem |
type | -a | pokazuje wszystkich kandydatów w kolejności |
type | -t | jedno słowo: alias, function, builtin, file |
alias | nazwa='...' | robi skrót |
unalias | nazwa | usuwa skrót |
\komenda | omija alias | |
command | komenda | omija aliasy i funkcje |
set | -x / +x | włącza / wyłącza podgląd komend po rozwinięciach |
hash | -r | czyści pamięć ścieżek programów |
echo $? | kod wyjścia ostatniej komendy | |
echo $PATH | lista folderów, gdzie Bash szuka programów | |
strace | -f -e trace=execve | pokazuje, jakie programy naprawdę startują |
| Kod wyjścia | Znaczenie |
|---|---|
0 | sukces |
126 | plik jest, ale nie da się go uruchomić |
127 | komendy nie znaleziono |
⚠ Typowy błąd
Napisałeś sobie skrypt skrypt.sh, jesteś w jego folderze i wpisujesz:
skrypt.shbash: skrypt.sh: command not foundPrzecież plik tu leży! Ale nazwa nie ma ukośnika, więc Bash szuka jej tylko w PATH. A bieżącego folderu w PATH nie ma. Celowo. Gdyby był, ktoś mógłby podrzucić do /tmp plik o nazwie ls i czekać, aż admin wejdzie do /tmp i odruchowo wpisze ls. Pułapka na niedźwiedzia, tylko na admina.
Rozwiązanie: powiedz, gdzie leży plik. ./skrypt.sh, czyli „ten z bieżącego folderu”. Kropka i ukośnik to adres, a z adresem Bash nie szuka, tylko idzie.
✎ Zadanie
Bez uruchamiania najpierw zgadnij, a potem sprawdź z set -x, co dokładnie dostanie echo w każdej z tych linijek:
echo ~/{a,b}
echo "$HOME" '$HOME'
echo $((3*4)) plik{1..2}.txtPotem sprawdź type -a dla cd, ls, ll i grep. Które są wbudowane, które to programy, a które aliasy?
Pokaż rozwiązanie
set -x
echo ~/{a,b}
echo "$HOME" '$HOME'
echo $((3*4)) plik{1..2}.txt
set +x+ echo /home/eryk/a /home/eryk/b
/home/eryk/a /home/eryk/b
+ echo /home/eryk '$HOME'
/home/eryk $HOME
+ echo 12 plik1.txt plik2.txt
12 plik1.txt plik2.txtPierwsza: klamry robią dwa słowa ~/a i ~/b, a potem tylda w każdym zamienia się w folder domowy. Druga: w podwójnym cudzysłowie zmienna się rozwija, w pojedynczym nie, bo pojedynczy to koperta zaklejona na amen. Trzecia: arytmetyka i klamry naraz. U Ciebie zamiast /home/eryk będzie Twój folder domowy, a jeśli ćwiczysz jako root, to /root. O tym porozmawiamy jak dorośli w lekcji o sudo.
Druga część:
type -a cd ls ll grepcd is a shell builtin
ls is aliased to `ls --color=auto'
ls is /usr/bin/ls
ls is /bin/ls
ll is aliased to `ls -alF'
grep is aliased to `grep --color=auto'
grep is /usr/bin/grep
grep is /bin/grepcd wbudowane, bo jak pamiętasz, inaczej nie da się zmienić folderu. ls i grep to programy przykryte aliasem z kolorkami. ll to sam alias, programu ll nie ma nigdzie. To jest wynik z Ubuntu. Na innym systemie lista aliasów może być inna, i to też jest ciekawa informacja.
Co dalej
Wiesz już, jak urzędnik czyta wnioski. Czas samemu napisać jakiś dłuższy. W następnej lekcji pierwszy edytor tekstu w terminalu: nano. Taki, z którego da się wyjść bez wyciągania wtyczki z gniazdka.