Pythonowe logowanie

Coś ostatnio dużo o Pythonie, ale jakoś tak wyszło. Jego siła leży w bibliotece standardowej i jednym z modułów się teraz zajmiemy:

import logging
logging.basicConfig(filename="log.txt", \
     format="%(asctime)s - %(levelname)s - %(message)s, \
     level=logging.DEBUG)

# no to coś zalogujmy
logging.info("piszemy do pliku log")

Konstrukcja %(coś)s odpowiada mniej więcej str(coś) i sporo osób tego używa. Dostajemy teraz plik log.txt z napisem 2016-02-08 18:45:01,622 – INFO – piszemy do pliku log. Proste, czytelne i na temat. Załóżmy teraz, że chcielibyśmy zalogować wyjątek. Utworzyłem plik plik.txt i zabrałem sobie prawa do zapisu. Ciekawe, co Python na to wymyśli:

try:
  plik = open("plik.txt", "w")
except:
  err = sys.exc_info() # zapisujemy komunikat błędu do zmienej
  logging.error("Nie można otworzyć pliku plik.txt do zapisu.")
  logging.error(str(err[1].args[0]) + " " + err[1].args[1])
  sys.exit()

Przedostatnia linijka jest dosyć tajemnicza. Python, jak to ma w zwyczaju, podaje komunikat błędu jako zagnieżdżoną listę. Zerowym elementem jest numer błędu, a pierwszym komunikat. Co teraz znajdzie się w log.txt?

2016-02-08 19:02:28,965 – ERROR – Nie można otworzyć pliku plik.txt do zapisu.
2016-02-08 19:02:28,966 – ERROR – 13 Permission denied

Znowu: proste, czytelne i na temat, czyli dokładnie to, co powinien zawierać log.
Na samym początku skryptu, przy funkcji basicConfig można ustawić szczegółowość logowania na poziomy DEBUG, INFO, WARNING, ERROR oraz CRITICAL. Można tu sprawdzić parametry podane przy uruchamianiu skryptu i ustawić jakąś wartość domyślną. Można zresztą wszystko, ale to już oddzielny temat.

GIT to przeszłość…

… przyszłość to zzzver.sh. 🙂

#!/usr/bin/bash
# very basic version control system

# let's check current directory
PWD=$(pwd)

# check flags and do stuff
if [ "$1" = "init" ] ; then
  # initialize repository
  mkdir $PWD/stable
  mkdir $PWD/test
  mkdir $PWD/archive
  touch $PWD/zzzrepo
  echo "$(date): repository initialized" | tee -a $PWD/zzzrepo
  
elif [ "$1" = "diff" ] ; then
  if [ -z $2 ] ; then
    # diff test and stable directories
    diff -d -r $PWD/test/ $PWD/stable/
  else
    # diff test and stable directories with the content of specified archive
    mkdir $PWD/archive/difftemp
    tar -zxvf $PWD/$2 -C $PWD/archive/difftemp
    echo "------ Test ------"
    diff -d -r $PWD/test $PWD/archive/difftemp/test
    echo " "
    echo " ----- Stable ------"
    diff -d -r $PWD/stable $PWD/archive/difftemp/stable
    rm -fr $PWD/archive/difftemp
    echo " "
  fi

elif [ "$1" = "merge" ] ; then
  # overwrite stable with test
  rm -fr $PWD/stable/*
  cp --recursive $PWD/test/* $PWD/stable/
  echo "$(date): merged test into stable" | tee -a $PWD/zzzrepo

elif [ "$1" = "save" ] ; then
  # save test and stable as an archive under a name of date and time
  tar -zcf $PWD/archive/$(date +%F_%T).tar.gz test/ stable/
  echo "$(date): repository archived" | tee -a $PWD/zzzrepo

elif [ "$1" = "load" ] ; then
  if [ -z $3 ] ; then
    # overwrite test and stable with content of the specified archive
    rm -fr $PWD/test
    rm -fr $PWD/stable
    tar -zxf $2 -C $PWD
    echo "$(date): restored backup from $2 archive" | tee -a $PWD/zzzrepo
  elif [ "$3" = "test" ] ; then
    # overwite just test with content of specified archive
    rm -fr $PWD/test 
    tar -k -zxf $2 -C $PWD &> /dev/null
    echo "$(date): restored test repo backup from $2 archive" | tee -a $PWD/zzzrepo
  elif [ "$3" = "stable" ] ; then
    # overwrite just stable with content of specified archive
    rm -fr $PWD/stable
    tar -k -zxf $2 -C $PWD &> /dev/null
    echo "$(date): restored stable repo backup from $2 archive" | tee -a $PWD/zzzrepo
  fi

elif [ -z $1 ] ; then
  # echo some instructions
  echo " "
  echo -e "Please specify:\r
   'init' to create repository,\r
   'diff' to compare test and stable repo,\r
   'diff archive/name.tar.gz' to compare test and stable with specified archive,\r
   'merge' to copy test into stable,\r
   'save' to archive test and stable as date_time.tar.gz,\r
   'load archive/name.tar.gz' to replace test and stable with content of specified archive,\r
   'load archive/name.tar.gz test' to replace test with content of specified archive,\r
   'load archive/name.tar.gz stable' to replace stable with content of specified archive."
   echo " "

else
  echo "I give up, you wrote something wrong. :("
  echo " "

fi

W ramach standardowego bashowego klepania tylko mi potrzebnych rzeczy, napisałem sobie system kontroli wersji. Ja niby wiem, że wszyscy mają GITa (czytaj: Github i Bitbucket), ale chciałem czegoś prostszego. W skrócie, skrypt umie: rozróżniać pomiędzy STABLE i TESTING, archiwizować obydwa powyższe i każdy z osobna, diffować repozytoria oraz archiwa, odtwarzać repo z archiwum. To wszystko, czego potrzebuję. Sprawdza się tutaj stara prawda, że najlepszy program to ten, który sam sobie napiszesz.
Nie wiem do końca czemu zrobiłem go po angielsku. Przypuszczalnie zamierzałem stosować go na wirtualkach bez polskich znaków, ale tak do końca to już nie pamiętam.

Skrypt został troszkę zaktualizowany. Jedna z najlepszych rzeczy, jaką sam dla siebie zrobiłem.

Pobieranie danych z dialoga

Dialog jest świetnym narzędziem, pomagającym tworzyć pseudograficzne elementy skryptów Basha. O ile samo utworzenie okna dialogowego jest dosyć proste, to pobranie wprowadzonych przez użytkownika danych nie jest już takie oczywiste.

Najważniejsze jest to, że dialog przekazuje owe dane na wyjście błędu, więc odczytanie ich nie jest takie trudne. Nie widzimy go bezpośrednio, zanim nie zakończymy naszego skryptu, można jednak przekierować wyjście błędu do pliku.

WYBRANEMENU="/tmp/$0-$USER"
echo "NIC" > $WYBRANEMENU

dialog --title "Okno menu" \
       --backtitle "Teleturniej." \
       --menu "Wybierz jedno z trzech:" \
       15 50 3 \
       1 "Bramka nr 1" \
       2 "Bramka nr 2" \
       3 "Bramka nr 3" \
       2>"${WYBRANEMENU}"

Co osiągamy? Najpierw przygotowyjemy wspomniany plik. Należy zadbać o to, by jego nazwa była unikalna. Jeżeli kilku użytkowników jednocześnie uruchomiłoby skrypt zapisujący dane do jakiegoś pliku, skutki byłyby zupełnie nieprzewidywalne. Przekierowanie „NIC” do pliku ma dwie zalety: po pierwsze nadpisze go, co by tam wcześniej nie było, a po drugie będziemy mieli jakąś wartość domyślną.

Dialog oczekuje od nas kilku paratetrów:

    • –title: tytuł dialogowego okna,
    • –backtitle: napis pojawiający się w górnym lewym rogu terminala,
    • –menu: rodzaj dialogowego okna i tekst w jego środku,
    • 15 50 3: wymiary okna,
    • 1, 2, 3: nazwy możliwych wyborów, do których będziemy odwoływać się później w naszym skrypcie,
    • „Bramka nr 1”: tekst dla użytkownika programu, żeby wiedział co wybiera,

2>”${WYBRANEMENU}”: standardowe wyjście błędu zostaje zapisane w pliku /tmp/$0-$USER, przy czym $0 zostanie w nazwie pliku zastąpione nazwą pliku skryptu, $USER zaś nazwą uruchamiającego go użytkownika.

Pozostaje tylko odczytać zawartość pliku /tmp/plik-użytkownik. Wystarczy zwykły case:

clear
case $(<"${WYBRANEMENU}") in
  1) echo "ZONK! 1";;
  2) echo "ZONK! 2";;
  3) echo "Wygrałeś 50 złotych!!";;
  "") echo "Anuluj? Poważnie? :(" ;;
esac

Poza starożytną magią, związaną ze sposobem, w jaki Bash potrafi odczytywać zawartość pliku kryjącego się pod zmienną, nie ma tu niczego nadzwyczajnego. Najpierw czyścimy ekran, pozbywając się okna dialogowego. Potem decydujemy co zrobić. Z pliku tymczasowego zostanie odczytana wartość podana w oknie dialogowym: 1, 2, lub 3. Moglibyśmy zamiast liczb wpisać co się nam podoba, ale będzie to widoczne dla użytkownika, więc „kupa”, czy „foo” niestety odpadają. Jeżeli użytkownik wybierze Anuluj, do pliku tymczasowego zostanie przekierowane kompletnie nic i trzeba wziąć to pod uwagę w naszym case.

Na koniec pozostało nam jeszcze usunięcie pliku tymczasowego, przy czym ewentualne błędy nie są dla nas najważniejsze.

rm -f $WYBRANEMENU &> /dev/null

Oczywiście zamiast wypisywania tekstu do terminala możemy uruchamiać nowe okna dialogowe, uruchomić system ponownie, lub zmieniać domyślną Javę. Miłej zabawy.