Ping! reloaded

Keywords: #ping #terminale

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, nel far migrare il suo blog su Hugo. Dietro c’è anche un’operazione nostalgia, ossia la voglia di recuperare l’aspetto dell’ormai abbandanato Octopress 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
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.

#!/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.
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.
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.
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 è:

for f in *; do
mv -- "$f" "$(stat -f %SB-%n -t %Y-%m-%d $f)$f";
done

O in una versione più robusta

#!/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. In poche parole ho fatto web scraping su WaybackMachine, 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.
#!/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ì
[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

#!/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.
gsed -i '1 s/./title: &/' *
  • Inserire i delimitatori yaml
gsed -i '1i ---' *
gsed -i '4a ---' *
  • Aggiungere la data sotto il titolo, da utilizzarsi solo se la data è stata ricavata dai metadati del file.
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.
gsed -i '3a categories: [“ping“]' *
  • Aggiungere l’estensione.md con lo script seguente solo se si è utilizzato il file csv.
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

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
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

#!/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.