"Gazeta Linux...haciendo Linux un poco más divertido!"


Aprendiendo Perl, parte 4

Por: Ben Okopnik

Traducción al español por Octavio Melendres
el día 16 de Octubre 2002, para La Gaceta de Linux


La revolución del Internet fué fundada en base a sistemas abierto; un sistema abierto es un tipo de software al que puedes mirar, una caja que puedes desempacar y jugar. No es acerca de archivos binarios secretos o "crippleware" o shareware barato. Si todas las personas tuvieran siempre software oculto, tú no tendrías 1/100 del software útile que tienes ahora.

Y no tendrías Perl.
   -- Tom Christiansen

Resúmen

Si has seguido ésta serie de artículos, tienes ahora construídas algunas herramientas - que probablemente ya has experimentado - y que pueden ser utilizadas para construír scripts. Por lo tanto, éste mes vamos a llevarte a escribir algunos, en particular usando la función "open" que nos permite asignar manejadores o "filehandles" para archivos, sockets, y pipes. "open" es un importante pieza de construcción en el uso de Perl, por lo cual veremos la función en detalle.
 

Ejercisios

La última vez, mencioné haber escrito algunos scripts de práctica. Vamos a ver algunas maneraras posibles de hacer ésto:

El primero fué un script que capturaba un número como entrada, y escribía "Hola!" el número de veces. También validaba la captura para evitar la entrada de caracteres ilegales (no-numéricos). A continuación hay un buen ejemplo, enviado por David Zhuwao:

#! /usr/bin/perl -w

#@autor David Zhuwao
#@desde Apr/19/'01

print "Escribe el número de ciclos: ";

#Captura la entrada e asígnala a una variable.
chomp ($input = <>);

# checa la entrada por caracteres no-numéricos.
if ($input !~ m/\D/ && length($input) > 0) {
    for ($i = 0; $i < $input; $i++) {
        print "Hola!\n";
    }
} else {
    print "Entrada no-numérica.\n";
}

Primero, señalo buenas prácticas de programación: David ha usado el switch "-w" con el cual Perl señala si hubo algunas advertencias de compilación "compile-time warnings" - un hábito excelente. También el autor usó espacios en blanco (líneas en blanco y tabulación) efectivamente con el fin de hacer el código fácil de leer, así como agregó comentarios libremente. También, en vez de checar por la presencia de un número (que podría crear también un problema con entradas como "1A"), él prueba por caracteres no-numéricos y la longitud mayor de cero - bien pensado!.

Puntos menores (nota que ninguno de éstos son problemas, simplemente observaciones): al usar el operador de comparación, "m//", la "m" no es necesaria a menos que el delimitador sea algo diferente a "/". Así como el uso del loop de Perl "for/foreach", sería más compacto que el loop "if", tipo el lenguaje-C, y aún realizando la misma función.

print "Hola!\n" for 1 .. $input;

También haría innecesaria el uso de la variable "$i". Aparte de éstos puntos menores - bien hecho, David!
 

Esta es otra manera:

#!/usr/bin/perl -w

print "Por favor inserta un número: ";
chomp ( $a = <> );

print "Hello!\n" x $a if $a =~ /^\d+$/;

A diferencia de la versión de David, mi versión no muestra un mensaje de error; simplemente repite la pregunta si el valor dado no es numérico. También, en vez de validar por caracteres no-numéricos, yo valido el string desde su inicio a su fin por solo contenido numérico. Cualquiera de éstas técnicas funciona bien. También, en vez de usar un ciclo explícito, yo uso el operador de Perl "x", que repetirá simplemente la instrucción de escritura precedente por "$a" veces.
 

...Y, una vez más...

Explicamos otro script, la segunda sugerencia del mes pasado: un script que toma una hora como entrada (0-23) y dice "Good morning", "Dobriy den'", "Guten Abend", o "Buenas noches" como resultado (Voy a usar solo inglés en el script para evitar confusión.)

#!/usr/bin/perl -w

$_ = <>;

if    ( /^[0-6]$/          )   { print "Good night\n";     }
elsif ( /^[7-9]$|^1[0-2]$/ )   { print "Good morning\n";   }
elsif ( /^1[3-8]$/         )   { print "Good day\n";       }
elsif ( /^19$|^2[0-3]$/    )   { print "Good evening\n";   }
else                           { print "Entrada incorrecta!\n"; }

En la superficie, éste script parece muy fácil - y, realmente es - pero contiene algunas consideraciones ocultas que quiero mencionar. Primero, porqué necesitamos las validaciones para todo de "inicio de línea" y de "fin de línea"? obviamente, queremos evitar confundir el "1" del "12" - pero que puede salir mal con /1[3-8]/?

Lo que puede salir mal es un tipo de dato equivocado o mis-type. Algo que no importa demasiado en éste caso, pero siendo paranoico acerca de las validaciones es una buena idea en general. :) Que sucede si un usuario, mientras intenta teclear "14", ha tecleado "114"? Sin éstos "límites", la entrada será "11" - e obtendremos la respuesta equivocada.

OK - porqué no usé pruebas numéricas en vece de equivalencia? quiero decir, después de todo, solo se trata de números... no sería más fácil e obvio? Si, pero. Lo que sucede si usamos pruebas numéricas y el usuario teclea "joe"? Obtenemos un error adicional con nuestro mensaje "Entrada incorrecta!":

Argument "joe\n" isn't numeric in gt at -e line 5, <> chunk 1.

Como una buena práctica de programación, queremos que el usuario vea solo los mensajes que nosotros generamos (o esperamos generar); no debería haber errores que genera el programa propio. Una comparación "regex" no va a ser 'sorprendida' con una entrada no-numérica; simplemente regresará un 0 (no-igualdad) y pasa al siguiente "elseif" o "else", que es la clausura que 'recibe-todo'. Todo lo demás que no resulta positivo en una de las primeras cuatro pruebas es una entrada inválida - que es lo que nosotros queremos que se reporte.
 

Manejo de archivos

Una capacidad importante de cualquier lenguaje es la del manejo de archivos. En Perl, esto es relativamente fácil, pero hay un par de consideraciones donde se requiere que pongas atención.

# La manera correcta
open FILE, "/etc/passwd" or die "No puedo abrir /etc/password: $!\n";

Estas son algunas maneras equivocadas o dudosas de hacer ésto:

# No prueba por el resultado obtenido
open FILE, "/etc/passwd";

# Ignora el error regresado por el shell via la variable '$!'
open FILE, "/etc/passwd" or die "No puedo abrir /etc/password\n";

# Usa el "logical or" para verificar - puede ser un problema debido a conflictos precedentes
open FILE, "/etc/passwd" || die "No puedo abrir /etc/password: $!\n";

Por default, los archivos son abiertos solo lectura. Otros métodos son especificados al agregar un obvio "modificador" al nombre del archivo especificado.

# Abrir para escritura - cualquier cosa escrita re-escribirá el contenido del archivo
open FILE, ">/etc/passwd" or die "No puedo abrir /etc/password: $!\n";

# Abrir para agregación - la información será agregada al final del archivo
open FILE, ">>/etc/passwd" or die "No puedo abrir /etc/password: $!\n";

# Abrir para lectura y escritura
open FILE, "+>/etc/passwd" or die "No puedo abrir /etc/password: $!\n";

# Abrir para lectura y agregación
open FILE, "+>>/etc/passwd" or die "No puedo abrir /etc/password: $!\n";

DEspués de haber creado el manejador de archivo o filehandle ("FILE", en los casos previos), ahora puedes utilizarlos de la siguiente manera:

while ( <FILE> ) {
    print;   # Esto se ciclará en el archivo y desplegará todas las líneas contenidas
}

O lo puedes hacer de la siguiente manera, si lo que quieres es desplegar el contenido en una sola vuelta:

print ;

Escribir al archivo es igual de fácil:

print FILE "Esta línea será escrita en el archivo.\n";

Recuerda que el método por default es de "solo lectura". Usualmente me gusta mencionar ésto escribiendolo de ésta manera:

open FILE, "</etc/passwd" or die "No puedo abrir /etc/password: $!\n";

Nota el signo de "<" frente al nombre del archivo: Perl no encuentra problema con ésto, y se convierte en un buen recuerdo visual. La frase "dejando migajas" describe ésta metodología, y tiene que ver con la idea de hacer lo que escribes lo más obvio posible para que quisiera seguir el código. No olvides que esa persona "siguiendo" puede ser tú, un par de años después de haber escrito el código..

Perl automáticamente cierra los manejadores de archivo cuando el script termina.. o al menos, se supone. Por lo que te he mencionado, algunos sistemas operativos tienen un problema con ésto - por lo tanto, no es una mala idea (mejor dicho necesidad) de ejecutar una operación "close" explícita a manejadores de archivos (filehandles) abiertos:

close FILE or die "No puedo cerrar FILE: $!\n";

A propósito, el efecto de la función "die" debe de ser relativamente obvio: Escribe la cadena de caracteres especificada (string) y sale del programa.

No hagas ésto, a menos que estés en la última línea del script:

close;

Esto cierra todos los manejadores de archivo o filehandles... incluyendo STDIN, STDOUT, y STDERR (las cadenas standard), que dejan tu programa tonto, sordo, y ciego. También, no puedes especificar múltiples manejadores en un close, por lo que debes cerrar de uno por uno:

close Fh1 or die "No puedo cerrar Fh1: $!\n";
close Fh2 or die "No puedo cerrar Fh2: $!\n";
close Fh3 or die "No puedo cerrar Fh3: $!\n";
close Fh4 or die "No puedo cerrar Fh4: $!\n";

Por supuesto, puedes hacer ésto:

for ( qw/Fh1 Fh2 Fh3 Fh4/ ) { close $_ or die "No puedo cerrar $_: $!\n"; }

:) Esto es Perl para tí; Hay más de una sola manera de hacerlo...
 

Usando esos manejadores

Digamos que tienes dos archivos con información financiera - tasa de préstamos en una, el tipo y la cantidad de tus prestamos en la otra - y quieres obtener cuanto interés pagarás, y escribir el resultado en un archivo. Aquí está la información:

tasas.txt

Casa    9%
Auto     16%
Barca    19%
Misc    21%
prestamos.txt

Chevy   AUTO     8000
BMW     auto     22000
Scarab  BARCA    150000
Pearson barca    8000
Piano   Misc    4000

Muy bien, hay que hacer que ésto suceda:

#!/usr/bin/perl -w

open Tasas, "<tasas.txt" or die "No puedo abrir tasas.txt: $!\n";
open Prestamos, "<prestamos.txt" or die "No puedo abrir prestamos.txt: $!\n";
open Total, ">total.txt" or die "No puedo abrir total.txt: $!\n";

while ( <Tasas> ) {
    # Eliminar los signos '%'
    tr/%//d;
    # Separa cada línea en un arreglo
    @tasas = split;
    # Crear un hash con tipos de préstamos como llave y porcentajes como valores
    $r{lc $tasas[0]} = $tasas[1] / 100;
}

while ( <Prestamos> ) {
    # Separa cada línea en un arreglo
    @prestamos = split;
    # Escribe el prestamo y la cantidad de interés en el manejador "Total";
    # calcular al multiplicar la cantidad total por el valor regresado
    # por la llave del hash.
    print Total "$prestamos[0]\t\t\$", $prestamos[2] * $r{lc$prestamos[1]}, "\n";
}

# Cerrar los manejadores de archivo - no es necesario, pero no está de más
for ( qw/Tasas Prestamos Total/ ) {
    close $_ or die "No puedo cerrar $_: $!\n";
}


Obviamente, Perl es muy bueno en éste tipo de cosas: hicimos el trabajo con una docena de líneas de código. Los comentarios tomaron la mayoría del espacio. :)
 

Aqui está otro ejemplo, uno que venía acerca de un resultado de uno de mis artículos acerca de procmail ("No More Spam!" en LG#62). El script original "blacklist" que fué invocado desde Mutt extrae la dirección E-mail del spammer via "formail", para después enviar el resultado a la dirección "user@host"con un script de Perl de una sola línea. Tomó el Email spam completo con una entrada 'piped'. Martin Bock, sin embargo, sugirió todo con Perl, después de intercambiar algunos emails con él, salí con el siguiente script basado en su idea:

#!/usr/bin/perl -wln
# El switch '-n' hace que el script lea las entradas línea por línea a la vez--
# El script completa es executado línea por línea;
# el '-l' habilita procesamiento por línea, que agrega salto de línea a las líneas
# que se imprimen.

# Si la línea se iguala a la expresión, entonces...
if ( s/^From: .*?(\w\S+@\S+\w).*/$1/ ) {
    # Abre la "blacklist" con el filehandle "OUT" en modo de append
    open OUT, ">>$ENV{HOME}/.mutt/blacklist" or die "Aargh: $!\n";
    # Imprimir $_ a el filehandle
    print OUT;
    # Cierra
    close OUT or die "Aargh: $!\n";
    # Salir del ciclo
    last;
}


El operador de substitución en la primera línea no es perfecto - puedo escribir direcciondes de E-mail incorrectas que pudieran causar que el script no funcione correctamnete - pero funciona bien con variaciones como:
one-two@three-four.net
<one-two@three-four.net>
joe.blow.from.whatever@whoever.that-might-be.com (Joe Blow)
Joe Blow <joe.blow.from.whatever@whoever.that-might-be.com>
[ The artist formerly known as squiggle ] <prince@loco.net>
(Joe) joe-blow.wild@hell.and.gone.com ["Wildman"]

Para "decodificar" lo que la expresión regular dice, consulta la manpage "perlre". No es tan complicado. :) Pista: busca por la palabra "greed" para entender la expresión ".*?", y busca por la palabra "capture" para entender el la construción "(...) / $1" . Both of them are very important concepts, and both have been mentioned in this series.

Esta es una versión más compacta que la mostrada anteriormente (y mucho menos leíble); notar que el mecanismo es de alguna manera diferente:

#!/usr/bin/perl -wln
BEGIN { open OUT, ">>$ENV{HOME}/.mutt/blacklist" or die "Aargh: $!\n"; }
if ( s/^From: .*?(\w\S+@\S+\w).*/$1/ ) { print OUT; close OUT; last; }

El bloque BEGIN de la primera línea del script corre solo una vez durante la ejecución, si importar el hecho que el script se cicla muchas veces; es muy similar a la misma construcción en Auk.
 

En la próxima ocasión

El próximo mes, veremos algunos modos de ahorrarnos trabajo al usar we'll be looking at a few nifty ways to save ourselves work modulos: código útile que otras personas han escrito de la Comprehensive Perl Archive Network (CPAN). También veremos como Perl puede ser usado para imlplementar CGI, la 'Common Gateway Interface' - el mecanismo que funciona detrás de la escena del web. Hasta entonce, aquí estan algunas cosas para entretenerse:

Esccribe un script que abre "/etc/services" y cuente cuantos puertos estan listados que soportan las operaciones UDP, y cuantos puertos TCP. Escribe el nombre de los servicios en un archivo llamado "udp.txt" y "tcp.txt", y escribe los totaes en pantalla.

Abre dos archivos e intercambia el contenido.

Lee "/var/log/messages" y escribe y escribe todas la líneas que contienen dentro la palabra "fail", "terminated/terminating", o " no ". Hacerla
sensible a mayúsculas y minusculas.
 

Hasta entonces-

perl -we 'print "Nos vemos el próximo mes!"'
 

Ben Okopnik
perl -we'print reverse split//,"rekcah lreP rehtona tsuJ"'
Referencias:

Páginas del manual de PErl relevantes(disponibles en cualquier sistema configurado con pro-Perl-y
):

perl      - overview              perlfaq   - Perl FAQ
perltoc   - doc TOC               perldata  - data structures
perlsyn   - syntax                perlop    - operators/precedence
perlrun   - execution             perlfunc  - builtin functions
perltrap  - traps for the unwary  perlstyle - style guide

"perldoc", "perldoc -q" and "perldoc -f"

Ben Okopnik

A cyberjack-of-all-trades, Ben wanders the world in his 38' sailboat, building networks and hacking on hardware and software whenever he runs out of cruising money. He's been playing and working with computers since the Elder Days (anybody remember the Elf II?), and isn't about to stop any time soon.


Copyright © 2001, Ben Okopnik.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 67 of Linux Gazette, June 2001