Przejdź do treści
Lekcja 10 z 117

Kurs › Moduł 1 › Lekcja 10

Jak Bash czyta twoją komendę

⏱ ok. 10 min czytania· moduł 1: Praca w powłoce

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 $imie

Chcesz 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:

  1. Czyta linijkę: z klawiatury, ze skryptu albo z napisu po bash -c.
  2. 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.
  3. Składa słowa w komendy: zwykłe, rury, pętle.
  4. Robi rozwinięcia. To ten etap, gdzie *, ~, $zmienna zamieniają się w prawdziwe wartości.
  5. Ustawia przekierowania, czyli >, <, 2>, i wyrzuca je z listy argumentów.
  6. Uruchamia komendę.
  7. 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 ll
ll 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:

  1. Nawiasy klamrowe: {a,b}, {1..3}. Znasz z lekcji 5.
  2. 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)) daje 4,
    • podstawienie komendy: $(date +%Y) wstawia wynik innej komendy.
  3. Dzielenie na słowa: wynik rozwinięć bez cudzysłowu jest cięty na spacjach.
  4. Wzorce: *, ?, [...] zamieniają się na nazwy plików.
  5. 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 3

Jest 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.txt

Linijka 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
*.txt

Bez 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.txt
raport1.txt raport2.txt
lista.txt
lista2.txt
raport1.txt
raport2.txt

Pierwszy 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:

  1. Funkcja: przycisk w windzie, który ktoś sam zaprogramował w tej powłoce.
  2. Komenda wbudowana: coś, co urzędnik umie zrobić sam przy biurku, na przykład cd, echo, type.
  3. 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/echo

PATH 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] test

Funkcja 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: 127

Urzę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: 126

Fachowiec 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
koniec

clone 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

KomendaFlagaCo robi
type(brak)mówi, czym jest nazwa: aliasem, funkcją, wbudowaną czy plikiem
type-apokazuje wszystkich kandydatów w kolejności
type-tjedno słowo: alias, function, builtin, file
aliasnazwa='...'robi skrót
unaliasnazwausuwa skrót
\komendaomija alias
commandkomendaomija aliasy i funkcje
set-x / +xwłącza / wyłącza podgląd komend po rozwinięciach
hash-rczyści pamięć ścieżek programów
echo $?kod wyjścia ostatniej komendy
echo $PATHlista folderów, gdzie Bash szuka programów
strace-f -e trace=execvepokazuje, jakie programy naprawdę startują
Kod wyjściaZnaczenie
0sukces
126plik jest, ale nie da się go uruchomić
127komendy nie znaleziono

⚠ Typowy błąd

Napisałeś sobie skrypt skrypt.sh, jesteś w jego folderze i wpisujesz:

skrypt.sh
bash: skrypt.sh: command not found

Przecież 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}.txt

Potem 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.txt

Pierwsza: 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 grep
cd 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/grep

cd 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.