Jak wygenerować zalążek postu Jekylla w bashu?

koziolekweb.pl 5 miesięcy temu

Odkąd przemigrowałem z Wordpressa na Jekylla, to na mojej liście rzeczy do zrobienia był punkt dotyczący stworzenia skryptu, który pozwoliłby mi na tworzenie zalążków tekstów dzięki jednej komendy. Co prawda jest jekyll-compose, ale nie spełnia on moich wymagań. Jest zbyt ubogi w stosunku do tego co wygenerowane został w czasie migracji. Wniosek jest jeden. Trzeba stworzyć własne narzędzie. Tak najprościej byłoby wziąć wspomniany wyżej plugin i dopisać potrzebne rzeczy, ale…

Po pierwsze średnio znam rubiego. Po drugie to co chcę osiągnąć jest bliżej moich prywatnych wymagań, niż wymagań ogółu. Padło zatem na starego dobrego basha.

Szablon

Na początku stworzyłem szablon pliku, który będzie wzorcem do generowania:

Listing 1. Szablon

--- id: IDEN title: 'TITLE' date: 'DATET08:37:00+02:00' author: Koziołek layout: post guid: 'https://koziolekweb.pl/?p=IDEN' permalink: PERMALINK categories: - Programowanie tags: - Programowanie ---

Flagi IDEN, TITLE, DATE i PERMALINK chcę zastąpić argumentami czy to wyliczonymi, czy pobranymi z linii poleceń. Całość zapisać w pliku, którego nazwa ma format DATE-TYTUL-KEBAB_CASEM.md. Przy czym tytuł w nazwie pliku oraz w PERMALINK musi zostać pozbawiony polskich znaków, znaków interpunkcyjnych i nawiasów. To oznacza trochę rzeźby, ale damy radę :)

Funkcje pomocnicze

Na początek potrzebujemy kilku funkcji pomocniczych, które zrobią nam robotę ze zmianą znaków w tytule oraz zamienią format daty z yyyy-mm-dd na yyyy/mm/dd. Zacznijmy od tej ostatniej.

Format daty

Tutaj sprawa jest bardzo prosta.

Listing 2. Zamiana formatu daty

replace_dash_with_slash() { local input="$1" echo "$input" | tr '-' '/' }

Bierzemy dowolny tekst i zamieniamy wszystkie znaki - na znaki /.

Znaki interpunkcyjne i nawiasy

Tu sprawa robi się odrobinę bardziej zabawna, ale przez cały czas da się żyć:

Listing 3. Usunięcie znaków interpunkcyjnych i nawiasów

remove_punctuation_and_brackets() { local input="$1" echo "$input" | tr -d '[:punct:](){}[]<>' }

Generalnie polecenie tr uznaje jedynie niewielki podzbiór wyrażeń regularnych. Stąd taki dziwny zapis. Idziemy dalej.

Polskie znaki

I nie tylko polskie. Wszelkie litery spoza zakresu ASCII chcemy zamienić na ich odpowiedniki w ASCII.

Listing 4. Zamiana polskich znaków

to_ascii() { local input="$1" echo "$input" | iconv -f UTF-8 -t ASCII//TRANSLIT }

Polecenie iconv zamieni nam ciąg w UTF-8 na ciąg w ASCII z użyciem transliteracji (TRANSLIT). TransliteracjaW, to proces zamiany znaków jednego alfabetu na inny. Najbardziej znaną transliteracją jest zamiana ; na ; (grecki znak zapytania) w kodzie javascript i patrzenie na frontasie dostają palpitacji :D

Kebab case

Kolejny krok to zamiana tytułu na zapisany dzięki kebab-case – wszystkie litery to małe litery, spacje zastępujemy -

Listing 5. Doner time

to_kebab_case() { local input="$1" echo "$input" | tr '[:upper:]' '[:lower:]' | sed -e 's/ /-/g' -e 's/^-//' -e 's/-$//' }

Tutaj samo tr nam nie wystarczy. O ile zamiana wielkich liter na małe jest prosta to już podmiana spacji może przysporzyć pewnych problemów. o ile przekażemy do funkcji ciąg znaków zaczynający się lub kończący spacją, to ta ostatnia spacja też zostanie zamieniona na -. dzięki sed nie tylko zmieniamy spacje na - ('s/ /-/g'), ale też usuwamy początkowy ('s/^-//') i końcowy ('s/-$//') znak - o ile trzeba.

Identyfikator

W sumie nie jest on potrzebny, ale w czasie migracji pozostał, więc i jego przeniesiemy.

Listing 6. Wyliczamy ID

max_id () { local m_id=0 m_id=$(find ../_posts/ -maxdepth 1 -type f -name "*.md" -exec head -n 2 {} + | awk '/id: [0-9]/ {print $2}' | sort -n | tail -n 1) m_id=$((m_id + 1)) echo $m_id }

Po pierwsze musimy popracować w kontekście opublikowanych artykułów (../_post). Tutaj mam jedno założenie – skrypt będzie siedział w katalogu _drafts. Następnie:

  • szukamy wszystkich plików .md w opublikowanych plikach – find ../_posts/ -maxdepth 1 -type f -name "*.md"
  • bierzemy pierwsze dwie linijki – -exec head -n 2 {} +
  • odsiewamy linię z identyfikatorem – awk '/id: [0-9]/
  • wypisujemy sam identyfikator – '{print $2}'
  • sortujemy numerycznie – sort -n
  • wybieramy ostatni wynik – tail -n 1
  • dodajemy do niego 1 m_id=$((m_id + 1))

Z ciekawości wrzuciłem ten kod do AI i poprosiłem o optymalizację i średnio to wyszło. Dodał tylko sprawdzenia czy aby na pewno nie nadpisaliśmy m_id pustą wartością.

Budujemy adekwatny program

Czas przejść do adekwatnego programu. Będę potrzebował jeszcze jednej funkcji, która zbuduje mi tytuł w formacie przyjaznym przeglądarkom:

Listing 7. Tworzymy tytuł

to_title() { local input="$1" local nopunc=$(remove_punctuation_and_brackets "$input") local kebab=$(to_kebab_case "$nopunc") to_ascii "$kebab" }

Tu właśnie używamy większości funkcji pomocniczych, które stworzyliśmy wcześniej. Kolejny krok to wczytanie szablonu i podmiana odpowiednich elementów:

Listing 8. Generujemy zalążek z szablonu

create_draft() { local input_file="template.md" local url_title="$(to_title "$t_param")" local output_file="$d_optional_param-$url_title.md" local iden=$(max_id) local title="$t_param" local url_date="$(replace_dash_with_slash $d_optional_param)" local permalink="/$(replace_dash_with_slash $d_optional_param)/$(to_title "$t_param")" if [[ ! -f "$input_file" ]]; then echo "Input file does not exist." return 1 fi if [[ -z "$output_file" ]]; then echo "Output file name is not set." return 1 fi local content=$(<"$input_file") content="${content//IDEN/$iden}" content="${content//TITLE/$title}" content="${content//DATE/$d_optional_param}" content="${content//PERMALINK/$permalink}" echo "$content" > "$output_file" echo "Flags replaced and saved to $output_file" }

Teraz jeszcze musimy pobrać argumenty i je obrobić. Będą dwa. Pierwszy to tytuł, a drugi, opcjonalny, to data dzienna publikacji. Domyślna data publikacji to dzisiaj.

Listing 9. Kompletny program

#!/usr/bin/env bash set -eo pipefail # Initialize variables d_optional_param=$(date +%Y-%m-%d) t_param="" max_id () { local m_id=0 m_id=$(find ../_posts/ -maxdepth 1 -type f -name "*.md" -exec head -n 2 {} + | awk '/id: [0-9]/ {print $2}' | sort -n | tail -n 1) m_id=$((m_id + 1)) echo $m_id } to_kebab_case() { local input="$1" echo "$input" | tr '[:upper:]' '[:lower:]' | sed -e 's/ /-/g' -e 's/^-//' -e 's/-$//' } to_ascii() { local input="$1" echo "$input" | iconv -f UTF-8 -t ASCII//TRANSLIT } remove_punctuation_and_brackets() { local input="$1" echo "$input" | tr -d '[:punct:](){}[]<>' } replace_dash_with_slash() { local input="$1" echo "$input" | tr '-' '/' } to_title() { local input="$1" local nopunc=$(remove_punctuation_and_brackets "$input") local kebab=$(to_kebab_case "$nopunc") to_ascii "$kebab" } create_draft() { local input_file="template.md" local url_title=$(to_title "$t_param") local output_file="$d_optional_param-$url_title.md" local iden=$(max_id) local title="$t_param" local url_date=$(replace_dash_with_slash $d_optional_param) local permalink="/$url_date/$url_title" if [[ ! -f "$input_file" ]]; then echo "Input file does not exist." return 1 fi if [[ -z "$output_file" ]]; then echo "Output file name is not set." return 1 fi local content=$(<"$input_file") content="${content//IDEN/$iden}" content="${content//TITLE/$title}" content="${content//DATE/$d_optional_param}" content="${content//PERMALINK/$permalink}" echo "$content" > "$output_file" echo "Flags replaced and saved to $output_file" } usage() { echo "Usage: $0 -d [optional] -t <multiple words>" exit 1 } while [[ "$#" -gt 0 ]]; do case "$1" in -d) shift d_optional_param="${1:-$(date +%Y-%m-%d)}" shift ;; -t) shift if [[ -n "$1" ]]; then t_param="$1" shift while [[ "x-$1" != "x-" && -n "$1" && "$1" != "-"* ]]; do t_param="$t_param $1" shift done else usage fi ;; *) echo "Unknown parameter passed: $1" usage ;; esac done if [[ -z "$t_param" ]]; then usage fi echo "$(create_draft)"

Pominąłem opcję set -u, która przerywa działanie skryptu, gdy zmienna nie jest zainicjowana. Jej ustawienie „psuje” przetwarzanie parametru tytułu o ile ten zawiera nawiasy. Głupie, ale działa. I to jest najważniejsze.

Idź do oryginalnego materiału