Shell
Oczywiście najważniejszym narzędziem w Linux/Unix jest shell.
Parafrazując "w moim IDE nie ma, więc się nie da" z części 3 napiszę tutaj tezę właściwą:
jeśli czegoś nie ma w shellu, to albo jest w C, albo się nie da.
Przede wszystkim: czym jest shell?
Shell to powłoka umożliwiająca nam komunikację z systemem operacyjnym oraz faktyczne wykorzystanie jego możliwości. Wielu ludziom shell kojarzy się z jakimś niefunkcjonalnym oknem, w którym "siermiężnie klepiemy komendy".
Kiedyś implementacje shell były prostsze, nie zawierały przydatnych funkcji nakierowanych na przyjemną, interaktywną pracę. Ale to było dawno. Wprawdzie nadal bazowy shell ma być w systemie jako /bin/sh
i dla wsparcia skryptów ma to być shell zgodny ze standardem POSIX, ale już wykorzystany w sesji interaktywnej nie musi być tak "okrojony". Po to powstały shelle "nowsze", interaktywne, jak choćby Bash.
Bash jak i inne shelle mają mnóstwo interaktywnych funkcji ukrytych pod skrótami klawiszowymi. Trzeba poznać te skróty klawiszowe (domyślnie takie, jak w emacs).
Video
Najpierw 2 krótkie video.
Video: MP4, 173K, 1920x1080. Duration: 00:00:35 Link
Bash kill-ring:
Video: MP4, 111K, 1920x1080. Duration: 00:00:20 Link
Interaktywność w konsoli
Identyczne zachowanie i możliwości edycji mamy w większości programów konsolowych.
Działa to w sqlite3, python, psql i we wszystkich programach implementujących swoją linię komend przy użyciu biblioteki GNU Readline (Initial release 1989; 32 years ago), lub posiadających jej kod.
Przykładowo psql:
$ ldd /usr/lib/postgresql/14/bin/psql | grep readline
libreadline.so.8 => /lib/x86_64-linux-gnu/libreadline.so.8 (0x00007f743305d000)
SQLite3:
$ ldd $(command -v sqlite3 ) | grep readline
libreadline.so.8 => /lib/x86_64-linux-gnu/libreadline.so.8 (0x00007ffb15a1d000)
Python posiada modułu readline, którego możemy użyć we własnych programach jeśli budujemy swoją linię komend. Uzyskamy w naszym programie tę samą funkcjonalność, łącznie z obsługą historii (strzałka w górę, strzałka w dół, zapis/odczyt pliku historii).
Są jednak programy, w których to nie działa. Z tych, które przychodzą mi aktualnie do głowy:
- redis-cli - implementuje swoją linię poleceń we własnym zakresie i wyświetla kolorowe podpowiedzi
- sqlplus - klient bazy Oracle. Dawniej nie działało, pewnie nadal nie działa. Powodem jest zapewne kwestia licencji - GNU Readline
jest na licencji GNU GPL (Generic Public License). W Oracle widocznie nie potrzebują tej funkcjonalności, albo nie ma biblioteki na innej licencji, której mogliby użyć w swoim programie.
Forum Oracle, rok 2000.
Istnieje program rlwrap (można go znaleźć w repozytoriach dystrybucji Linux), który czasem może pomóc. Mimo wszystko - nie zawsze to obejście jednak zadziała.
Jak zacząć?
Często ludzie dziwią się, że można "szaleć" lub "śmigać" w shellu, "pisać tak szybko", itd. Rzecz polega na tym, aby właśnie pisać jak najmniej, a wspomagać się interaktywnością i wbudowanymi udogodnieniami. Niekiedy można zaobserwować doświadczonych użytkowników, którzy w celu przeskoczenia o kilka/kilkanaście znaków trzymają klawisz strzałki i obserwują jak kursor przeskakuje literka po literce, cyferka po cyferce.
Jak zatem zacząć "śmigać" w shellu?
Uruchomić terminal i sprawdzić skróty:
- C-a - na początek
- C-e - na koniec (end)
- C-f - na następny znak (forward)
- C-b - na poprzedni znak (backward)
- M-f - o słowo w przód (forward-word)
- M-b - o słowo wstecz (backward-word)
- C-d - delete
- M-d - delete word (wewnątrz słowa lub przed nim)
- M-Backspace - kasuje poprzednie słowo (delete word backwards)
- M-u - uppercase word
- M-l - lowercase word
- M-c - capitalize word
- M-t - zamiana sąsiednich słów miejscami (transpose words)
- C-k - kasuje do końca linii (kill line)
- C-y - wklej (yank-pop ze schowka kill-ring)
- M-y - w trybie wklejania - kolejny element ze schowka
- M--, M-1, M-2... - argumenty
- C-r - reverse search (inkrementalnie)
Możemy wykonywać operacje na znakach, słowach, liniach. Można wykonać lowercase-word, uppercase-word, zamienić słowa miejscami (transpose), capitalize, wielokrotnie wycinać, wklejać, powielać znaki, itd. Przeskakiwać możemy na początek, na koniec, o słowo w przód, w tył, kasować słowo w przód i w tył. Skasowane słowa (kill) trafiają do schowka (kill-ring). Kill-ring jest listą, więc można wkleić ostatnie wycięcie lub dowolne z poprzednich, nic nie ginie. Dokładnie jak w Emacs, skróty klawiszowe dokładnie te same. Kill-ring w Emacs jest listą dwukierunkową, tam zatem można kręcić się po wcześniej wyciętych fragmentach w przód i w tył. W bashu tylko cyklicznie w tył (a przynajmniej ja nie znam rozwiązania, choć szukałem). Programy w shellu możemy zawieszać i wznawiać. Bash implementuje job-control
. Przechowuje więc listę programów działających w tle sesji, możemy je wyświelać (jobs
, jobs -l
). Możemy wracać do konkretnej komendy (bg, fg), możemy też użyć tych funkcji w skryptach/aliasach. Przykład jednego z moich własnych aliasów:
alias k9l='jobs -p | tail -1 | xargs kill -9'
W skrócie: kill 9 last. Bardzo użyteczny alias przy pracy nad programami wielowątkowymi/asynchronicznymi. Kiedy coś jeszcze nie działa, zablokuje się, przerwanie Ctrl+c nie działa i nie ma żadnej nadziei... wtedy zawieszam taki program (klawisze Ctrl+z) i... k9l [←].
Do tego wszystkiego dochodzą oczywiście strzałki, tabulator.
Pomijam tutaj standardowe rzeczy jak tylda zamiast pełnego /home/<user>
, czy autouzupełnianie ("completion"). Warto jednak wspomnieć, że pakiet bash-completion pozwala na dopełnianie przełączników komend (np. git), nazw hostów z /etc/hosts, czy choćby targetów w Makefile
Siermiężnie w shellu pracuje się wtedy, kiedy zamiast go odkryć, traktuje się go jak okienko, gdzie wpisujemy literki. Oczywiście musimy znać skróty klawiszowe. W tym miejscu czytelnika odsyłam do dokumentacji (man bash
), a najlepiej do emacs tutorial (uruchamiamy Emacs, wciskamy: Ctrl+h t
("kontrolha a później te"), emacs interaktywnie prowadzi nas przez skróty i podstawowe operacje na załadowanym buforze z tekstem tutoriala).
Jeśli opanujemy sprawną nawigację w shellu, to być może wcześniej pojawi się motywacja by odkryć jeszcze więcej. A jest wiele do odkrywania.
Warto też zaglądnąć we flagi
. W bash wpisujemy set
i wciskamy tabulator:
$ set
allexport errtrace history monitor nolog physical verbose
braceexpand functrace ignoreeof noclobber notify pipefail vi
emacs hashall interactive-comments noexec nounset posix xtrace
errexit histexpand keyword noglob onecmd privileged
Jeśli zaczniemy czytać dokumentację dotyczącą tych flag, być może zaczniemy odkrywać potencjał shella i poznawać różne "ślaczki" i "maczki", których zaprezentowanie w takim artykule, jak ten, często odstrasza lub nudzi.
Przykłady narzędzi shellowych (krótkie zadania)
Zadanie:
Mam malutki projekt w pythonie, taki "publiczny", kilka plików *.py
.
$ tree
.
├── docs
│ ├── backend
│ │ ├── 1.txt
│ │ └── 2.txt
│ ├── bugs.md
│ └── ideas
│ └── wishlist.md
├── README.md
└── src
├── mylib
│ ├── bar.py
│ ├── foo.py
│ └── __pycache__
│ └── foo.pyc
└── __pycache__
└── main.pyc
7 directories, 9 files
Chcę móc pisać dokumentację w jednym katalogu "docs" w plikach *.txt
, *.md
i innego typu . Chciałbym te pliki umieścić jako stronę www i wygenerować indeks (html) z linkami do nich. Chcę kopiować na serwer cały katalog, gdzie serwer www wyświetli już ten index.html. Aha, jeśli się da, to niech te pliki w "docs/" będą posortowane według kryterium daty ostatniej modyfikacji. Będzie wtedy od razu widać co się ostatnio w projekcie dzieje. Jeśli do tego będą te daty wyświetlone, to w ogóle super. Pracuję nad tym kodem bezpośrednio w tym drzewie katalogów, więc są tam też katalogi __pycache__/
. Niech to rozwiązanie je pomija. Niech pomija też puste katalogi. Nie musi być "ładnie", byle działało i robiło co ma robić.
Zadanie
Do tego rozwiązania dodatkowo chciałbym mieć jeszcze index tych plików w xml, albo json, taki "sitemap" z datami i rozmiarami plików.
Nie musi w tym być sortowania, bo to dla botów/automatów jest.
Co robisz? Odpowiedź
XML, JSON
Zadanie
Super. A teraz chciałbym jeszcze żeby to się generowało i wysyłało od razu na serwer jedną komendą. Niech się generuje i wgrywa, wpiszę tylko hasło do konta na serwerze i już. Da się? I co wpisać?
Co robisz? Odpowiedź: make
Zadanie
Potrzebuję jeszcze automatyczne backupy przed generowaniem. Niech robi katalog "backups" i niech tam umieszcza archiwa ZIP z datą w nazwie, a przy generowaniu indeksów niech pomija te backupy.
Co robisz? Odpowiedź
Zadanie:
Super. To mam ten projekt i blog w jednym. Jest już sporo plików, niektóre z nich są skompresowane (te backupy).
Potrzebuję wyszukać teraz wszystkie pliki i backupy, które zawierają ciąg 0xdeadbeef
bez rozróżniania wielkości znaków ("0xdeadbeef", "0xDEADBEEF", itd). Jeśli się da, to niech dla każdego znalezionego fragmentu wypisze kontekst 5 linii przed i 7 po znalezionym ciągu.
Zadanie
A teraz uważaj: potrzebuję, żeby te indeksy regenerowały się automatycznie kiedy w katalogu "docs" albo "src" cokolwiek zmienię/dodam/usunę. W projektach JavaScript ludzie mają takie "watchery", ale nie chcę rozwijac własnego narzędzia, ściągać pakietów npm, utrzymywać kodu, itd. Da się coś takiego zrobić?
Co robisz? Odpowiedź
Zadanie
A wiesz... te katalogi __pycache__
są trochę problematyczne. Wiem, że mogę ustawić zmienną PYTHONPYCACHEPREFIX
na jakiś inny katalog, albo użyć opcji -B
any ten bajtkod się nie generował, ale ciągle o tym zapominam... One są zawsze w src/
. Jest jakaś komenda do tego?
Co robisz? Odpowiedź
Zadanie
No teraz to prawie już wszystko. Bo widzisz... to jest tak proste i wygodne, że ja sobie ten projekt/blog piszę na bieżąco. Coś napiszę i od razu wrzucam na serwer. A czasem mi się coś przypomni albo muszę poprawić literówkę. To dalej jest wygodne, ale zawsze robi te backupy automatycznie. Mógłbyś dodać to usuwanie __pycache__
do tego Makefile, a do backupów coś, co będzie usuwało archiwa starsze niż 1h? Oczywiście tylko wtedy jak zrobi nowy backup!
Co robisz? Odpowiedź
Zadanie
Chciałbym jeszcze lokalny serwer www... A w ogóle to mam nowy mini projekt i chciałbym mieć w nim to samo.
Odpowiedź: blog.mk
Blog.mk
blog.mk powyżej to trywialny przykład na rozwiązywanie "problemów" najprostszymi metodami. Jest trywialny, ale robi co ma robić.
Można go wrzucić do dowolnego nowego katalogu i najzwyczajniej w tym katalogu tworzyć podkatalogi i pliki. Program tree robi za nas tutaj wszystko. Można dorzucić inny styl css, pozmieniać opcje (daty, sortowania, itd.). Posiadając konto na jakimś serwerze z zainstalowaną usługą www można tę treść łatwo udostępnić. Daleko temu do zwyczajowego bloga, ale można to rozwinąć. Przykładowo - aby dodać obsługę markdown można użyć programu pandoc, który przetwarza markdown na html, pozwala dołączyć style, rozszerzenia, itd. Blog.codeasap.pl powstaje w bardzo podobny sposób - każdy post to pojedynczy plik index.md, a całość generowana jest krótkim skryptem w Python, który używa modułów markdown i jinja2. Całość wrzucam na serwer używając po prostu rsync, treść jest statyczna, nie ma za tym żadnej aplikacji. Do większego bloga utrzymanego w duchu minimalizmu można użyć programów Hugo, czy Jekyll, które opierają się na podobnej idei, ale są znacznie bogatsze w funkcje i rozszerzenia.
Czy tak prosta rzecz jak "blog.mk" ma jakieś sensowne zastosowanie? Być może tak, być może nie... Dla zachowania prostoty może się czasem przydać. A skoro komukolwiek coś polecam, to nie może być tak, że sam tego nie używam: https://storage.codeasap.pl/blog
Podsumowanie
Shell to niesamowite środowisko pracy, które pozwala nam na pisanie programów małych i dużych przy użyciu drobnych pojedynczych narzędzi. Ten artykuł ma na celu jedynie zachęcić czytelnika do eksploracji, przedstawić inny punkt widzenia. W shellu można praktycznie wszystko, bo musi się dać wszystko zrobić. Oczywiście shell to też cały język i obsługa interfejsów systemu operacyjnego. Te zagadnienia będą się po prostu pojawiać w treści kolejnych artykułów przy okazji omówień różnych problemów i rozwiązań.
Shella nie trzeba się bać, a można wiele w nim odkryć i wiele dzięki niemu zrozumieć.
Przykładowo: możemy uczyć się wyabstrahowanych systemów jak np. kontenery Docker, ale możemy też odkrywać czym tak naprawdę kontenery są i odkryć np. bocker (Docker implemented in around 100 lines of bash.)
Pamiętać jednak należy, że w świecie systemów uniksowych "obowiązuje" standard POSIX, dzięki któremu systemy uniksowe są ze sobą kompatybilne a programy mogą być łatwo przenoszone z jednego na drugi - przy drobnych zmianach, a czasem nawet bez zmian. Dlatego w skryptach shellowych lepiej kierować się standardem POSIX i w miarę możliwości pisać skrypty #!/bin/sh
. Skrypty uruchamiamy w różnych środowiskach: codzienny laptop, serwer, router, telefon, systemy automatyzacji, itp. Każdy z nich może mieć rożny shell, a to co zapewnia kompatybilność, to standard POSIX. Do codziennej pracy, możemy jednak zapomnieć o ograniczeniach i korzystać z wszelkich rozszerzeń i udogodnień danego shella.