--- title: "Ping! reloaded" date: 2022-03-30T13:10:03+02:00 tags: ["ping", "terminale"] draft: false --- *Modificato il 22 febbraio 2024* Ho voluto cimentarmi con la linea di comando approfittando dell'occasione di dare una mano a Lucio Bragagnolo, guardate le note biografiche su [Quickloox](https://macintelligence.org), nel far migrare il suo blog su [Hugo](https://gohugo.io). Dietro c'è anche un'operazione nostalgia, ossia la voglia di recuperare l'aspetto dell'ormai abbandanato [Octopress](http://octopress.org) e un notevole numero di post a suo tempo pubblicati in una rubrica che si chiamava Ping! e ospitata da *Macworld* edizione italiana. 1. Problema 1: come convertire circa 3500 file di testo con un nome astruso e senza suffisso? 2. Problema 2: come fare per renderli digeribili a Hugo e inserirli in un blog già esistente? I file sono suddivisi cartelle e sottocartelle e il nome file è preceduto da tre caratteri e uno spazio. Cartelle, sottocartelle e i tre caratteri seguono un ordine alfabetico e non, per es., un ordine cronologico. Inoltre i nomi file contengono altri spazi e apostrofi. Per normallizzare i file dei post è necessario: - Eliminare i primi 3 caratteri e lo spazio - Eliminare gli apostrofi e sostituire gli spazi dal nome file con trattini - Aggiungere l'estensione.md e un prefisso del tipo YYYY-MM-DD- - La data del prefisso dovrebbe essere quella della creazione del file originale per poter mantenere la cronologia - Costruire il front matter dei post Per ottenere tutto questo mi sono prefissato di usare la linea di comando e comandi nativi su macOS. Per esempio non ho installato *rename* che è presente su altri SO \*nix, ma ho sfruttato *mv*. Per ottenere la data dei file è stato utile *stat*, che non solo estrae ciò che serve, ma ne permette anche la formattazione. E poi l'intramontabile e veloce *sed*. Il tutto condito con dei cicli *for*. Alcune cose si sarebbero potute ottenere probabilmente anche con *awk*, che mi ha sempre affascinato, ma non mi ci sono mai dedicato molto. **ATTENZIONE: shell zsh, gnu sed e gnu awk necessari** - Nella situazione di partenza i post erano annidati in sottocartelle con una gerarchia molto particolare. Per portare tutti file in un'unica cartella, per comodità chiamiamola ping, ho usato questo script ```sh find . -not -type d -print0 | xargs -0J % mv -f % . ; find . -type d -depth -print0 | xargs -0 rm -rf ``` - Il nome dei file comincia con tre lettere e uno spazio, che servivano per l'ordinamento gerarchico precedente, ma ora i primi 3 non ci servono più. Il comando seleziona i caratteri dall'inizio del nome fino al primo spazio e rinomina il file eliminandoli. Cambiando il pattern di ricerca in sed è possibile cambiare ogni parte del nome del file. **Nota bene**: questo script è rieditato. ```sh #!/bin/bash for file in *; do if [ -f "$file" ]; then # Check if the item is a file new_name=$(echo "$file" | gsed 's/^.\{4\}//') # Remove the first 4 characters if [ ! -e "$new_name" ]; then # Check if the new name already exists mv "$file" "$new_name" # Rename the file else echo "Error: File '$new_name' already exists. Skipping renaming of '$file'." fi else echo "Skipping '$file': Not a regular file." fi done ``` - Alcuni dei post più vecchi hanno un formato Mac OS Roman, con a capo *CR*, mentre ora vogliamo *LF*. ```sh mac2unix -k * ``` mac2unix è presente in dos2unix. L'opzione -k preserva i metadati di creazione del file. - Per rimuovere gli apostrofi ho selezionato i file con il nome contenente un apostrofo e rinominato con *mv*. ```sh for file in *; do # Remove leading and trailing single quotes new_name="${file%%'\*}" # Remove trailing quotes new_name="${new_name##'\*}" # Remove leading quotes # Handle existing files and renaming errors if [[ -e "$new_name" ]]; then echo "Error: File '$new_name' already exists." else mv -- "$file" "$new_name" || echo "Error renaming '$file' to '$new_name'." fi done ``` - Il nome del file dovrebbe, idealmente essere formato con un prefisso corrispondente alla data di creazione o pubblicazione; *stat* estrae la data di creazione del file e la formatta secondo lo schema voluto. Ma c'è un grosso *ma*. Non è detto che la data estratta dai metadati sia esatta, in quanto nei passaggi e nei salvataggi i metadati si possono essere modificati. Comunque pensiamo a una situazione favorevole e usiamo il seguente script. ```sh for file in *; do mv -- "$file" "`stat -f %SB-%n -t %Y-%m-%d`$file"; done ``` Ma questo script mostra qualche grave pecca per come stat viene usato nel ciclo for. La prima è che ho dimenticato di codificare esplicitamente quali file dare in pasto a stat, che così gira a vuoto e mette una data di creazione uguale per tutti i file, ovvero **1970-01-01**, che è come dire che tutti sono stati creati al tempo 0. Il secondo errore è costituito dal non aver correttamente applicato l'espansione della variabile risultante dal comando stat. La versione emendata è: ```sh for f in *; do mv -- "$f" "$(stat -f %SB-%n -t %Y-%m-%d $f)$f"; done ``` O in una versione più robusta ```sh #!/bin/zsh for file in *; do if [ -f "$file" ]; then # Check if the item is a file timestamp=$(stat -f "%SB" -t "%Y-%m-%d" "$file") # Get the modification timestamp new_name="${timestamp}-${file}" # Create the new file name if [ ! -e "$new_name" ]; then # Check if the new name already exists mv -- "$file" "$new_name" # Rename the file else echo "Error: File '$new_name' already exists. Skipping renaming of '$file'." fi else echo "Skipping '$file': Not a regular file." fi done ``` Ancora una nota, il flag B, birth date dell'inode, è valido solo in macOs, altri sistemi *nix non hanno quel metadato. + Se la procedura precedente dovesse fallire, si potrebbe optare per una soluzione che passa per [R](https://www.r-project.org). In poche parole ho fatto web scraping su [WaybackMachine](https://web.archive.org), salvato titolo e data di pubblicazione dei post trovati - non sono sicuramente tutti - in un file trovati.csv, che fantasia! Ho preparato uno script e lo ho chiamato ledate.sh, anche qui grande sforzo di fantasia. ```sh #!/opt/homebrew/bin/zsh # setopt extendedglob for t in **/^ledate*; do pubb=$(gawk -F ";" '$1=="'"$t"'" {print $2}' ../trovati.csv) gsed -i.bu "1a date: $pubb" $t done unsetopt extendedglob ``` La prima riga abilita una delle opzioni di zsh così da poter utilizzare la sintassi di esclusione di alcuni file dall'elaborazione dello script, in questo caso ledate.sh che è nella stessa cartella dei post. gsed -i.bu crea dei file di backup se qualcosa va storto con gsed. Io preferisco avere la cartella dove lavoro inizializzata a git e fare dei commit frequenti, così posso fare dei passi indietro. + Se volessimo sfruttare un'altra delle capacità di hugo potremmo modificare il file hugo.toml così ```toml [frontmatter] date = [':filename', ':default'] ``` In questo modo non è necessario inserire le date nel frontmatter, ma viene ricavato dal nome del file con questo formato **YYYY-MM-DD-titolo**. Questo risultato si ottiene eseguendo lo script seguente ```sh #!/opt/homebrew/bin/zsh # Percorso della directory dei file directory="/percorso/della/directory/dei/file" # Percorso del file che contiene l'elenco parziale dei nomi dei file e le date elenco="/percorso/del/file/trovati.csv" # Itera attraverso ogni riga del file elenco while IFS= read -r line; do # Divide la riga in nome file e data filename=$(echo "$line" | awk -F";" '{print $1}') date=$(echo "$line" | awk -F";" '{print $2}') # Crea il nuovo nome del file new_filename="$date-$filename" # Verifica se il file esiste nella directory e rinominalo if [[ -e "$directory/$filename" ]]; then mv "$directory/$filename" "$directory/$new_filename" echo "Il file $filename è stato rinominato in $new_filename" else echo "Il file $filename non esiste nella directory" fi done < "$elenco" ``` - Sistemare il front matter dei post in formato yaml. - Dare un nome di campo al titolo. ```sh gsed -i '1 s/./title: &/' * ``` - Inserire i delimitatori yaml ```sh gsed -i '1i ---' * gsed -i '4a ---' * ``` - Aggiungere la data sotto il titolo, da utilizzarsi **solo se la data è stata ricavata dai metadati del file**. ```sh for file in *; do if [ -f "$file" ]; then stat_output=$(stat -f %SB -t %Y-%m-%d "$file") gsed -i "2idate: \\$stat_output" "$file" fi done ``` Notare l'uso dei doppi apici in questo caso per espandere le variabili. - Per riconoscere questi antichi file ho aggiunto una categoria. ```sh gsed -i '3a categories: [“ping“]' * ``` - Aggiungere l'estensione.md con lo script seguente solo se si è utilizzato il file csv. ```sh for file in **/^ledate*; do mv -- "$file" "${file}.md"; done ``` Se dovesse comparire un messaggio di errore ricordarsi di riattivare *setopt extendedglob*. Altrimenti usare questo script ```sh for file in ./path/to/directory/*ledate*; do if [[ -e "${file}.md" ]]; then read -p "File '${file}.md' already exists. Overwrite? (y/N) " -r answer if [[ $answer =~ ^[Yy]$ ]]; then mv -- "$file" "${file}.md" fi else mv -- "$file" "${file}.md" fi done ``` - Adesso gli spazi tra le parole che compongono il nome del file vanno sostituiti con trattini ```sh for f in *; do # Remove leading/trailing spaces new_name="${f## }" # Remove trailing spaces new_name="${new_name%% }" # Remove leading spaces # Replace remaining spaces with hyphens new_name="${new_name// /-}"; # Handle existing files and renaming errors if [[ -e "$new_name" ]]; then echo "Error: File '$new_name' already exists." else mv -- "$f" "$new_name" || echo "Error renaming '$f' to '$new_name'." fi done ``` C'è ancora un problemuccio di codifica dei caratteri per i file che furono scritti con MacOs Roman. La soluzione c'è e si chiama textutil, di serie in Darwin di macOs. In alternativa c'è iconv, ma prevede un file nuovo di output che poi dovrebbe essere rinominato. Uno script di esempio ```sh #!/bin/zsh # Define source encoding and target encoding source_encoding="MacOsRoman" target_encoding="UTF-8" # Iterate through files in the current directory for file in *; do # Check if it's a file and skip hidden files if [[ -f "$file" && ! -d "$file" && "$file" != .?* ]]; then # Check if the file needs conversion current_encoding=$(textutil -stdout -encoding -file "$file") if [[ "$current_encoding" == "$source_encoding" ]]; then # Convert the file textutil -stdout -encoding "$target_encoding" -file "$file" > "$file.new" if [[ $? -eq 0 ]]; then # Replace the original file if conversion was successful mv "$file.new" "$file" echo "Converted '$file' to UTF-8" else echo "Error converting '$file': $?" fi else echo "Skipping '$file' as it's already in $current_encoding" fi fi done ``` Troppi passaggi? Sicuramente poteva essere fatto in modo più elegante, per esempio creando delle variabili e mettendo tutto in uno script. In questo modo ho imparato, applicato e controllato passo a passo cosa fare e sono soddisfatto. Per ora.