Go - wypisuję nazwy użytych na blogu tagów
Czas na kolejne ćwiczenie w ramach nauki języka Go i jego biblioteki standardowej. Napiszę program, który wypisze nazwy tagów, jakich dotychczas użyłam na moim blogu wraz z ilością otagowanych w ten sposób wpisów.
Problem - jak wypisać tagi używane na blogu
Podczas pisania każdego kolejnego posta zastanawiam się, jak go otagować. Czy tag, który chcę nadać, już istnieje? Czy pisałam wcześniej o tej dziedzinie? Potrzebuję szbko sprawdzić, jakie tagi nadałam wcześniej moim wpisom.
Nie wiem, czy hugo posiada jakąś opcję wypisywania tagów - jeśli ma, to dobrze ukrytą, bo nie służą do tego:
hugo list [typ]
- to polecenie wyświetla wpisy, których typ to jedna z wartości [all, draft, future, expired],hugo gen
- generuje css, strony podręcznika man czy skrypty autocompletion
Nie widziałam żadnego hugo print tags
…
Jestem pewna, że istnieje jakiś plugin, który to robi. Ale sama też umiem, więc do dzieła!
Wiersz poleceń - rozwiązanie w 15 minut
Spróbowałam najpierw skonstruować odpowiedni pipeline z wiersza poleceń:
|
|
- używam polecenia
ripgrep
(rg
):- bez wypisywania nazw plików (
--no-filename
) - podaję wyrażenie
'tags\s?=\s?\[([^]]*)\]'
, do którego musi się dopasować wiersz w pliku (posiada grupę łapiącą zawartość tablicy z tagami) - z opcją
-r '$1'
która zamieni tekst dopasowany przez wyrażenie na zawartość pierwszej grupy (dzięki temu “wyjmę” pooddzielane przecinkami tagi ze środka tags=[…]) - szukam we wszystkich plikach wewnątrz
content/
- bez wypisywania nazw plików (
- używam fishowego polecenia
string split ","
które podzieli wiersz zawierający przecinki na listę wierszy - używam
tr '"' ' '
- zamieniam podwójne cudzysłowia (hm, może mogą być inne znaki?…) wokół taga na spacje - używam fishowego polecenia
string trim
, które usunie spacje przed i po tagu - sortuję, żeby
- zrobić
uniq -c
- co da mi napis w formacieilość_tagów tag
dla każdego unikalnego taga - sortuję odwrotnie (żeby tagi z największą ilością były u góry)
Czy osiągnęłam to, co chciałam?
Prawie. Jak widać na załączonym obrazku - pipeline przetwarza wszystkie wiersze w plikach, a więc rg
zgarnie nie tylko ten jeden znajdujący się we frontmatter, ale także wszystkie innne, zawierające podane wyrażenie. Chciałabym kończyć przetwarzanie pliku po znalezieniu pierwszego wiersza. Hm, nie mam pomysłu, jak to zrobić w wierszu poleceń.
To chyba dobra okazja, żeby napisać program w go 😄
Program w Go - rozwiązanie w dwa wieczory
Przy okazji pisania tego narządka nauczyłam się kilku interesujących rzeczy na temat języka i biblioteki standardowej.
Czego się nauczyłam
Nazwa binarki
Nie bardzo rozumiałam, skąd bierze się nazwa budowanej binarki - czy to nazwa pliku, w którym jest funckja main
, czy może nazwa bierze się z nazwy modułu, która jest zapisana w go.mod
? W zależności od tego, jak budowałam program w go, uzyskiwałam różne rezultaty:
go build .
budował binarkętools
go build main.go
budował binarkęmain
Odpowiedź znalazłam w dokumentacji na stronie Compile packages and dependencies:
When compiling a single main package, build writes the resulting executable to an output file named after the first source file (‘go build ed.go rx.go’ writes ’ed’ or ’ed.exe’) or the source code directory (‘go build unix/sam’ writes ‘sam’ or ‘sam.exe’).
W go możliwy jest Null Pointer Exception
Operator new nie zaalokuje pamięci na pustą mapę będącą polem wewnątrz struktury. Zwróci tylko wskaźnik do zaalokowanego obiektu struktury, ale pola - tu: jedno pole, mapa - będą zainicjowane wartościami zerowymi odpowiednimi dla ich typów. Dla typów referencyjnych (mapy, tablicy) wartością zerową jest nil
. Zamiast
|
|
co kończy się odpowiednikiem NPE:
należy użyć literału “konstruującego” strukturę (composite litaral):
|
|
Moje typy
Oto moje typy danych:
- Tag reprezentuje pojedynczy tag wraz z ilością wpisów nim oznaczonych:
|
|
- Tags to mapa nazwy taga w strukturę Tag
|
|
Rekurencyjny spacer wśród katalogów - Walk
Spodziewałam się, że w Go istnieje jakiś sposób przechodzenia po strukturze katalogów w systemie plików - jakiś odpowiednik os.walk dla Pythona czy Files.walk dla Javy.
Istnieje: [filepath.Walk]. Nie wiem, czy używam tej funkcji w sposób idiomatyczny. W porównaniu z API Javowym, które generuje mi piękny strumień obiektów Path, tutaj muszę przekazać funkcję - callback, która będzie wołana dla każdego znalezionego pliku.
Funkcja ma typ filepath.WalkFunc
i sygnaturę func(path string, info fs.FileInfo, err error) error
. Chcę, żeby ta funkcja podczas przechodzenia przez wiersze napotkanych plików aktualizowała mój obiekt Tags. Tworzę więc implementację filepath.WalkFunc
jako clojure zawierający Tags przy pomocy mojej funkcji CreateProcessor
:
|
|
Czytanie kolejnych wierszy z pliku
Oto CreateProcessor
zwracająca implementację filepath.WalkFunc
, która
- sprawdza, czy jest to plik z rozszerzeniem
.md
- otwiera go
- używa NewScanner do czytania kolejnych wierszy
- jeśli dopasuje wiersz jako tags:
- wywołuje na nich metodę
Extract
przekazanego w parametrze objektuTagExtractor
.
- wywołuje na nich metodę
|
|
Po dopasowaniu w pliku wiersza nie analizuję już kolejnych wierszy - kończę pętlę skanowania i przechodzę do kolejnego pliku.
Metoda Extract
zwraca wartość bool
oznaczającą, czy było dopasowanie do wiersza tags=[...]
. Jeśli było, jest to sygnał do przerwania pętli iterującej po wierszach w pliku.
|
|
Wyrażenia regularne
Bardzo spodobało mi się API modułu regexp
- dokumentacja tutaj - jest uporządkowane, logiczne i, kiedy się je już pozna, bardzo intuicyjne i łatwe w użyciu. Bardzo możliwe, że tak tylko mi się wydaje, bo nie robię nic skomplikowanego, ale sposób nazywania fukcji w API według pewnego schematu jakoś dobrze rezonuje z potrzebą porządku i logiki w mojej głowie.
Używam tego modułu do przefiltrowania wierszy zaczynających się od tags=
oraz wyjęciu z nich nazw tagów.
Ciekawe:
regexp.MustCompile()
obok regexp.Compile()
to chyba idiom - spotkałam go już w module do templejtów - dzięki któremu można łatwiej używać API:
MustCompile is like Compile but panics if the expression cannot be parsed. It simplifies safe initialization of global variables holding compiled regular expressions.
Tak wygląda implementacja TagExtractor
dla typu Tags
:
|
|
Sortowanie tagów
Po raz pierwszy napisałam implementację interfejsu sort.Interface
, dzięki czemu mogę wypisać swoje taki od najczęstszego do najrzadziej występującego, a tagi występuące równie często wypisuję alfabetycznie.
Nauczyłam się też używania dokumentacji z wiersza poleceń (polecenie go doc
):
|
|
Jeśli dobrze rozumiem, to sort.Sorted()
działa jedynie na strukturze danych, której kolejne elementy są indeksowane liczbą. Nie mogę więc zaimplementować sort.Interface
dla Tags. Potrzebuję nowy typ danych:
|
|
oraz sposób na przekształcenie Tags w posortowany TagA:
|
|
Musi być kolorowo
Nie kombinowałam, tylko dokleiłam sekwencje sterujące, które wyświetlą mi w bashu kolorki. Niezbyt to przenośne.
Formatowanie tekstu z polem o dynamicznie wyliczanej szerokości
W znaczniku szerokości pola formatu należy umieścić nie liczbę, ale gwiazdkę. Wartość zostanie odczytana z kolejnego argumentu - poprzedzającego ten, dla którego pisany jest format.
Przykład
Oto funkcja wypisująca posortowane tagi.
Bardziej poprawnie byłoby, gdybym nie obliczała szerokości pola na podstawie stałej czterocyfrowej, tylko znalazła największą liczbę wśród liczności tagów. Przyznam szczerze, że zajęło mi chwilę, żeby odkryć, że szerokość mojego pola musi uwzględnić również te sekwencje sterujące kolorem…
|
|
Kod źródłowy
Kod go-tags znajduje się na repozytorium na githubie.