Jak wygenerować zalążek postu Jekylla w bashu?
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 za pomocą 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 nadal 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 za pomocą 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. Jeżeli przekażemy do funkcji ciąg znaków zaczynający się lub kończący spacją, to ta ostatnia spacja też zostanie zamieniona na -
. Za pomocą sed
nie tylko zmieniamy spacje na -
('s/ /-/g'
), ale też usuwamy początkowy ('s/^-//'
) i końcowy ('s/-$//'
) znak -
jeżeli 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 właściwy program
Czas przejść do właściwego 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 jeżeli ten zawiera nawiasy. Głupie, ale działa. I to jest najważniejsze.