Bash dla każdego

Większość języków programowania ma jakoś zaimplementowane pętle for. Bash nie jest tu wyjątkiem, chociaż składnia jest po Bashowemu trochę udziwniona. Najpierw podam najprostszy przykład sprawdzający wielkość plików w katalogu:

for plik in `ls /home/lukasz/katalog` ; do du -h $plik ; done

Najpierw wymyślamy nazwę zmiennej, do której będziemy podstawiać kolejne wartości podawane przez ls, a potem wykonujemy du -h na każdej jednej z tych wartości. Sztuczka polega na tym, że lista ma być po prostu ciągami znaków oddzielonymi spacją, lub znakiem nowej linii. Może to być więc lista domen zapisana w pliku:

for linia in `cat domeny.txt` ; do echo $linia ; host $linia ; echo '------------------' ; done

Inny przykład: oglądanie plików hosts.

for i in `cat hosty` ; do echo $i; ssh -o PreferredAuthentications=publickey -o PasswordAuthentication=no -l lukasz $i "cat /etc/resolv.conf" ; echo '------------------' ;
done

Dzięki PrefferedAuthentications oraz PasswordAuthentication unikniemy zapytań o hasło i, nawet jeżeli nasza lista hostów będzie bardzo długa, możemy wrócić do komputera po minucie i sktypt zakończy działanie, informując gdzie nie mógł logować się po kluczu.

Linux dla leworęcznych

Nauczyłem się obsługiwać myszkę lewą ręką. Prawa czasem pobolewa mnie po pracy, więc pomyślałem, że czas na zmianę. Wszystko generalnie działa, ale skróty klawiszowe, których Gnome ma coś około osiemdziesięciu i są naprawdę przydatne, są zaprojektowane pod używanie ich lewą ręką. Z pomocą przyszły stare, dobre Xsy. Podawany przeze mnie sposób nie zadziała na Waylandzie.

Najpierw sprawdzam jak wygląda moja klawiatura w Xsach:

xmodmap -pke

Następnie zmapowałem klawisze:

  • NUM7 jako F4,
  • NUM8 jako lewy ALT,
  • NUM4 jako TAB,
  • NUM0 jako WINDOWS,
  • NUM2 jako c,
  • NUM3 jako v.

Zapisałem to sobie w krótkim skrypcie shella, który z czasem będzie pewnie rozbudowywany:

#!/usr/bin/sh
# mapowanie klawiatury dla lewej reki
xmodmap -e "keycode 79 = F4 F4 F4 F4 F4 F4 XF86Switch_VT_4"
xmodmap -e "keycode 90 = Super_L"
xmodmap -e "keycode 88 = c"
xmodmap -e "keycode 89 = v"
xmodmap -e "keycode 80 = Alt_L"
xmodmap -e "keycode 83 = Tab ISO_Left_Tab Tab ISO_Left_Tab"

Po dwóch dniach przyzwyczajania pracuje mi się z tym wspaniale. Lewa ręka jest na myszcze, a prawa na klawiaturze numerycznej. Jednego, czego brakuje to lewego CRTL, którego chcę mieć pod jedynką, ale nie mogę go prawidłowo zmapować. Dopiszę, kiedy mi się to uda.

Mała strona z Pythonem

Ostatnio dłubię sobie stronę do obsługi programu javowego MegaMek. Jest tutaj: https://github.com/seem8/astech. Oczywiście pierwszym problemem było: w jakim pythonowym frameworku to zrobić? Jest Django, jest Flask, jest Bottle, jest Cherrypy, jest Pyramid i jeszcze jakieś 20 działających.

Zauważyłem, że są generalnie dwie kategorie stron internetowych:

  • operujące na dużych ilościach danych,
  • robiące coś fajnego.

Najistotniejsze w pierwszej kategorii jest utrzymanie spójności danych i bardzo szybkie na nich operowanie. Mamy kolekcję rzeczy, albo sklep, albo terminarz, albo bardzo dużo plików, albo coś takiego. I wówczas lepiej sprawdzają się duże frameworki, które pozwalają na tym się właśnie skupić, upraszczające tworzenie formularzy, wyszukiwarek, itp…

W drugiej kategorii ważne jest pozostawienie programiście swobody i generalnie zostawienie go w spokoju. Zauważyłem, że bardzo to lubię. Dlaczego? Bo można po prostu programować w Pythonie i nie przejmować się w ogóle faktem, że będzie to obsługiwane przez przeglądarkę.

Przedstawiam małe demo, które obrazuje powyższe:

#!/usr/bin/env python3

def read_logs(log_file):

  try:
    open(log_file,'r').close()
  except FileNotFoundError:
    open(log_file,'w').close()
    
  with open(log_file,'r') as myfile:
    mylines = myfile.readlines()
    lastlog = mylines[len(mylines)-50 : len(mylines)]
    lastlog.reverse()

    return lastlog

Powiedzmy, że mamy coś takiego. Zwykły kod, który działa wszędzie. Skorzystajmy więc z tej funkcji w programie tekstowym:

#!/usr/bin/env python3

def read_logs(log_file):

  try:
    open(log_file,'r').close()
  except FileNotFoundError:
    open(log_file,'w').close()

  with open(log_file,'r') as myfile:
    mylines = myfile.readlines()
    lastlog = mylines[len(mylines)-50 : len(mylines)]
    lastlog.reverse()

    return lastlog


important_logs = read_logs('journal.txt')

for i in important_logs:
  print(i)

Znowu nic niezwykłego. Wczytałem fragment logów systemowych i wypisałem 50 ostatnich linijek, korzystając z funkcji read_logs. Teraz zróbmy tak samo, ale żeby logi pokazywały się na stronie interenetowej.

#!/usr/bin/env python3

from bottle import template, route, run
def read_logs(log_file):

  try:
    open(log_file,'r').close()
  except FileNotFoundError:
    open(log_file,'w').close()

  with open(log_file,'r') as myfile:
    mylines = myfile.readlines()
    lastlog = mylines[len(mylines)-50 : len(mylines)]
    lastlog.reverse()

    return lastlog

@route('/')
def index_page():
  important_logs = read_logs('journal.txt')
  return template('index_page', logs=important_logs)

run(host='localhost', port=8080)

Tyle Pythona. Bottle używa dekoratorów, żeby określić jaki rodzaj funkcji deklarujemy: get, post, route, czy jeszcze jakiś inny. Jako „parametr” dekoratora wpisujemy ścieżkę, którą wpiszemy do przeglądarki. W tym wypadku po wpisaniu w pasek adresu localhost:8080 pokażą się nasze logi. Template potrzebuje co najmniej jednej informacji: jak ma się nazywać plik z końcówką .tpl, w którym będzie kod html. Oprócz tego możemy przekazać dowolną ilość argumentów, które pozwolą szablonowi na dostęp do naszych zmiennych oraz funkcji. Zmienna jest dla szablonu, a wartość to coś z naszego kodu Pythona, którą może być wszystko: zmienna, funkcja z parametrami, egzemplarz klasy, czy cokolwiek sobie umyślimy.

Musi to być jeszcze zrozumiałe dla przeglądarki, więc mamy do tego, wspomniany w kodzie, osobny plik index_page.tpl:

<html>
<head>
<title>Important Logs</title>
<head>
<body>

% for i in logs:
<p>{{i}}</p>
% end

</body>
</html>

Jest tutaj odrobina magicznych rzeczy:

  • znakiem % rozpoczynamy linijkę zawierającą kod Pythona,
  • na zmiennych w kodzie html operujemy poprzez konstrukcję {{zmienna}},
  • jako, że html ignoruje białe znaki, a Python ich właśnie używa do „kończenia” instrukcji for/if/def/itd…, korzystamy ze słowa kluczowego end, żeby wyjść z pętli.

Ścieżką może to być ‚/’, ‚/login’, albo ‚/read_logs/<log_file>’, co pozwala na przekazywanie argumentów na poziomie hiperłączy. Mógłbym więc rozpisać funkcję index_page w ten sposób:

@route('/read_logs/<log_file>')
def index_page(log_file):
  important_logs = read_logs(log_file)
  return template('index_page', logs=important_logs)

Wówczas łącze do przeglądarki wyglądałoby tak: http://localhost:8080/read_logs/journal.txt. Oczywiście jeżeli pozostawiamy cokolwiek użytkownikowi, zwłaszcza w internecie, trzeba się odpowiednio zabezpieczyć, ale o ciasteczkach i logowaniu napiszę innym razem.

Jeżeli ktoś widział kiedyś kod Django, to zapewne czuje już różnicę. To jest bardzo, bardzo proste. Pozwala mi skupić się na logice programu, który czasem robi dziwne rzeczy i nie stawia mi na drodze plików konfiguracyjnych oraz mnóstwa modułów. W dużym programie prędzej czy później taka swoboda odbija się czkawką (i poczułem to na sobie), ale w małym programie, tak do 1000 wierszy, robi świetną robotę.

Bash i zawijanie linii

Bash ma historię wpisywanych w terminal rzeczy. To bardzo dobrze. Używając strzałek w górę oraz w dół można przeglądać ostatnio wpisywane rzeczy. To również bardzo dobrze. W czym więc problem? Skąd właściwie bash wie, gdzie kończy się nasz prompt, a zaczyna historia? Skąd wie, gdzie umieścić znaki polecenia, które przekracza jedną linię?

Ano może sobie policzyć na której kolumnie terminala kończy się prompt, a na której zaczyna moje pisanie. Problem jest taki, że Bash liczy sobie znaki niedrukowane, na przykład kody kolorów, czy inne tego typu rzeczy. Oczywiście rachunek wychodzi błędnie, ponieważ ilość znaków w PS1 nie równa się tym wypisywanym na terminalu. Wynikiem tego są takie kwiatki:

[ lukasz@GAZEvim .bashrc

Jak temu zaradzić? Obszerne wyjaśnienie problemu jest na stronie Stack Exchange. Ja podam tylko wybrane przeze mnie rozwiązanie, polegające na wyciągnięciu niedrukowanych znaków do zmiennych i opatrzeniu ich numerkami:

green="\001$(tput setaf 2)\002"
blue="\001$(tput setaf 4)\002"
dim="\001$(tput dim)\002"
reset="\001$(tput sgr0)\002"
PS1="$green\u@\h $blue $ \n  $green->$reset  "
export PS1
unset green blue dim reset

Ło matko, co to jest? W zmiennych z kolorami oraz zmiennej resetującej kolor tłumaczymy Bashowi jak krowie na polu, że $(tput setaf 2) ma liczyć jako jeden znak. Czy te zmienne są niezbędne? Nie, ale bez nich czytelność linijki PS1 byłaby niewielka. Nazwy tych zmiennych oraz kolory ustawiane setafem nie mają oczywiście znaczenia. Dzięki nim unikamy znaków niedrukowanych w prompcie i bash wie, gdzie zacząć moje pisanie.

Losowe słowo

Jakiś czas temu pisałem o programiku pass, który, między innymi, potrafi tworzyć losowe hasła. Czasem jednak potrzebuję na szybko jakieś losowe słowo. Na przykład, żeby ustawić je do ciasteczek. Znalazłem taką funkcję dla Basha:

 

losowik()
{
  cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1
}

w pewnym komentarzu na Githubie. Proste i działa, tak jak lubię. Ale co z tym zrobić? Trzeba te cztery linijki dopisać do .bashrc i uruchomić nowy terminal, albo się na chwilę wylogować.

Potem wpisuję do terminala: losowik 22 i mam słowo na 22 alfanumeryczne znaki. Już wyjaśniam o co w tym chodzi:

  • cat wypisuje na standardowym wyjściu zawartość pliku /dev/urandom
  • jako, że nie chcemy go w całości, filtrujemy go „rurką” |
  • tr to taki mieszajnik do znaków; potrafi usuwać podany znak (-d), oraz zastępować go innym (-c); w naszym przypadku zastępuje znaki niealfabetyczne (które również są w urandom), alfabetycznymi,
  • fold skraca każdą linijkę urandoma do 32 znaków (linijki bywają dłuższe),
  • head wyświetla fragment tekstu, w tym przypadku pierwszą linijkę.

Trochę to zawiłe i pewnie da się to zrobić w bardziej elegancki sposób, ale lubię rurkować komendy i potem nieco poprawiać całą składnię pod siebie. Możliwe, że jeszcze to poprawię i dam znać, kiedy wymyślę coś ciekawego.

Porty dla NFS

Krótko i na temat. Dodałem kiedyś wpis o aktówce w Linuksie, ale nie doprecyzowałem w nim wszystkich portów, jakich potrzebuje do komunikacji. Ostatnio potrzebowałem uruchomić sobie NFS na CentOSie i okazało się, że 111 oraz 2049 nie wystarczy. Mam sobie taki plik /etc/sysconfig/nfs, do którego należy dopisać pewne wartości, które chcę mieć na stałe:

MOUNTD_PORT=22222
STATD_PORT=22223
LOCKD_TCPPORT=22224
LOCKD_UDPPORT=22225

Przed wybraniem portu należy oczywiście upewnić się, że nic w systemie tego nie używa. Dla mnie najwygodniej użyć do tego netstat -atpn. Do czego są te porty:

  • MOUNT_PORT jest używany przez rpc.mountd za każdym razem, kiedy klient zamontuje dowolny eksport nfsa,
  • STATD_PORT dla rpc.statd, którego serwer używa do kontroli stanu podłączonych klientów,
  • lockd kontroluje coś związanego z kernelem; jeszcze nie wiem co, ale kiedy się dowiem, to napiszę 🙂 .

W pliki /etc/sysconfig/iptables piszemy więc coś takiego:

# nfs
-A INPUT -s 192.168.1.0/24 -p tcp -m tcp --dport 111 -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p udp -m udp --dport 111 -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p tcp -m tcp --dport 2049 -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p udp -m udp --dport 2049 -j ACCEPT

# nfs-mountd
-A INPUT -s 192.168.1.0/24 -p tcp -m tcp --dport 22222 -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p udp -m udp --dport 22222 -j ACCEPT

# nfs-statd
-A INPUT -s 192.168.1.0/24 -p tcp -m tcp --dport 22223 -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p udp -m udp --dport 22223 -j ACCEPT

# nfs-lockd
-A INPUT -s 192.168.1.0/24 -p tcp -m tcp --dport 22224 -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p udp -m udp --dport 22225 -j ACCEPT

I mamy łączność. 192.168.1.0/24 można poprawić, biorąc pod uwagę, w jakiej podsieci jesteśmy. Można też w ogóle zrezygnować z parametru -s ip.ip.ip.ip, jeżeli mamy dodatkowy firewall na routerze. Ja dla pewności zawsze go dopisuję.

Bottle.py (1)

Pomyślałem sobie, że napiszę o Bottle i o tym, jak zwyczajnie ułatwia życie. Niewiele programów zostało napisanych z myślą o ułatwianiu życia, ale mamy szczęśliwie kilka wyjątków.

Czym się różni wróbelek? Tym, że ma jedną nóżkę bardziej.

W Pythonie frameworki webowe piszą się same. Ludzie tworzą ich pełno, czasem nawet do najbardziej trywialnych zastosowań, jak na przykład tylko i wyłącznie do wysyłania plików na serwer. Pierwszy raz zainteresowałem się nimi bardziej dzięki prelekcji https://www.youtube.com/watch?v=AYjPIMe0BhA. O niektórych z nich wiedziałem, ale kilka było nowością.

Co było ważne dla mnie? Tylko kilka rzeczy:

  • prostota instalacji i minimalna ilość zależności,
  • swoboda kodzenia bez konieczności tworzenia wielu plików i katalogów,
  • musi działać idealnie w Pythonie 3,
  • dokumentacja w archiwum do ściągnięcia, najlepiej w pdf (najczęściej pracuję bez dostępu do internetu).

Bottle spełnia wszystko idealnie: nie ma żadnych zależności i jest po prostu plikiem bottle.py, który trzeba skopiować do katalogu z programem (co bardzo lubię), ewentualnie zainstalować pipem. Dokumentację można ściągnąć jako jeden duży plik pdf.

Do czego to jest?

Jeżeli kiedykolwiek miałe(a)ś stronę na www.republika.pl z dwoma megabajtami na twoje dane w darmowym pakiecie, a menu składało się z apletów javy, możesz pamiętać, że rzeczy wpisywane w pasek adresu przeglądarki rzeczywiście znajdowały się w podanym katalogu na serwerze. HTML działa tak do dzisiaj i nie ma w tym nic złego.

Wyobraźcie sobie jednak, że łącza na stronie nie mają nic wspólnego z plikami na dysku. Co zyskujemy? Ano to, że możemy mieć klasycznie działające podstrony, lub podstrony, które wywołują jakieś funkcje, na przykład sprawdzają ciasteczka, albo włączają subprocesy, albo kasują pliki z dysku, a zaraz potem przekierowują użytkownika na „normalną” stronę z kodem HTML dla przeglądarki. Nie ma tu granic możliwości i za to lubię programowanie.

No to zaczynamy.

Najpierw zajmiemy się przygotowaniem „środowiska programistycznego” i programem Witaj Świecie.

mkdir bottle
cd bottle
wget https://github.com/bottlepy/bottle/raw/master/bottle.py

I skoro instalację mamy za sobą, jedziemy z kodem. Jestem trochę leniwy, więc na początku zajmiemy się przykładowym kodem ze strony głównej Bottle.

from bottle import route, run, template

@route('/hello/<name>')
def index(name):
    return template('Hello {{name}}!', name=name)

run(host='localhost', port=8080)

Bottle składa się z wielu niezależnie od siebie działających funkcji, które zwykle hurtowo importujemy zaraz na początku. Najważniejsze są dla nas właśnie te trzy:

  • route to właśnie podstrony wywołujące funkcje,
  • run to wbudowany miniserwer www,
  • template to szablony stron, czyli html z bardzo prostą obsługą pythonowych zmiennych, warunków, pętli oraz kilku innych rzeczy.

@route(‚/hello/<name>’) to właśnie tego typu podstrona. Oznacza to, że możesz wpisać w pasku przeglądarki twojastrona.pl/hello, żeby skrypt wywołał funkcję i coś zrobił. <name> to parametr fukcji, który trzeba wpisać w pasek adresu, albo zawrzeć w łączu z innego miejsca.

def index(name): to zwykła, pytonowa funkcja.  Można ją nazwać wszystko jedno jak, może pobierać parametry i nie ma co do tego żadnych ograniczeń. W tym przypadku funkcja index wymaga podania parametru, który ustawia sobie pod zmienną name. Jak jej to podać, skoro mamy tylko przeglądarkę? Ano tak: http://localhost:8080/hello/lukasz. 🙂

return template(‚Hello {{name}}!’, name=name)… Tutaj zaczyna się magia. Co zwraca funkcja hello? Jest to szablon strony. ‚Hello {{name}}’ jest po prostu kodem HTML (no, prawie) ze zmienną name ukrytą w nawiasach, żeby można było ją jakoś odróżnić od tagów tabel, czy akapitów.

Dalej jest name=name. Masło maślane, przecież wiadomo. Lewa strona równania to jednak zmienna używana przez szablon strony (który może być osobnym plikiem), a prawa zmienną z kodu Pythona, który właśnie robimy. Nie ma jednak żadnego powodu, żeby nie nazywać ich tak samo. Kod Pythona oraz szablony to osobne przestrzenie nazw, więc łatwiej napisać i potem pamiętać jedną nazwę zmiennej.

Tyle na początek. Następnym razem weźmiemy się za przygotowanie ładnego inaczej (nie jestem stylarzem) kodu html, w który opakujemy nasz pythonowy kod.

Stronka z bottle.py

Potrzebowałem małej strony internetowej do włączania i wyłączania programu w Javie. Okazało się, że wyszło z tego nieco więcej, a wszystko dzięki Pythonowi i Bottle.

Python dobrze się rymuje z html, to wiemy: Django, Flask, Cherrypy, nawet stare Plone daje radę. Przez lata narobiło się wiele mniejszych skryptów, pozwalających na przesyłanie plików, albo wyświetlanie stron. Bottle.py jest czymś więcej: ma szablony stron, obsługuje ciasteczka, przekierowania, itd… a wszystko to w jednym pliku, który wystarczy skopiować do katalogu z programem. Jako, że nie mam wielkiego doświadczenia z pythonowymi stronami internetowymi, wybrałem na początek to, co najprostsze.

W sieci można znaleźć wiele przykładowych Hello World, ale było to dla mnie za mało. Zapraszam więc na githuba: https://github.com/seem8/astech. W kilku kolejnych wpisach rozłożę programik na części i wyjaśnię jak działa.