Script per tutti i giorni: entra in scena awk


Tastiera A.W.K., Viscount Instruments.

Nelle prime tre puntate di questa serie abbiamo imparato a scrivere uno script in bash per trasformare una stringa di testo in modo che segua delle convenzioni ben determinate a priori (qui i link alla prima, seconda e terza puntata).

In questo caso particolare, la stringa risultante dalla trasformazione deve essere scritta tutta in minuscolo e non deve contenere apostrofi o altri caratteri speciali, a parte il trattino usato come separatore di parole. L’idea è quella di usare questa stringa, insieme alla data di pubblicazione del post, per dare un nome standard e facilmente rintracciabile al file Markdown che contiene il testo del post stesso, utilizzando il formato YYYY-MM-DD-titolo-del-post.md, dove YYYY indica l’anno, MM il mese e DD il giorno di pubblicazione.

L’approccio seguito finora è utile per imparare i fondamenti della programmazione in bash, ma manca decisamente di praticità. Partire dal titolo e dalla data di pubblicazione presenti nell’intestazione del post per arrivare al nome completo del file richiede un certo lavoro di copia e incolla fra l’editor, il Finder e il Terminale, e gli errori sono sempre in agguato.

Quello che ci vuole è uno script che renda il processo completamente automatico, rinominando il file Markdown a partire dal contenuto del documento stesso.1

Si potrebbe benissimo fare anche con bash, ma perché complicarsi la vita quando c’è uno strumento fatto apposta per analizzare ed estrarre dei dati dai file di testo?

AWK, questo sconosciuto

AWK è un linguaggio di programmazione interpretato sviluppato negli anni ’70 presso i Bell Labs, il famosissimo centro di ricerca americano che ci ha dato il transistor, il laser, Unix e il C (e moltissimo altro). AWK è stato sviluppato da Alfred Aho, Peter Weinberger e Brian Kernighan, tre grandi studiosi di computer science, fra i quali spicca il terzo, coautore insieme a Dennis Ritchie di The C Programming Language, il volume di riferimento sul linguaggio C, noto anche semplicemente come “K&R” dai cognomi dei due autori.

Alla base di AWK c’è l’idea di elaborare un file di testo una riga alla volta, controllando che la riga in esame soddisfi una o più condizioni prestabilite ed eseguendo le azioni programmate per ciascuna di queste condizioni

    condizione #1: {azione #1}
    condizione #2: {azione #2}
    ...
    condizione #N: {azione #N}   

Per AWK un file di testo è costituito da un certo numero di righe (record), ciascuna delle quali è suddivisa in uno o più campi (field), separati fra loro da un separatore di campo, che di default è lo spazio ma che può essere modificato a piacere. Per riferirsi a ciascun campo AWK usa le variabili speciali $1, $2 e così via, dove il numero indica la posizione del campo all’interno della riga. La variabile $0 contiene l’intera riga corrente.

Tutto qui? Più o meno. C’è (quasi) solo da aggiungere che AWK prevede due azioni speciali opzionali, racchiuse fra i blocchi BEGIN {...}, END {...}, che servono per eseguire le operazioni preliminari e conclusive necessarie per il buon funzionamento del programma, ad esempio per definire il separatore di campo. Un programma completo in AWK ha quindi una struttura di questo tipo,

    BEGIN { istruzione
            istruzione
            ....
          }

    condizione #1: { istruzione
                     istruzione
                     ....
                   }
    condizione #2: { istruzione
                     istruzione
                     ....
                   }

    ...
    condizione #N: { istruzione
                     istruzione
                     istruzione
                     ....
                   }

     END { istruzione
           istruzione
           ....
         }

dove come già detto i blocchi BEGIN {...}, END {...} sono opzionali.

Nonostante la fama dei suoi autori, AWK è poco utilizzato, molto meno di quanto meriterebbe, i programmatori del mondo UNIX preferiscono usare strumenti più semplici ma meno potenti come sed (che abbiamo già incontrato nelle puntate precedenti) o il declinante perl, un linguaggio di manipolazione di file di testo perfetto per i programmatori che mitizzano Tafazzi.

AWK invece ha il giusto equilibrio di potenza e semplicità d’uso ed è uno strumento perfetto se gli si chiede di fare quello per cui è stato ideato, elaborare informazioni strutturate contenute in file di testo.

Per fortuna AWK è installato di default in macOS e in Linux, oltre che in tutti i sistemi operativi basati su UNIX che si trovano in giro, per cui per provarlo basta lanciare il Terminale ed eseguire il comando awk. In realtà ci sono in giro almeno due versioni diverse di AWK. In macOS è installato awk liscio, la versione del linguaggio definita dai tre autori originali nel volume The AWK programming language. Su Linux, invece, si trova in genere gawk, una implementazione del linguaggio della Free Software Foundation pienamente compatibile con awk, a cui sono state aggiunte alcune estensioni piuttosto utili. Installare gawk su macOS è facile per chi usa Homebrew, dal Terminale

$ brew install gawk

Da ora in poi per semplicità farò cadere la distinzione fatta finora fra AWK, il linguaggio di programmazione, e awk, l’interprete del linguaggio, usando sempre e solo il termine awk per riferirmi ad entrambi.

Un programma banale in awk

Per avere una idea di cosa può fare awk, ecco un piccolissimo (e rozzo) programma di esempio,

BEGIN { FS = ","
        OFS = ", "
      }

$1 ~ /Violanda/ { $1 = "Jolanda" }
                { print $2, $1, $3, $6, $4, $5 }

che applicato ad un elenco di indirizzi come questo,

NOME,COGNOME,INDIRIZZO,LOCALITA,PROVINCIA,CAP
Massino,Nardini,Via Roma 8,Grana,AT,14031
Primo,Sabbatini,Via C. Cattaneo 50,Cala di Volpe,SS,07020
Giuseppe,Marino,Via C. Alberto 75,Barni,CO,22030
Ivano,Costa,Via Guantai Nuovi 29,Ischia,NA,80077
Margherita,Davide,Via Nuova Agnano 83,Rufina,FI,50068
Daphne,Lettiere,Via R. Conforti 67,Castel Di Ieri,AQ,67020
Violanda,Lori,Via A. Manzoni 101,Parona,PV,27020
Luigia,Cremonesi,Via Castelfidardo 145,Cittadella Del Capo,CS,87020
Fiore,Mucciano,Via Valpantena 120,Buccino Stazione,SA,84020
Sandra,Greco,Via Pisanelli 140,Castiglione D'Adda,LO,26823

scambia la posizione del nome e del cognome e sposta il CAP prima della Località in tutte le righe del file. Già che c’è, corregge anche il nome sbagliato “Violanda” e lo trasforma in “Jolanda”. Nella riga #5, $1 ~ /Violanda/ è la condizione, e {$1 = "Jolanda"} l’azione relativa, mentre nella riga #6 la condizione è vuota e quindi l’azione conseguente {print $2, $1, $3, $6, $4, $5} si applica a tutte le righe del file. Il blocco BEGIN {...} serve per definire il carattere (o i caratteri) che separa i campi contenuti in ciascuna riga letta o scritta dal programma: la variabile predefinita FS è il separatore dei campi del file di input (quello letto dal programma), OFS è il separatore dei campi delle righe stampate dallo script.

Per provarlo, copiate il programma in un editor e salvatelo come swap.awk, poi copiate la lista di indirizzi e salvatela come address.csv (meglio se salvate i due file nella cartella ~/Development, ricordate?). Infine lanciate il Terminale, eseguite il comando

$ awk -f swap.awk address.csv 

e vedete cosa viene fuori. Provate a cambiare OFS e a vedere che succede.

Domanda 1: Come si fa a lanciare lo script senza dover premettere il comando awk?

Domanda 2: Se si rimuove la variabile FS dal blocco BEGIN{...} lo script continua a funzionare correttamente?

Ma basta con awk. Questo articolo non vuole essere una introduzione al linguaggio ma vuole solo mostrare come si può risolvere con AWK il problema che ci sta a cuore. Chi volesse è approfondire la conoscenza del linguaggio può consultare i volumi e le guide online riportate in bibliografia.

Rinominare automaticamente un post

E finalmente eccoci al programma awk che rinomina da solo il file Markdown di un post in base al titolo e alla data contenuti nei metadati del documento, che contengono, fra l’altro, il titolo, la data di pubblicazione, la categoria e i tag associati al post stesso.

BEGIN { FS = " "
        OFS = "-"
        EXT = ".md"
      }

$1 ~ /[Dd]ate:/   { date = $2 }
$1 ~ /[Tt]itle:/  { title = ""
                    for (i = 2; i <= NF; i++) {
                        title = title FS tolower($i)
                    }
                  }

END { newfilename = date OFS title
      gsub("[[:cntrl:]]", "", newfilename)
      gsub("[\"]+", "", newfilename)
      gsub("[\.,;:!\?&\$]+", "", newfilename)
      gsub("[-| ]+", "-", newfilename)
      newfilename = newfilename EXT
      system("mv " FILENAME " " newfilename)
    }

Riassumo brevemente cosa fa il programma, chi non fosse interessato può saltare direttamente al prossimo paragrafo. La prima condizione $1 ~ /[Dd]ate/ cerca la stringa date: (indifferentemente in minuscolo o maiuscolo) nel primo campo di tutte le righe del post e quando la trova assegna il secondo campo, corrispondente alla data, alla variabile date.
La seconda condizione fa lo stesso per il titolo e lo assegna alla variabile title. Poiché il titolo è distribuito su un numero imprecisato di campi (ricordo che il separatore di campo FS è lo spazio), viene utilizzato un ciclo for per leggere ed aggiungere in successione a title tutti i campi della riga successivi al primo. Il numero di campi presenti nella riga del titolo è contenuto nella variabile di sistema NF, aggiornata automaticamente dall'interprete ogni volta che viene letta una nuova riga.
La parte finale del programma, racchiusa nel blocco END {...}, si occupa di definire la variabile newfilename, contenente il nuovo nome da assegnare al file, e di trasformarla secondo le regole desiderate.
La variabile newfilename contiene inizialmente (riga #12) la data e il titolo letti nel post, separati da un trattino (il valore di OFS). Nelle quattro righe successive viene utilizzata la funzione gsub per rimuovere da questa variabile tutti i caratteri indesiderati (caratteri di controllo, virgolette, punteggiatura) e per sostituire spazi e trattini (anche multipli) con un trattino singolo. Alla riga #17 viene aggiunta l'estensione definita in EXT, mentre l'ultima riga del blocco effettua una chiamata al sistema operativo per rinominare effettivamente il file, utilizzando un'altra variabile di sistema, FILENAME, che contiene il nome originale del file Markdown su cui sta operando lo script.

Salviamo il programma nella solita cartella Development con il nome setpostname.awk. Per provarlo dobbiamo avere anche un file Markdown contenente nell'intestazione (header) almeno i metadati relativi al titolo e alla data. Possiamo prendere la prima parte di questo post,

---
layout: post  
title: "Script per tutti i giorni: entra in scena awk"  
author: Sabino Maggi  
date: 2019-02-18 18:00  
categories:  
  - programmazione  
tags:  
  - awk  
  - bash  
  - editor  
  - gawk  
  - perl  
  - script  
  - terminale  
comments: true  
published: true  

---

Nelle prime tre puntate di questa serie abbiamo imparato a scrivere uno script in `bash` per trasformare una stringa di testo in modo che segua delle  convenzioni ben determinate a priori (qui i link alla [prima](https://melabit.wordpress.com/2018/11/23/script-per-tutti-i-giorni-semplici-modifiche-alle-stringhe-di-testo/), [seconda](https://melabit.wordpress.com/2018/12/05/script-per-tutti-i-giorni-dalla-linea-di-comando-al-programma/) e [terza](https://melabit.wordpress.com/2018/12/30/script-per-tutti-i-giorni-shell-e-parametri/) puntata).

In questo caso particolare,...

e salvarlo con un nome qualsiasi nella stessa cartella Development. Poiché la fantasia fa difetto, chiamiamolo articolo.md. A questo punto lanciamo il Terminale, spostiamoci nella cartella Development

$ cd ~/Development

e lanciamo lo script in awk con il comando

$  /usr/bin/awk -f setpostname.awk articolo.md

e voilà, il file articolo.md viene rinominato automaticamente in 2019-02-18-script-per-tutti-i-giorni-entra-in-scena-awk, che è esattamente quello che volevamo. Nota per i più curiosi: dato che awk ha bisogno di sapere sia il nome dello script contenente i comandi che quello del file da elaborare, si usa l'opzione -f per indicare esplicitamente lo script con i comandi.

Perché uso l'intero percorso /usr/bin/awk per richiamare awk? Semplicemente perché voglio essere sicuro di utilizzare l'interprete awk presente di default in macOS e non la versione estesa installata tramite Homebrew, che funziona in modo leggermente diverso e darebbe degli avvertimenti poco incomprensibili, pur riuscendo lo stesso a rinominare correttamente il file.

Conclusioni

Ormai siamo quasi a posto con la rinominazione automatica di un post in Markdown (che poi in effetti è solo una scusa per gettare le basi e per poter affrontare script complessi e più utili). Manca solo un piccolissimo dettaglio, che può far diventare questo script del tutto indistinguibile dai comandi standard del sistema operativo e che sarà l'argomento della prossima puntata.

Bibliografia


  1. Una cosa che io trovo bellissima anche dal punto di vista diciamo così, filosofico, e che mi ricorda i bellissimi articoli di Douglas Hofstadter su Le Scienze sui testi autoreferenziali
Annunci
Tagged with: , , , , , ,
Pubblicato su programmazione
6 comments on “Script per tutti i giorni: entra in scena awk
  1. MailMaster C ha detto:

    Buongiorno Sabino, solo una curiosità: hai pubblicizzato questo post su twitter? Io non ho visto nulla, ma forse mi è sfuggito. Spero di avere tempo presto per leggere e capire questo e il post precedente. Grazie.

    Mi piace

  2. MailMaster C ha detto:

    Ritiro tutto, errore mio: per qualche motivo non ti seguivo più su twitter (don’t ask). Al prossimo post verifico. Scusa il disguido.

    Mi piace

  3. MailMaster C ha detto:

    Ok, letto, piccolo refuso:

    “Nella riga #5, $1 ~ /Violanda/ è la condizione, e {$1 = “Jolanda”;} l’azione relativa, mentre nella riga #6 la condizione è vuota…”

    in realtà le righe sono le 4 e 5, la 6 non l’hai scritta. 🙂

    Grazie ancora per lo sforzo, in effetti AWK non lo conoscevo, sembra interessante.

    Piace a 1 persona

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Informativa
Questo sito utilizza cookie di terze parti per inviarti pubblicità e servizi in linea con le tue preferenze. Se vuoi saperne di più o negare il consenso a tutti o ad alcuni cookie, clicca qui. Scorrendo questa pagina, cliccando su un link o su qualunque altro elemento o proseguendo la navigazione in altra maniera, acconsenti all'uso dei cookie.
Follow Melabit on WordPress.com
Categorie
%d blogger hanno fatto clic su Mi Piace per questo: