
Flavio Poletti
Consulente in telecomunicazioni per una piccola società a Roma, usa Perl
per lavoro e per diletto, con particolare attenzione all'automazione di
processi ripetitivi e noiosi. www.polettix.it
|
Capita a volte di voler redirigere l'output di un programma su un file, o
da qualche altra parte. Altre volte, invece, vorremmo che gli input
arrivassero da dove diciamo noi, invece che da dove il programma pensa
che stiano arrivando. A volte, infine, abbiamo bisogno che queste
redirezioni siano temporanee, e dopo un po' vogliamo ``tornare indietro''.
Vediamo un po' come fare.
In un programma avete a disposizione tre flussi di input/output
detti standard (proprio perché ve li ritrovate sempre):
- stdin
-
è da dove il programma prende i suoi ingressi, normalmente associato alla
tastiera;
- stdout
-
è dove il programma invia le sue uscite, normalmente associato al terminale;
- stderr
-
è dove il programma invia messaggi di errore, che segnalano una condizione
anomala ma che non fanno parte delle uscite. Anche questo, normalmente,
è associato al terminale.
Nel mondo Unix (sinceramente sono un po' all'oscuro del mondo Windows)
questi flussi standard sono associati a dei descrittori di file,
che sono degli indici numerici interi che che il sistema operativo
utilizza per individuare i vari canali di I/O di un processo. In
particolare, stdin è sempre associato al descrittore 0, stdout al
descrittore 1 e stderr al descrittore 2.
Chiunque abbia giocato un po' con qualcosa di somigliante ad una shell
Unix sa che quelle ``associazioni'' di default a tastiera e terminale
possono essere aggirate con
facilità utilizzando pipe (con il carattere | a separare due chiamate
a due programmi differenti, ove lo stdout di quello a sinistra viene
collegato allo stdin di quello a destra) e redirezioni su file (con il
caratteri < per legare stdin ad un file, e > per legare stdout ad un file). Se volete saperne di più l'ottima pagina di manuale della shell (sto pensando a bash) saprà soddisfare tutte le vostre curiosità.
Qui però non ci poniamo il problema di come fare queste redirezioni
da fuori, ma di come farle da dentro al nostro programma in Perl.
Nel seguito non parleremo quasi più di stderr, comunque, visto che le
considerazioni per stdout possono essere applicate (quasi) per intero.
I flussi standard in Perl sono rappresentati da tre filehandle dai nomi
poco sorprendenti (ma in lettere maiuscole, attenzione!): STDIN,
STDOUT e STDERR. (Ok, in realtà probabilmente potrete utilizzare
anche le lettere minuscole, ma non lo fate.)
I flussi standard in Perl si usano come qualsiasi altro filehandle:
my $un_input = <STDIN>;
print STDOUT "Ciao, Mondo!\n";
print STDERR "Ooops, hai commesso un errore!\n";
Se non mettete nessun filehandle in una print si va per default su
STDOUT:
print "Ciao, Mondo!\n"; # va su standard output
È appena il caso di dire che una delle ``best practice'' predicate da
Damian Conway (autore emerito in ambito Perl) è quella di usare un
costrutto un po' più involuto per dire su quale filehandle vogliamo
stampare:
print {*STDOUT} "Ciao, Mondo!\n"; # fa quello che ci si aspetta
ma stiamo veramente divagando.
Va osservata una differenza fra filehandle e descrittori:
-
un descrittore è un indice condiviso fra processo e sistema operativo per
effettuare operazioni di I/O (in particolare, è un indice che punta ad un
elemento di una tabella dei file che il sistema operativo tiene per il
dato processo);
-
un filehandle è uno ``strumento'' a disposizione del programmatore
per istruire il processo sulle operazioni di I/O da effettuare, che
include anche possibili operazioni avanzate come buffering, encoding
ecc.
Ovviamente i due concetti sono fortemente correlati fra loro: un
filehandle è una specie di mantello intorno al descrittore;
alla fin fine quando il processo decide di effettuare l'I/O utilizza
proprio il descrittore associato al filehandle. Per impicciarci su
questa associazione possiamo utilizzare la funzione fileno():
my $descrittore = fileno $filehandle;
Definiamo allora una piccola funzione che ci tornerà utile nel
seguito per cercare di capire cosa succede:
sub stampa_descrittore {
my ($nome, $filehandle) = @_;
print {*STDERR} "$nome ha descrittore ", fileno($filehandle), "\n";
}
stampa_descrittore(STDIN => \*STDIN); # STDIN ha descrittore 0
stampa_descrittore(STDOUT => \*STDOUT); # STDOUT ha descrittore 1
stampa_descrittore(STDERR => \*STDERR); # STDERR ha descrittore 2
Beh, questo è veramente fuori dall'ambito di questo articolo, non vi pare?
Se non vi interessa, che ve lo leggete a fare?
Scherzi a parte, potrebbero esserci molti motivi per fare una cosa del
genere. Un primo esempio è il seguente: avete un programma già fatto, e
volete aggiungere un'opzione di chiamata per cui le uscite vanno su
file anziché su terminale. Come fare? Una soluzione è stabilire, all'inizio
del programma, quale debba essere il filehandle di uscita, dipendentemente
dalle opzioni che vi sono state passate, qualcosa tipo:
my $output;
if (exists $opzioni{output}) {
open $output, '>', $opzioni{output}
or die "open('$opzioni{output}'): $!";
}
else {
$output = \*STDOUT;
}
A questo punto $output dovrebbe essere usato tutte le volte che volete
stampare un'uscita del programma.
Come?!?!? Devo cambiare tutte le uscite?!?
Beh, con questa tecnica sì, magari era meglio pensarci prima. Altrimenti,
potreste redirigere STDOUT sul file e non cambiare una virgola. Un po'
meno pulito, forse, ma efficace.
Un altro caso in cui questa redirezione risulta utile è quando volete
chiamare un programma esterno che - senza che ci sorprendiamo troppo -
prende i suoi ingressi dal proprio stdin e manda le uscite sul
proprio stdout. Ci sono mille modi per chiamare un programma esterno
in Perl, ma se siete per qualche motivo costretti ad usare ``il vecchio
sistema'' di pipe()/fork() allora dovrete fare un po' di
redirezioni prima di chiamare il programma esterno, sulla falsariga
di quanto segue:
my ($figlio_r, $figlio_w, $padre_r, $padre_w);
pipe($figlio_r, $padre_w);
pipe($padre_r, $figlio_w);
defined(my $pid = fork()) or die "fork(): $!";
if ($pid) { # padre
close $figlio_r;
close $figlio_w;
# ... dialoga con il figlio utilizzando:
# $padre_r per leggere dal figlio
# $padre_w per scrivere al figlio
}
else { # figlio
close $padre_r;
close $padre_w;
# dobbiamo capire come fare:
# REDIRIGI STDIN su $figlio_r
# REDIRIGI STDOUT su $figlio_w
exec {$programma} $programma, @ARGOMENTI
or die "exec('$programma'): $!";
}
Il sistema più semplice per redirigere uno dei filehandle standard in
Perl (anzi, forse l'unico vero modo, come avremo modo di vedere più
avanti) è ri-aprire il filehandle su qualcosa di differente. In questo
ci viene in aiuto la funzione open(), ovviamente: con cosa pensavate
di aprire un filehandle?
Il livello più base di redirezione è piuttosto semplice. Se infatti
l'obiettivo è quello di redirigere su file, basterà ri-aprire il
filehandle sul file relativo:
open STDIN, '<', $file_input or die "open('$file_input'): $!";
open STDOUT, '>', $file_input or die "open('$file_input'): $!";
Per redirigere su file non avete bisogno di chiudere il filehandle prima
della nuova open(), perché Perl lo farà automaticamente per voi.
Funziona ed è semplice, ma non sempre dovremo redirigere su un file,
o su un file che dobbiamo ancora aprire. Forse è il caso di andare
avanti.
A partire da Perl 5.8.x è possibile aprire un filehandle che punta
ad una variabile in memoria invece che da qualche altra parte.
Per fare questa cosa meravigliosa basta passare, al posto del nome
del file, un riferimento alla variabile da aprire:
my $variabile;
open my $fh, '>', \$variabile or die "open(): $!";
Inutile dirlo, la redirezione dei flussi standard funziona anche in questo
caso, esattamente come descritto per un normale file nella sezione
precedente. L'unica avvertenza però è la seguente: per redirigere
un filehandle di output (STDOUT o STDERR) sarà prima necessario chiuderlo:
close STDOUT;
open STDOUT, '>', \$variabile or die "open('$file_input'): $!";
Questa tecnica
può risultare particolarmente comoda quando avete un pezzo di codice
preesistente, o anche un modulo, che stampano le proprie uscite su
STDOUT, ma voi volete catturare queste uscite per potervele ``lavorare''
prima di stamparle effettivamente per farle vedere agli utenti del programma
(ad esempio, se volete
aggiungere dei numeri di riga, oppure eliminare tutte le parolacce
dal testo). Attenzione che, in questo caso, dovete anche tenere una
ciambella di salvataggio verso la vera uscita del programma, altrimenti
dopo aver effettuato la redirezione non saprete più come comunicare
con l'utente. Come farlo... sarà evidente nel seguito (i più curiosi
possono saltare subito alla fine dell'articolo, proprio prima delle
conclusioni).
In qualche modo - da un modulo bellissimo che avete appena trovato,
o per sola imposizione delle mani - vi ritrovate un filehandle che
vorreste sostituire ad uno dei flussi standard.
Il metodo più semplice che può venire in mente è far ``puntare'' i filehandle
dove vogliamo noi. I filehandle standard ``vivono'' nel pacchetto main,
in dei GLOB che hanno lo stesso nome (no, non vi tedierò spiegando cos'è
un GLOB, tranquilli). In maniera molto cruda, possiamo fare qualcosa
del genere:
*STDIN = $filehandle_nuovo_input;
*STDOUT = $filehandle_nuovo_output;
Cominciamo subito a dire che questo approccio funziona:
open my $fh, '>', 'prova.txt' or die "open(): $!";
*STDOUT = $fh; # Redirezione!
print "Ciao, Mondo!\n"; # stampa su prova.txt
Per casi molto semplici può andare bene, anche se probabilmente non
vi guarderanno molto bene se fate qualcosa del genere. In particolare,
occorre prestare attenzione al fatto che questa operazione snatura
i flussi, dissociandoli dal loro significato di ``flussi standard'' che
hanno a livello di sistema operativo. Che significa? Ci arriviamo subito,
basta vedere cosa sta succedendo in basso, sui descrittori:
open my $fh, '>', 'prova.txt' or die "open(): $!";
stampa_descrittore(STDOUT => \*STDOUT); # STDOUT ha descrittore 1
stampa_descrittore(fh => $fh); # fh ha descrittore 3
*STDOUT = $fh; # Redirezione!
stampa_descrittore(STDOUT => \*STDOUT); # STDOUT ha descrittore 3
print "Ciao, Mondo!\n"; # stampa su prova.txt
Attenzione! STDOUT ha cambiato descrittore! È come se avessimo tolto
il mantello con scritto STDOUT dal descrittore standard numero 1 e
l'avessimo avvolto intorno al descrittore numero 3. Per il codice la
chiamata a print() continua ad essere diretta su STDOUT, ma per il
sistema operativo le chiamate di I/O sono rivolte al descrittore 3,
che punta al file e non è il descrittore standard per l'output.
Quanto può importarci di questa cosa? Dipende. Se l'obiettivo era unicamente
redirigere le varie print() nel programma, questo approccio se la
cava egregiamente. Ma se vogliamo fare la redirezione per poi chiamare
un altro programma con exec() questo sistema fallirà miseramente,
perché in fase di esecuzione del nuovo programma i descrittori considerati
per i flussi standard sono sempre 0, 1 e 2, e questi coincidono con
quelli del processo prima della chiamata ad exec()!
Ci sarà pure un sistema, allora? Ebbene sì, il sistema c'è ed è fare in
modo da non cambiare il descrittore di STDOUT. Lapalissiano? Forse.
Ricordate quando s'è detto che - probabilmente - l'unico modo per effettuare
una redirezione come si deve è usare open()? È il momento di mettere
alla prova quell'affermazione.
La funzione open() ha circa 2000 varianti, ed una di queste è utilizzata
per chiamare la sottostante funzione di sistema dup(), che clona gli
elementi nella tabella dei file gestita dal sistema operativo:
open my $fh, '>', 'prova.txt' or die "open(): $!";
stampa_descrittore(STDOUT => \*STDOUT); # STDOUT ha descrittore 1
stampa_descrittore(fh => $fh); # fh ha descrittore 3
open STDOUT, '>&', $fh or die "dup/open(): $!"; # Redirezione!
stampa_descrittore(STDOUT => \*STDOUT); # STDOUT ha descrittore 1
print "Ciao, Mondo!\n"; # stampa su prova.txt
Benissimo, quindi! Mettendo il carattere di e commerciale & dopo
il carattere che indica la modalità di apertura possiamo fare una redirezione
a prova di exec()! Una redirezione dello STDIN sarebbe dunque:
open STDIN, '<&', $filehandle_nuovo_input
or die "dup/open(): $!";
Come detto, dup() lavora a livello di tabella dei file nel sistema
operativo: in pratica non fa altro che copiare un elemento di una
tabella in un altro elemento. In questo modo, entrambi gli elementi
puntano verso lo stesso flusso - sia esso un file su disco, un socket per
una comunicazione in rete, o qualsiasi altra cosa sia rappresentata
da un file in senso esteso - pur avendo sorti ormai slegate: se
chiudo uno dei due, l'altro rimane vivo e vegeto.
Che succede se un modulo che vogliamo utilizzare non si attiene alla
forma pulita ed effettua redirezioni alla buona? Non è una domanda
oziosa per allungare un po' il brodo di questo articolo, ma anzi il
motivo principale per cui mi è venuto in mente di scriverlo!
La prima cosa che mi è venuta in mente quando ho avuto questo problema
(con HTTP::Server::Simple) è stato fare qualcosa di questo tipo:
# Ad inizio programma, quando i filehandle sono ancora "intatti"
my $STDIN_originale = \*STDIN;
# ...
# Ad un certo punto il modulo farà qualcosa del genere:
*STDIN = $nuovo_input;
# ...
# ...
# Nel programma, allora, proviamo ad invertire
my $STDIN_nuovo = \*STDIN;
*STDIN = $STDIN_originale; # TENTATIVO di ripristino
open STDIN, '<&', $STDIN_nuovo; # redirezione corretta
Peccato che non funzioni! Il problema è che $STDIN_originale non è
slegata da STDIN, ma anzi ne segue le sorti molto da vicino, visto
che la ``punta'' direttamente. In poche parole, la redirezione fatta
``alla buona'' modifica anche $STDIN_originale, vanificando il nostro
tentativo di ``conservazione'':
# Ad inizio programma, quando i filehandle sono ancora "intatti"
my $STDIN_originale = \*STDIN;
stampa_descrittore(STDIN_originale => $STDIN_originale); # stampa:
# STDIN_originale ha descrittore 0
# ...
# Ad un certo punto il modulo farà qualcosa del genere:
*STDIN = $nuovo_input;
stampa_descrittore(STDIN => \*STDIN); # STDIN ha descrittore 3
# ...
# ...
stampa_descrittore(STDIN_originale => $STDIN_originale); # stampa:
# STDIN_originale ha descrittore 3
Dobbiamo trovare un altro modo per conservare il filehandle originale,
quindi. Da notare che una semplice redirezione dup() non andrebbe bene:
open my $copia, '<&', \*STDIN
or die "dup/open(): $!";
stampa_descrittore(copia => $copia); # copia ha descrittore 3
Pensandoci a posteriori, è abbastanza chiaro (o almeno a me è risultato
chiaro solo pensandoci a posteriori). dup() effettua una copia, a
livello di tabella dei file del processo (gestita dal sistema operativo),
da un certo descrittore a un altro descrittore. In questo caso,
$copia è una variabile inizialmente undef, per cui - dietro le
quinte - per prima cosa $copia viene creata come filehandle, cui è
associato un descrittore non utilizzato (3 nell'esempio),
poi viene effettuata la copia dal descrittore 0 a quello di $copia.
Non tutto il male viene per nuocere: a questo punto $copia
può diventare la nostra ``memoria storica'' dell'input originale del processo,
e può giocare (come vedremo) un ruolo fondamentale se dovessimo averne di
nuovo bisogno. Ma non anticipiamo troppo, e torniamo al nostro problema
originale: non perdere il nostro aggancio al descrittore standard.
Ciò di cui abbiamo bisogno è poter
clonare il filehandle originale, facendo in modo da riutilizzare anche
il descrittore: è quello che - nel sistema operativo - fa fdopen(). Non
sorprende, pertanto, che fra le 2000 varianti di open() ce ne sia anche
una che si comporta come fdopen():
open my $clone, '<&=', \*STDIN # notare il carattere "="
or die "fdopen/open(): $!";
stampa_descrittore(clone => $clone); # clone ha descrittore 0
Non avevamo detto che open() era la panacea a tutti i nostri problemi
di redirezione? Per aggirare un modulo ``alla buona'', quindi, la sequenza
più corretta sarebbe la seguente:
# Ad inizio programma, quando i filehandle sono ancora "intatti"
open my $STDIN_originale, '<&=', \*STDIN
or die "fdopen/open(): $!";
# ...
# Ad un certo punto il modulo farà qualcosa del genere:
*STDIN = $nuovo_input;
# ...
# ...
# Nel programma, allora, proviamo ad invertire
my $STDIN_nuovo = \*STDIN; # salva quanto impostato dal modulo
*STDIN = $STDIN_originale; # ripristina STDIN, descrittore compreso
open STDIN, '<&', $STDIN_nuovo; # redirezione corretta
Ripristinare la corretta associazione filehandle/descrittore può essere
importante per vari motivi. Se ci interessa unicamente perché vogliamo
chiamare exec() ed essere sicuri di impostare i giusti flussi standard
al programma che stiamo per chiamare, però, probabilmente tutto il
lavoro di salva/ripristina/redirigi correttamente che abbiamo descritto
è un po' troppo. Quello di cui abbiamo veramente bisogno, in fondo, è
poter lavorare sui descrittori standard 0, 1 e 2.
A tal proposito, il modulo POSIX mette a disposizione esattamente la
funzione che ci serve, ossia dup2(). Questa funzione fa un mestiere
piuttosto semplice: clona le impostazioni associate ad un descrittore
su un altro descrittore, entrambi di nostra scelta.
Nell'esempio pipe()/fork(), quindi, possiamo cavarcela molto
semplicemente:
my ($figlio_r, $figlio_w, $padre_r, $padre_w);
pipe($figlio_r, $padre_w);
pipe($padre_r, $figlio_w);
defined(my $pid = fork()) or die "fork(): $!";
if ($pid) { # padre
close $figlio_r;
close $figlio_w;
# ... dialoga con il figlio utilizzando:
# $padre_r per leggere dal figlio
# $padre_w per scrivere al figlio
}
else { # figlio
close $padre_r;
close $padre_w;
require POSIX; # contiene la funzione dup2
POSIX::dup2(fileno($figlio_r), 0); # REDIRIGI stdin su $figlio_r
POSIX::dup2(fileno($figlio_w), 1); # REDIRIGI stdout su $figlio_w
exec {$programma} $programma, @ARGOMENTI
or die "exec('$programma'): $!";
}
Ora abbiamo abbastanza materiale per poter rispondere anche ad un'altra
domanda: come torno indietro dopo aver fatto una redirezione? Come abbiamo detto prima, ad esempio, potremmo aver rediretto l'output di una parte del codice per catturarlo in una variabile, in modo da avere la possibilità di operare qualche trasformazione (come cancellare le parolacce, o aggiungere numeri di riga) prima di stampare effettivamente. Se ho rediretto l'output, però... come faccio a stampare effettivamente?
Il sistema più generale è quello di dup()licare
l'elemento che ci interessa nella tabella dei file del processo, per poterlo
ripristinare in seguito:
# salva STDOUT per poterlo riutilizzare in seguito
open my $STDOUT_originale, '<&', \*STDOUT or die "dup/open(): $!";
my $testo_catturato;
close STDOUT; # Ricordate? Va chiuso quando si opera con la variabile!
open STDOUT, '>', \$testo_catturato or die "open(): $!";
# ... ora, la "print" opera su $testo_catturato!
print "una riga, scemo!\n";
print "altra riga\n";
print "ultima riga, scemo!\n";
$testo_catturato =~ s{scemo}{**beep**}gi;
# per stampare, posso usare $STDOUT_originale...
print {$STDOUT_originale} "Testo 'ripulito':\n";
# ... oppure ripristinare STDOUT
open STDOUT, '<&', $STDOUT_originale or die "dup/open(): $!";
print $testo_catturato;
Avete mai usato la funzione local in un programma Perl? No? Fate bene,
perché nella stragrande maggioranza dei casi non ne avete bisogno. Però...
Quando localizzate una variabile (rigorosamente di pacchetto, non potete
farlo con una variabile my), sostanzialmente state creando una copia
temporanea che ``dura'' finché non viene chiuso il blocco in cui appare
questa localizzazione. Ad esempio, se volete leggere un file tutto
d'un fiato, potete impostare $/ ad undef giusto il tempo di
effettuare la lettura:
my $contenuto_file = do { # apre un blocco
local $/; # automaticamente impostata a "undef"
open my $fh, '<', $nomefile or die "open('$nomefile'): $!";
<$fh>;
}; # fine del blocco, $/ viene ripristinata al valore precedente
Visto che i filehandle STDIN, STDOUT e STDERR sono agganciati al
GLOB omonimo nel pacchetto main, un sistema molto rapido per
rendere temporanea una redirezione consiste proprio nel localizzare
il GLOB stesso:
my $testo_catturato;
{
local *STDOUT;
open STDOUT, '>', \$testo_catturato or die "open(): $!";
# ... ora, la "print" opera su $cattura!
print "una riga, scemo!\n";
print "altra riga\n";
print "ultima riga, scemo!\n";
}
$testo_catturato =~ s{scemo}{**beep**}gi;
# Qui STDOUT ritorna quello originale!
print "Testo 'ripulito':\n";
print $testo_catturato;
Viene stampato:
Testo 'ripulito':
una riga, **beep**
altra riga
ultima riga, **beep**
Da notare che, in questo caso, non abbiamo avuto bisogno di chiudere
il filehandle STDOUT. Perché? Semplice: a valle dell'operazione di
localizzazione, il filehandle STDOUT non ``punta'' più all'originale - che
dovremmo chiudere - ma è indefinito, per cui non c'è proprio niente
da chiudere.
Questo sistema può essere utilizzato come tecnica di difesa contro
i moduli che effettuano la redirezione alla buona. Ritornando
all'esempio sviluppato in precedenza, una possibile soluzione alternativa
è la seguente:
# ...
# Ad un certo punto dovremo chiamare il modulo...
my $STDIN_nuovo;
{
local *STDIN; # Inganno! Ora possiamo chiamare il modulo...
# ... che farà qualcosa tipo:
*STDIN = $nuovo_input;
# Prima di uscire da questo blocco:
open $STDIN_nuovo, '<&', \*STDIN; # or die... omesso
}
# Nel programma, allora, proviamo ad invertire
open STDIN, '<&', $STDIN_nuovo; # redirezione corretta
Insomma, local non è proprio una funzione con cui giocare troppo - in
generale non ne avrete mai bisogno, per le variabili utilizzate my -
ma ha ancora una nicchia di utilità.
... possiamo dire che redirigere i flussi standard di un programma non
è particolarmente complicato, anche se dobbiamo fare attenzione a farlo
nella maniera giusta per non avere sorprese e perdere troppo tempo a
capire cosa succede.
Come approfondimento, sicuramente consiglierei di dare un'occhiata
al manuale Perl su perlfunc/open. Se poi vi è rimasto il dubbio su
cosa siano questi famigerati GLOB, probabilmente un'occhiata a
perldata sarebbe d'uopo.
Se proprio siete curiosi di sapere quando non vi rideranno dietro se
usate local, potete leggere l'articolo di Mark Jason Dominus
Coping with Scoping (in inglese), anche disponibile in
italiano nell'ottima traduzione di larsen
Lo Scopo Dello Scope. Ok, viene
detto che non dovete mai usare local, ma non è proprio vero... date
un'occhiata all'articolo (sempre di Mark Jason Dominus), all'indirizzo
Seven Useful Uses for local (in inglese).
Non mi vengono invece in mente moduli che potrebbero esservi utili
su questo argomento... suggerimenti?
Ti è piaciuto questo articolo? Iscriviti al feed!
|