2021-12-14 / Bartłomiej Kurek
Od czego zacząć? #4 (shell)

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

Co robisz? Odpowiedź
Wynik

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.

Co robisz? Odpowiedź
!

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.