"La Gaceta de Linux... haciendo de Linux algo un poco más divertido!"


Tutorial de Perl, segunda parte

Por Ben Okopnik

Traducción al español por David Chiner
el día 8 de Julio 2002, para La Gaceta de Linux


"Comprendí hasta qué punto era extenso el nicho ecológico entre el lenguaje C y los shells de UNIX. El C era bueno para manipular cosas complejas  (llámese 'manipulaxar'). Y los shells eran buenos para rematar tareas (lo que llamo 'salir del paso'). Pero quedaba una extensa área para la que no servía el C ni el shell, y ahí es para donde quería el Perl."
 -- Larry Wall, creador del Perl

Introducción

En la primera parte hablamos de algunos aspectos básicos de Perl (escribir un script, hash-bangs, estilo) y de algunos aspectos concretos (escalares, arrays, hashes, operadores y modos de entrecomillar). Este mes repasaremos esas herramientas intrínsecas de Perl que hacen tan fácil su uso desde la línea de comandos, así como sus equivalentes en scripts.También profundizaremos un poco más en el entrecomillado, y realizaremos una primera incursión en las regexes (expresiones regulares o REs) -una de las herramientas más poderosas en Perl, y una de las que merece un libro aparte.[1]
 
 

Modos de entrecomillar

La mayoría de vosotros conocerá los mecanismos estándar de entrecomillado en Unix: las comillas simples y dobles, que ya mencioné en mi anterior artículo, tienen casi la misma funcionalidad en Perl que en el shell. No obstante, a veces entrecomillar todos los metacaracteres puede ser un poco engorroso. Imaginad tened que imprimir una cadena como esta:

``/// Don't say "shan't," "can't," or "won't." ///''

¡Menudo problema! ¿Qué puede hacerse con un lío como este?

Pues bien, podríamos ponerla entre un puñado de comillas ("\"), pero sería un martirio -y de paso un caso de SEB ("Síndrome del Encaje de Bolillos"):

print '\`\`\/\/\/ Don\'t...

<Escalofrío> Obviamente no es la respuesta correcta. Para cosas como esta, Perl aporta mecanismos alternativos de entrecomillado:

q//        # Comillas simples
qq//       # Comillas dobles
qx//       # Comillas contrarias, para ejecuciones en shell
qw//       # Lista de palabras - útil para arrays densos

Observa también que el delimitador no tiene por qué ser '/' sino que puede ser cualquier carácter. Ahora nuestra tarea se vuelve un poco más fácil.

print q-``/// Don't say "shan't," "can't," or "won't." ///''-;

Sencillo, ¿verdad? Por cierto, esto algo que sólo usaría en un script; los mecanismos de interpretación de shell se harían un lío increíble si lo intentásemos desde la línea de comandos, especialmente con cosas como comillas contrarias y barras.
 
 

Invocación de Perl

"¡Escucha mi súplica, O Perl del Gran Poder!" No te preocupes; creo que esto era estándar en Perl3 y ahora está deprecado... :)

Si lo ejecutas desde la línea de comandos, el switch más usado al invocar Perl es '-e'; así dices a Perl que ejecute todo lo que venga detrás. De hecho, '-e' debe ser el último switch usado en la línea de comandos porque todo lo que viene detrás es considerado parte del script.

perl -we 'print "Los Dioses ordenan que empiece la Web.\n"'

"-w" es el switch de aviso que mencioné la última vez. Te detalla todos los errores no fatales de tu código, incluyendo las variables que definiste pero no usaste (inútiles para encontrar nombres de variables mal escritos) y muchas cosas más. Siempre (has oído bien, siempre) deberías usar "-w", tanto en la línea de comandos como en un script.

"-n" es el switch "bucle de no impresión" que hace que Perl itere línea a línea sobre el imput (algo así como "awk"). Si quieres imprimir una línea determinada, debes especificarlo con una condición:

perl -wne 'print if /vacaciones/' calendario.txt

Perl iterará sobre "calendario.txt" y escribirá cualquier línea que contenga la palabra "vacaciones", así podrás deprimirte con el poco tiempo libre que tienes realmente.

"-p" es la invocación de un "bucle de impresión", que actúa igual que "-n" salvo en que imprime todas las líneas sobre las que itera. Esto es múy útil para las operaciones tipo "sed", como modificar un archivo y reescribirlo (dentro de poco trataremos el operador de sustitución 's///'):

perl -wpe 's/vaciones/¡juerga!/' calendario.txt

Esto sustituye la primera ocurrencia de la palabra 'vacaciones' en cualquier línea (véase en "perldoc perlre" un análisis de los modificadores utilizados con 's///', como 'g'lobal.)

El switch "-i" funciona bien en combinación con cualquiera de los anteriores, según la acción deseada; te permite realizar una edición "in situ", es decir, realizar los cambios en el archivo especificado (opcionalmente realizar primero una copia de seguridad) en vez de imprimirlos en pantalla. Observa que no podemos limitarnos a clavar una "i" en la cadena "wpe". Exige un argumento opcional: la extensión que añadir a la copia de seguridad. El texto que sigue a la "i" especifica dicha extensión.

perl -i~ -wpe 's/vacaciones/¡Juerga!/' calendario.txt

La línea anterior producirá un "calendario.txt" con el texto modificado, y un "calendario.txt~" que es el archivo original. La "-i" sin extensión alguna sobreescribe el archivo original; esto es mucho mejor que producir un archivo modificado y renombrarlo como el original, ¡pero asegúrate de que tu código es correcto, o perderás tus datos originales!
 
 

Las RegExes o "Parece que el gato ha vuelto a caminar sobre mi teclado"

Las expresiones regulares, una de las herramientas más poderosas de las que dispone Perl, son el modo de reconocer casi cualquier disposición de caracteres. Aquí me limitaré (por fuerza) a lo elemental; si crees que necesitas más información, escarba en la "perlre" manpage que viene con Perl. Eso debería manterte ocupado un buen rato. :)

Las REs se usan para reconocer patrones, en general con los operadores "m//" (matching) y "s///" (sustitución). Observa que, al igual que en los mecanismos de entrecomillado, estos delimitadores no se limitan a '/'; de hecho en el operador de reconocimiento sólo es necesaria la 'm' si se usa un operador no predeterminado. De otro modo, basta con "//".

Estos son algunos de los metacaracteres utilizados con REs. Observa que hay muchos más; estos bastan para empezar:

.        Reconoce cualquier carácter excepto el salto de línea
^        Reconoce el principio de línea
$        Reconoce el final de línea
|        Alternancia (reconoce "izquierda|derecha|arriba|abajo|extremos")
*        Reconoce 0 o más veces
+        Reconoce 1 o más veces
?        Reconoce 0 o 1 veces
{n}      Reconoce exactamente n veces
{n,}     Reconoce al menos n veces
{n,m}    Reconoce al menos n pero no más que m veces
 

Como ejemplo, pongamos que tenemos un archivo con una lista de nombres:

Anne Bonney
Bartholomew Roberts
Charles Bellamy
Diego Grillo
Edward Teach
Francois Gautier
George Watling
Henry Every
Israel Hands
John Derdrake
KuoHsing Yeh
...

y queremos sustituir el nombre de pila por 'Captain'. Obviamente, podríamos recorrer el archivo con un bucle de impresión y efectuar la sustitución si cumple nuestro criterio:

s/^.+ /Captain /;

el signo ('^') reconoce el principio de la línea, el "+" dice "cualquier carácter, repetido 1 o más veces", y el espacio reconoce un espacio. Una vez encontramos lo que buscamos, lo sustituiremos por 'Captain' seguido de un espacio (como el string que sustituimos contiene uno, debemos reponerlo).

Supongamos que también sabemos que en algún lugar del archivo hay un par de nombres con apóstrofes (Francois L'Ollonais), y queremos eliminarlos (junto a todo lo que contenga caracteres que no sean letras). Ampliemos un poco la regex:

s/^[A-Z][a-z]* /Captain /;

Hemos usado los "[]" o especificadores "characters class" para reconocer un carácter entre 'A' y 'Z' (observa que con este mecanismo sólo se reconoce un carácter, ¡es una distinción muy importante!) seguido por un carácter entre la 'a', la 'z' y un asterisco, que de nuevo dice "cero o más del anterior  carácter".

Ooops, ¡espera! ¿Qué pasa con "KuoHsing"? La 'H' no sería reconocida porque los caracteres en mayúscula no estaban incluidos en el rango especificado. De acuerdo, modificaremos la regex:

s/^\w* /Captain /;

'\w' es otra "palabra carácter" -sólo reconoce un carácter- que incluye 'A-Z', 'a-z', y '_'. Es mejor que [A-Za-z_] porque utiliza el valor de $LOCALE (un valor de sistema) para determinar los caracteres que deberían pertenecer o no a palabras (y eso es importante en idiomas que no son el inglés). Además, '\w' es más fácil de escribir que '[A-Za-z_]'.

Probemos con algo un poco diferente: ¿Qué pasa si queremos reconocer todos los nombres de pila, pero ahora, en vez de sustituirlos, queremos ponerlos después de los apellidos, separar ambos con una coma, y preceder el apellido con la palabra 'Captain'? Con las regexes a nuestras órdenes, no es ningún problema.

s/^(\w*) (\w*)$/Captain $2, $1/;

Observa los paréntesis y las variables "$1"  y "$2":  los paréntesis "capturan"  la parte delimitada de la regex, a la que de este modo podemos referirnos mediante las variables (la primera parte capturada es $1, la segunda $2, etc.). Así queda la regex anterior en español:

Empezando por el principio de la línea  (empieza a capturar en $1)  reconoce cualquier "palabra carácter" repetida cero o más veces (fin de la captura)  y seguida por un espacio, (empieza a capturar en $2)  seguida por cualquier "palabra carácter" repetida cero o más veces (fin de la captura) hasta el final de la línea. Devuelve la palabra 'Captain' seguida de un espacio, seguido a su vez del valor de $2, una coma, un espacio, y el valor de $1.

Yo diría que las regexes son una forma muy compacta de decir todo lo anterior. En momentos como este, se hace obvio que Larry Wall es un lingüista profesional. :)

Estos son sólo sencillos ejemplos de construcción de una regex. Debo admitir que hago un poco de trampa: el tratamiento de nombres es probablemente uno de los mayores retos que existen y podría haber complicado este ejemplo todo lo que hubiese querido. Considerando que las posibilidades incluyen "John deJongh", "Jan M.
van de Geijn", "Kathleen O'Hara-Mears", "Siu Tim Au Yeung", "Nang-Soa-Anee Bongoj Niratpattanasai", y "Mjölby J. de Wærn" (no olvides usar patrones de reconocimiento LOCALES, ¿de acuerdo?), el campo es muy extenso y rico en retos. Seguramente Miss Niratpattanasai estaría de acuerdo ante algo como "John Smith". :)
 

He aquí un importante factor del mecanismo de las regex que no debemos olvidar: por defecto practica el "reconocimiento voraz". En otras palabras, dada una frase como

Acciones son amores, no besos ni apachurrones

una regex como

/A.*es/

reconocería lo siguiente:

Acciones son amores, no besos ni apachurrones
|___________________________________________|

Hmmm. Todo lo que va desde la primera 'A' (seguida por cero o más caracteres cualesquiera) a la última 'es'. Entonces, ¿cómo podemos reconocer sólo la primera aparición? Para contrarrestar la voracidad, Perl aporta un modificador de "generosidad" a calificadores como '*', '+' y '?':

/A.*?es/

Acciones son amores, no besos ni apachurrones
|______|

Ahora sí. Mucho mejor. Para futuras ocasiones, recuerda: si rompes una cadena reconociendo sus trozos con series de regexes, y los últimos "cachos" llegan vacíos, seguramente has tenido un problema de "voracidad".
 
 

La Variable/Buffer por defecto

Algunos de vosotros, especialmente los que en el pasado programaron algo, seguramente han sentido curiosidad ante estructuras de código como

print if /vacaciones/;

"Print qué si qué? ¿Dónde está la variable que queremos reconocer? ¿No debería ser algo como 'if $x == /holiday/', igual que en el shell?"

Me gusta que me hagas esta pregunta. :)

Perl utiliza un interesante concepto, presente en otros pocos lenguajes, de buffer por defecto -también llamado variable por defecto y espacio de patrón por defecto. De manera nada casual, se utiliza para construir bucles (cuando usamos la sintaxis "-n/-p" en la llamada a Perl, es la variable utilizada para guardar la línea que se recorre en cada momento) así como en la sustitución, el reconocimiento y muchos otros casos. La variable '$_' se utiliza por defecto para todo lo anterior; cuando se espera una variable y no se especifica ninguna, '$_' suele ser la "acusada". De hecho, '$_' es bastante difícil de explicar (aparece en tantos lugares que explicarla con un algoritmo parece imposible) pero una vez comprendes la idea, es maravillosamente fácil e intuitiva de usar.

Considera lo siguiente:

perl -wne 'if ( $_ =~ /Henry/ ) { print $_; }' piratas

Si una línea del archivo "piratas" coincide con "Henry", será impresa. Estupendo; pero ahora juguemos un poco al "Golf Perl" de aficionados -existe una competición entre hackers de Perl para ver cuántos golpes (de tecla) pueden eliminarse de un código sin que deje de funcionar.

Como ya sabemos que Perl guarda cada línea que lee en '$_', nos libraremos de cualquier declaración explícita de esta variable:

perl -wne 'if ( /Henry/ ) { print; }' piratas

Perl "sabe" que estamos buscando un patrón en la variable por defecto, y "sabe" que el operador "print" se aplica a la misma. Ahora, apliquemos un poco de estilo Perl:

perl -wne 'print if /Henry/' piratas

¿No es bonito? Perl te permite realmente escribir tu código con la condición después de la acción; igual que cuando hablas. Oh, y hemos eliminado el punto y coma del final porque no lo necesitamos: es un separador de sentencias, y no hay ninguna sentencia después de
"/Henry/".

<sonrisa> Para los que estáis jugando en casa, probad

perl -ne'/Henry/&&print' piratas

No debería ser tan difícil de imáginárselo; el operador '&&' en Perl funciona igual que en el shell. El Golf Perl es divertido, pero cuidado:  es fácil escribir un código que funcione pero requiera quebraderos de cabeza para entenderlo. No Hagáis Eso. Puede que mañana deba mantener vuestro código... igual que podéis tener que mantener el mío.
 

En el primer ejemplo, observad el "binding operator", '=~', que verifica concordancias con la variable definida. Esto es lo que debe usarse si se buscan concordancias con una variable que no sea "$_". También hay un operador '!~' de "concordancia negativa" que devuelve true si la concordancia no se cumple (lo contrario de '=~').

Observad también que los modificadores diponibles para sentencias simples, como las de antes, incluyen no sólo el "if" sino también "unless", "while", "until" y "for". Todos ellos, y muchos más se explican en la tercera parte...
 
 

Ben Okopnik
perl -we '$perl=0;JsP $perl "perl"; $perl->perl(0)'\
 2>&1|perl -ne '{print ((split//)[19,29,20,4,5,1,2,
15,13,14,12,52,5,21,12,52,8,5,14,1,6,37,12,52,75])}'



[1]. Y de hecho tiene uno: "Mastering Regular Expressions" de Jeffrey E. Friedl está considerado una referencia en el tema. Incluye algunos ejemplos maravillosos, y enseña literalmente al lector a "pensar en regex".
 

Referencias:

Man pages importantes de Perl (disponibles en cualquier sistema adecuadamente configurado):

perl      - introducción           perlfaq   - Perl FAQ
perltoc   - doc TOC                perldata  - estructuras de datos
perlsyn   - sintaxis               perlop    - operadores/precedencia
perlrun   - ejecución              perlfunc  - funciones predefinidas
perltrap  - trampas para incautos  perlstyle - guía de estilo

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


Copyright © 2001, Ben Okopnik.
Copying license http://www.linuxgazette.com/copying.html
Publicado en el número 64 de Linux Gazette, Marzo 2001