Lo escrito, escrito está

Usando y abusando de ficheros y demás facilidades de entrada/salida. Variables predefinidas. Interpolación. Expresiones condicionales.

"Que si, que mucho Perl para apresurados y mucha gaita gallega, pero mira la hora que es y no nos hemos comido un jurel", estará diciendo a estas alturas el (apresurado) lector. Tenga paciencia vuecencia, que sin pausa pero sin prisa, todo llegará. En particular, llega que, no conforme con escribir cosas que estén dentro de un programa, alguien quiera escribir algo que esté, por el contrario, fuera de un programa. Y para más inri, con spoilers traseros y todo: añadiéndole números de línea. Pues para eso estamos (está Perl):

my $leyendo = "diablocojuelo.txt";                                                    (1)
open my $fh, "<", $leyendo                                                   (2)
  or die "No puedo abrir el fichero $leyendo por $!\n";                                                (3)
while (<$fh>) {                                                    (4)
  print "$.$_";                                                     (5)
}
close $fh;                                                         (2)
(1)
Vamos a dejar las cosas claras desde el principio. $leyendo es una variable. Una variable cara, porque lleva el dólar al principio. Y por eso es mía y le he puesto el my. Sólo mía. Bueno, de hecho, es una variable de ámbito léxico, que será invisible fuera del bloque. Este bloque abarca todo el programa. Pero el ámbito de una variable puede ser cualquier bloque (que están encerrados entre llaves ({}), como uno que hay un poco más abajo).

Y en cuanto a la variable propiamente dicha, es una variable escalar. Por eso lleva un dólar delante, porque el dinero también es escalar. Creo. En todo caso, en las variables escalares puede haber números, o cadenas alfanuméricas; a Perl le da igual. La interpretará como uno, o como otro, dependiendo del contexto.Por ejemplo

$foo="13abc"
print $foo + 1
14

En realidad, no es necesario declarar las variables. Cuando se usa una variable por primera vez, aparece automágicamente con valor nulo, pero el ámbito será global. Y las variables globales son una mala cosa.

(2)
Habrá que abrir (open) el fichero, claro. Eso es lo que hacemos aquí. A open se le pasa la variable que vamos a usar para referirnos al fichero abierto[1], que declaramos sobre la marcha, el modo de apertura, en este caso "<" para lectura (podía ser ">" para escritura) y, como es natural, el nombre del fichero, que tenemos en la variable $leyendo. Pero las cosas pueden ir mal: puede haberse comido el fichero el perro, puede no existir ese fichero, o puede haber cascado en ese preciso instante el disco duro. Así que hay que prever que la operación pueda ir mal y dejar que el programa fallezca, no sin un epitafio adecuado.

Los ficheros abiertos hay que cerralos, que si no pasa corriente y se puede resfriar el disco duro. Incluso habría que comprobar si se han cerrado correctamente, para ser más papistas que el camarlengo, pero lo vamos a dejar para mejor ocasión.

(3)
Si hay algún problema al abrir el fichero, el programa irremisiblemente muere (die). Pero no muere por las buenas, sino que te da un mensaje que te demuestra que hay una buena razón para hacerlo:
./escrito.pl
No puedo abrir el fichero diablocojuelo.txt porque No existe el fichero o el directorio
El epitafio lo proporciona $!, otra de las variables por defecto o implícitas de Perl que contiene el último mensaje de error del sistema.

Nota

Las documentación sobre las variables predefinidas o implícitas se puede consultar escribiendo perldoc perlvar.

Pero no hemos dicho nada del or al principio de la línea. Como esto es un lenguaje decente, para terminar una sentencia hace falta el ;, que no está hasta el final de esta línea. Con lo que esta línea y la anteriores viene a decir "Voy a intentar abrir el fichero, pero si no pudiera o pudiese, me muero y dejo este mensaje" Ese pero si no, or, que representa a la función lógica O[2]hace que se ejecute su parte derecha solo si la parte izquierda tiene como resultado un valor falso, es decir, si open devuelve undef, lo que ocurrirá si con cualquier problema a la hora de abrir el fichero.

Esta construcción viene a ser un condicional y el caso más general es <expresión> <condicional> <sentencia>. Lo veremos mucho.

Y de camino hemos visto como funciona la interpolación de variables en cadenas: una variable dentro de una cadena se sustituirá por su valor al ejecutar el programa.

(4)
Esto es un bucle while. Un pirulí para quien lo haya averiguado. Pero es un bucle raro, porque dentro de la condición de bucle (lo que hay entre paréntesis), lo que decide si se sigue o no, tiene lo que parece una etiqueta HTML. Pues no lo es. Por alguna razón ignota, los paréntesis angulares (que así se llaman) es un operador que, entre otras cosas, lee la siguiente línea del filehandle incluido dentro. Y devuelve verdadero; cuando no puede leer más líneas, devuelve falso. Como era de esperar, este bucle va a recorrer las líneas del fichero. Detrás de while siempre va un bloque y los bloques siempre llevan llaves. Como los coches. Aunque tengan una sola línea. Quién sabe qué podría pasarle a un bloque indefenso, si no llevara llaves.
(5)
Se interpolan dos variables en una cadena, también predefinidas. El bucle recorre las líneas del fichero, pero no sabemos dónde las va metiendo. Pues van a parar a $_, la variable por defecto por excelencia, donde van a parar un montón de cosas que sabemos y otras muchas que ignoramos. Muchas funciones actúan sobre esta variable por defecto sin necesidad de explicitarla y si hay un bucle huérfano sin variable de bucle, allí que te va esa variable por defecto para ayudarla. Y para que, a su vez, no se quede solita, le ponemos delante la $., que contiene el número de línea del fichero que se está leyendo. Y ni le ponemos el retorno de carro detrás, porque en Perl, cuando se lee de un fichero, se lee con sus retornos de carro y todo, para que uno haga con ellos lo que quiera (quitárselos de en medio y no acordarse de que están ahí, en la mayor parte de los casos).

El resultado que obtenemos, será tal que así:

jmerelo@vega:~/txt/tutoriales/perl-apresurados/code$ ./escrito.pl ../diablocojuelo.txt | head
1 The Project Gutenberg EBook of El Diablo Cojuelo, by Luis Vélez de Guevara
2
3 This eBook is for the use of anyone anywhere at no cost and with
4 almost no restrictions whatsoever.  You may copy it, give it away or
5 re-use it under the terms of the Project Gutenberg License included
6 with this eBook or online at www.gutenberg.net
7
8
9 Title: El Diablo Cojuelo
10

Pero lo cierto es que en Perl hay otra forma de hacerlo. Leer ficheros es una de las principales aplicaciones de Perl, así que el programa anterior se puede simplificar al siguiente:

#!/usr/bin/perl
while (<>) {
  print "$. $_";
}
Programa minimalista donde los haya. Cuando Perl se encuentra los paréntesis angulares en un programa, hace todo lo siguiente: toma el primer argumento de la línea de comandos, lo abre como fichero y mete la primera línea en la variable por defecto $_. Si no se ha pasado nada por la línea de comandos, se sienta ahí, a verlas venir, esperando que uno escriba cosas desde teclado (entrada estándar) y le dé a Ctrl-D, que equivale al carácter EOF, fin de fichero. Lo que equivale exactamente a leer de la entrada estándar, que se puede hacer de alguna de las múltiples formas posibles: cat diablocojuelo.txt| ./escrito2.pl o ./escrito2.pl < diablocojuelo.txt, sin ir más lejos. . Pero es que todavía se puede simplificar más, usando argumentos de línea de comandos:
#!/usr/bin/perl -n
print "$. $_";
que se quita de en medio hasta el while, que queda implícito por la opción -n que le pasamos al intérprete.

Nota

Y si se puede hacer eso en Python, que venga Guido Van Rossum y lo vea.

También se puede complicar más, claro. Por ejemplo, no me negaréis que esas líneas vacías con el numerito delante ofenden a la vista. No hay nada en esas líneas, nadie va a decir "Por favor, lean esta línea" porque está vacía. Así que lo mejor es quitarle también los números. Y, de camino, no está de más curarnos en salud antes de abrir un fichero comprobando si se puede leer o no:

my $leyendo = "diablocojuelo.txt";
if ( ! -r $leyendo ) {                                               (1)
  die "El fichero $leyendo no es legible\n";
}
open my $fh, "<", $leyendo 
  or die "No puedo abrir el fichero $leyendo porque $!\n";
while (<$fh>) {
  chop; chop;
  print "$." if $_;                                                   (2)
  print "$_\n";
}
close $fh;
(1)
En este caso, usamos el if en su forma más tradicional,
if (condición)
{bloque}
. Lo que no es tan tradicional es la condición: el operador -r comprueba si el fichero es legible (para el usuario) y devuelve falso si no lo es. De forma que:
jmerelo@vega:~/txt/tutoriales/perl-apresurados/code$ chmod -r diablocojuelo.txt
jmerelo@vega:~/txt/tutoriales/perl-apresurados/code$ ./escrito-if.pl
El fichero diablocojuelo.txt no es legible
(2)
Para quitar de enmedio las líneas vacías, primero hay que tener en cuenta que no lo están. Tienen al final dos caracteres (los de retorno de carro y salto de línea), que hay que eliminar con sendos chop, que hace precisamente eso, elimina un carácter al final de la línea. Y con eso, en la línea siguiente escribimos el número de línea, pero sólo si queda algo en la variable por defecto.

En Perl siempre hay más de una forma de hacer las cosas. Se puede elegir la más elegante, o la más divertida. O la que uno conozca mejor, claro. Por eso no tenemos que limitarnos a abrir ficheros y escribir porquería en pantalla, que luego se ponen los terminales perdidos y parecen el salvapantallas de Matrix. Así que vamos a escribir la salida en un fichero, como hacemos en el siguiente programa

my $leyendo = "diablocojuelo.txt";
if ( ! -r $leyendo ) {
  die "El fichero $leyendo no es legible\n";
}
open my $fh, "<", $leyendo 
  or die "No puedo abrir el fichero $leyendo por $!\n";
open my $fh_out, ">", "$leyendo-out";
while (<$fh>) {
  chop; chop;
  print $fh_out "$." if $_;
  print $fh_out "$_\n";
}
close $fh;
close $fh_out;


Este programa es igual que el anterior, salvo por las líneas resaltadas, que abren el fichero y escriben en él. La única diferencia entre abrir y cerrar un fichero es el símbolo de menor (<) que ahora mira para el otro lado. El nombre del fichero de salida lo hemos creado a partir del de entrada añadiéndole -out. Y en cuanto a escribir un fichero, se diferencia de escribir en pantalla (en salida estándar, en realidad) sólo en la inserción del filehandle (print $fh_out) tras la orden. Sin coma detrás, ojo.

Importante

Ejercicios. Ahora ya no tenemos excusa para no hacer nada. Vamos a hacerlo simple: un programa que cuente el número de líneas que no estén en blanco en un fichero, y lo escriba en un fichero de salida cuyo nombre se cree a partir del nombre del fichero original, con la extensión lc.

Notas

[1]

Lo que viene siendo un filehandle de toda la vida.

[2]

También se usa ||, pero la precedencia es diferente.