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


Inviato da giobis il October 10, 2006 2:42 PM

Ottimo articolo. Complimenti

Inviato da polettix il November 23, 2006 12:51 AM

GNU sed v. 4.1.2:

sed -i.bak '/Driver/s/radeon/vesa/' pippo.conf

Prese in giro a parte, è un articolo molto istruttivo e... spumeggiante.

Inviato da Frene il February 5, 2007 9:52 PM

Salve,avrei bisogno di una cortesia.
Se io ho dei file .mid (midifile) nei quali sono inserite delle parole come Lyrics o Testi e voglio sostituire intere stringhe,facendolo su tutti i midi, esempio:
un file midi ha come testo iniziale "PIPPO PIPPI". Se io voglio cancellare tutti i "PIPPO PIPPI" e sostituirli tutti con "FRANCHI FRANCHI" come posso fare??? Esiste un programma o una funzione speciale su Windows XP?
Grazie

Inviato da dree il February 8, 2007 9:59 PM

Per la ridenominazione di file su XP, prova a leggere questo articolo di Punto Informatico: http://www.pidownload.it/download/scheda.asp?i=1547823

Inviato da andrea il February 25, 2008 5:34 PM

ciao, mi potresti dire dove posso trovare un programma che mi trova e sostituisce una parola in tantissimi files diversi? Ho provato ad utilizzare sed ma non riesco a farlo funzionare.Fammi sapere
ciao

Inviato da antonio catalano il April 19, 2008 11:04 AM

semplicemente straordinario

Inviato da max il April 24, 2008 1:31 PM

Ciao bell'articolo, ma se volessi sostituire in un file un percorso?
Esempio. Il file pippo.conf ha al suo interno:

Ciao ciao ciao
href="/var/www/"
Ciao ciao ciao

e volessi sostituire /var/www con un'altro percorso tipo /etc/ come potrei fare??

Grazie

Inviato da ludan il May 13, 2008 11:39 PM

Ciao, scusate il ritardo nel rispondere ma non avevo fatto caso a questi post. Rispondo a max: il problema da te sollevato e' relativo all'utilizzo di delimitatori diversi da / quando si usa l'operatore s/PATTERN/REPLACEMENT/
Se vogliamo sostituire un pattern tipo /var/www/ con /var/ops/ ovviamente l'operatore s/// si incasina perche' non capisce dove inizia il pattern e dove finisce. Questa cosa si risolve facilmente in Perl perche' abbiamo molta flessibilita' nello scegliere il delimitatore che piu' ci piace. Per il problema da te posto potremmo scrivere:

perl -p -i.bak -e s#/var/www/#/etc/quellochevuoi/#' pippo.conf

andando ad utilizzare # come delimitatore e il gioco e' fatto. Saluti!

Inviato da ludan il May 14, 2008 12:35 AM

Ciao, rispondo ad Andrea che chiede come sostituire una stessa parola in diversi file:

perl -p -i.bak -e 's/foo/baar/g' file1 file2 fileN

questo comando cambia foo in bar in tutti i file chiamati da riga di comando. Immagino ti potrebbe servire di applicare questo cambio in moltissimi file sparsi in sottodirectory. Tanto per cambiare ci sono molte strade per ottenere il risultato desiderato :-)

1) usi il modulo File::Find
oppure fai un qualcosa di analogo combinando il "find" Unix + una riga di Perl cosi':

find /mypath -type f -execdir perl -p -i.bak -e 's/pensieri/parole/g' {} +

per tutti i file da /mypath in giu' nelle varie sottodirectory, applica lo script Perl che abbiamo visto piu' volte in questo articolo e cambia i "pensieri" in "parole" :-)

Inviato da Andrea il October 24, 2008 4:11 PM

Grazie per le indicazioni che in questo articolo ho trovato.

Una domanda: se volessi però aumentare il numero di condizioni, vale a dire, cambio radeon in vesa non solo perché "matcha" il controllo m/Driver ma magari perché in quella stessa riga c'è anche un'altra stringa di controllo.

Come faccio a mettere o in AND o in OR logico le stringhe di controllo?

Grazie ed attendo un tuo riscontro.

Andrea

Inviato da ludan il October 27, 2008 3:46 PM

AND: perl -p -i.bak -e 's/radeon/vesa/ if m/Driver/ && m/altro controllo/' pippo.conf
OR: perl -p -i.bak -e 's/radeon/vesa/ if m/Driver/ || m/altro controllo/' pippo.conf

Inviato da andrea il October 31, 2008 12:43 PM

.... ciao.

Ho difficolta a sostituire in una stringa il / con \/ da passare al perl o al sed per la sotituzione.Come faccio a manipolare una stringa che contiene i caratteri speciali?

Per esempio: STRINGA=/pippo/pluto/minny

deve diventare STRINGA=\/pippo\/pluto\/minny

per la sostituzione.

attendo un tuo aiuto e ed ti ringrazio anticipatamente.

Inviato da ludan il November 3, 2008 11:08 AM

Ciao Andrea,
puoi risolvere il tuo problema in questo modo:

perl -le '$stringa="/pippo/pluto/minny"; $stringa =~ s#/#\\/#g; print $stringa'

come vedi ho utilizzato l'operatore di sostituzione s#pattern#replacement# stando attento ad utilizzare un delimitatore diverso da / (poiche' e' presente nella stringa che stai trattando). Dopodiche' ti basta semplicemente andare a sostituire ogni occorrenza di / con \\/ dove ovviamente devi fare l'escape di \ (ecco perche' ce ne sono due).
Saluti

Inviato da Andrea il November 10, 2008 12:49 PM

Carissimo ludan buondì!! ....
grazie per le tue preziosissime indicazioni!!! E sulla scia di queste vorrei chiederti:
se volessi inserire una riga che descrive l'assegnazione ad una variabile di sistema in un file di configurazione (tipo il .bash_profile di root), come posso fare?
Per esempio, /root/.bash_profile da:
....
.....
......

  1. User specific environment and startup programs

PATH=$PATH:$HOME/bin

export PATH
unset USERNAME

...... deve diventare:
# User specific environment and startup programs

PATH=$PATH:$HOME/bin
JAVA_HOME=/installation/jdk1.6.0_01/

export PATH JAVA_HOME
unset USERNAME

... come vedi ho aggiunto la definizione della variabile di sistema JAVA_HOME, richiamata poi nella sezione di export del bash_profile.

Grazie, Andrea

Inviato da Andrea il November 10, 2008 3:50 PM

... sono di nuovo qui!!!
Scusami, ma come faccio ad una istruzione perl, a passargli il valore delle variabili d'ambiente?

Vedi esempio: perl -p -i.bak -e 's/\$HOSTNAME//' /etc/hosts

voglio eliminare in /etc/hosts appunto il $HOSTNAME dalla riga di alias con l'ip di loopback e localhost. Sembra non prendere il valore di $HOSTNAME;

ciao e grazie,

Andrea

Inviato da larsen il November 10, 2008 5:48 PM

@Andrea Le trovi tutte nella hash %ENV. Ad esempio $ENV{HOSTNAME}










Devo ricordare i dati personali?






D:
Annunci Google