-+  Documenti
 |-  Bibliografia
 |-  Articoli
 |-  Perlfunc
 |-  F.A.Q.
 |-  F.A.Q. iclp
-+  Eventi
-+  Contatti
-+  Blog
-+  Link



 

Versione stampabile.


Daniele Ludovici
Studente di ingegneria e appassionato di informatica. Usa Perl per passione e gli piace la comunità che vi sta dietro.
Vincitore Contest2005

Il miglior modo per imparare a risolvere i problemi è crearseli... e quando non si riesce a crearli abbastanza buoni, conviene rubare quelli degli altri e tentare di risolverli per cercare di imparare qualcosa in più. Mi è accaduto proprio questo leggendo un post su una delle mailing-list che spulcio abitualmente. Il problema era il seguente:

Sostituire una stringa in un file

Voi penserete: "Beh? Apro il file con il mio editor preferito, cerco la parola che devo sostituire, e il gioco è fatto. Al massimo posso avventurarmi in qualche pipe tra sed e cat per automatizzare il processo". Ok! Aspettate un po' e vedremo se continuerete ad esserne così convinti :-D

Immaginate di avere un file chiamato pippo.conf con molte righe tra le quali queste tre:

 (1) Load "radeon"
 .
 .
 .
 (N-1) Driver "radeon"
 (N) Driver "vesa"

e immaginate ora di voler sostituire la parola "radeon" con "vesa", ma di volerlo fare solamente nella riga N-1 e non da altre parti. Qualcuno potrebbe obiettare: "Apri il file con un editor qualsiasi ed effettua la sostituzione una volta per tutte!". Ma il nostro amico in mailing-list aveva bisogno di ripetere questa operazione più e più volte su file diversi.

Come automatizzare il tutto?

L'utente della mailing list aveva pensato di utilizzare degli strumenti evergreen come 'sed', 'grep' e 'awk': alla peggio una combinazione dei tre tramite pipe; noi che vogliamo fare sempre tutto con Perl abbiamo risposto: "Perché non usi Perl?"

Ma cerchiamo di capire la strada che intendeva percorrere:

Utilizziamo sed

 sed -e 's/radeon/vesa/' pippo.conf > pippo.conf

Così facendo vengono sostituite entrambe le occorrenze di radeon all'interno del file. Ma non era esattamente quello che volevamo, quindi non ci basta: potremmo pensare di far interagire sed con grep in modo da scegliere la riga da cambiare?

"Pipe tra cat, grep e sed più redirezione"

 cat pippo.conf | grep Driver | sed 's/radeon/vesa/' > pippo.conf

Questa serie di pipe anche se ben architettate non assolvono il compito che ci eravamo prefissati:

  • nel file pippo.conf vanno a finire solamente le righe che matchano il grep e quindi spazziamo via tutto il resto!
  • la bash prima di fare la exec dei vari comandi digitati, esegue le redirezioni e quindi sovrascrive il file pippo.conf prima di eseguire cat e gli altri comandi successivi; a noi invece piacerebbe avere un backup del file che stiamo modificando per dormire sonni tranquilli: possiamo copiare a mano il file sul quale stiamo operando, ma quanti comandi da digitare!

Altra soluziona ingegnosa e molto laboriosa:

sostituiamo tutte le tabulazioni con _ o altro carattere a piacere

 cat pippo.conf | tr '\t' '_' > pippo.conf

e ora è facile cambiare quello che ci interessa

 cat pippo.conf | tr '\t' '_' | sed s/_Driver__\"radeon\"/_Driver__\"vesa\"/g > pippo.conf

poi rimettiamo a posto le tabulazioni e salviamo in un altro file con

 cat /etc/X11/xorg.conf | tr '\t' '_' | sed s/_Driver__\"radeon\"/_Driver__\"vesa\"/g | tr '_' '\t' > prova

Il nostro obiettivo era sostituire una parola, ma se per farlo abbiamo bisogno di tre comandi... stiamo freschi! E i comandi diventano quattro se salviamo da una parte il file per non rischiar di far casini! Ammettendo che tutto ciò funzioni, (abbiamo supposto che ci sia il carattere di tabulazione a separare le parole e non semplici spazi), ci accorgiamo subito che questa soluzione è anch'essa troppo laboriosa.

"Smettila, e dimmi come lo faresti in Perl!"

Una soluzione Perlistica

"Ok, ci sto, vediamo se ti piace!":

 perl -p -i.bak -e 's/radeon/vesa/ if m/Driver/' pippo.conf

questo comando assolve pienamente il compito che ci eravamo prefissati: sostituisce radeon con vesa solamente nella riga in cui è presente anche la parola Driver e in più crea un file di backup chiamato pippo.conf.bak. Per giunta tutto in un colpo solo!

Analiziamolo in tutte le sue parti.

  • perl -> è il comando che usiamo :-D
  • -e <istruzioni> -> ci permette di scrivere il nostro codice tutto su una riga. Quando perl vede che gli abbiamo passato lo switch -e non cerca un file nella lista degli argomenti ma capisce che il programma si trova proprio lì su quella linea (racchiuso tra due tick ')
  • -p -> itera linea per linea sul file pippo.conf, è l'equivalente di: while(<>){ ... }, l'effetto è tale che il codice specificato con -e viene eseguito su ciascuna linea del file pippo.conf. Questa opzione inoltre, fa sì che ogni riga del file venga stampata automagicamente a video con delle print()
  • -i -> crea un backup del file rinominandolo in pippo.conf.bak, apre il file di output col nome originale (pippo.conf) e lo seleziona come default per l'output delle istruzioni print(). Possiamo intuire quindi che il risultato delle nostre operazioni verrà stampato nel nuovo file che in realtà ha il nome del vecchio! Quello che accade in pratica è che le righe che il nostro script processa, invece di esser stampate a video vengono stampate sul file pippo.conf; perl ha salvato in pippo.conf.bak il file originale, quindi siamo tutti contenti.
  • s/radeon/vesa/ if m/Driver/ -> sostituisce la prima occorrenza di radeon con vesa a patto che in quella riga ci sia la parola Driver. La condizione descritta sopra potrebbe esser espressa anche in altri modi:
    • if(m/Driver/){ s/radeon/vesa/;}
    • if($_ =~ m/Driver/){ $_ =~ s/radeon/vesa/; }

uhm... cresce la nostra curiosità.

Ma come lo fa?

Abbiamo detto che con -p, Perl cicla linea per linea e mette a disposizione il contenuto di ciascuna di esse in una variabile particolare: $_. Questa variabile, come si può notare dalla sua non presenza, è implicita. Sì! Ed è proprio la stessa variabile sulla quale andiamo a compiere l'operazione di sostituzione e confronto:

 s/radeon/vesa/ if m/Driver/

questa riga vuol dire: prendi il contenuto della variabile $_ e sostituisci la prima occorrenza di radeon con vesa se in $_ c'è la sequenza 'Driver', potevamo quindi scrivere:

 $_ =~ s/radeon/vesa/ if $_ =~ m/Driver/

Le due scritture sono perfettamente equivalenti ma quella senza la variabile $_ rispetta maggiormente uno degli obiettivi progettuali di Perl: essere assimilabile ad un linguaggio parlato. Questa modalità di sostituzione è molto elegante, compatta e di facilissima lettura, basta farci l'occhio. Abbiamo scoperto un'altra peculiarità di Perl, il cui motto è TMTOWTDI: "c'è più di un modo per fare la stessa cosa".

E quello strano =~ cos'è?

È un operatore di binding, praticamente lega il pattern di ricerca che si trova a destra con l'argomento a sinistra.

Lega?

Sì! A seconda di ciò che c'è a destra va a:

  • cercare: $stringa =~ m/QUALCOSA/;
  • sostituire: $stringa =~ s/QUALCOSA/QUALCOS'ALTRO/;
  • translitterare: $stringa =~ tr/QUALCOSA/QUALCOS'ALTRO/;

nella $stringa alla sua sinistra.

Conclusioni

Come sempre, in questo articoli tentiamo di mettere la pulce nell'orecchio del curiosone che è in ognuno di noi. Perl è considerato il "Coltellino Svizzero" del nostro sistema: abbiamo usato solo una delle sue mille lame, a voi il compito (spero il piacere) di scoprire le altre!

perlrun

Ottima lettura per imparare a scrivere "oneliner" che ci aiutino nei compiti di normale amministrazione del nostro sistema.

perlop

È necessario leggerlo per poter capire gli operatori come =~ in Perl, come utilizzarli e la loro precedenza e associatività.


Ti è piaciuto questo articolo? Iscriviti al feed!











Devo ricordare i dati personali?