Conversa de Bar Parte II
- Mozo! Trae un "chopps" e dos "pastel". Mi amigo hoy no va a beber porque él finalmente está siendo presentado a un verdadero sistema operativo y todavia tiene mucha cosa para aprender!
- Y entonces, amigo, está entendiendo todo lo que te expliqué hasta ahora?
- Entendiendo estoy, pero no vi nada práctico en eso...
- Calma, lo que te dije hasta ahora, sirve como base para lo que ha de venir de aqui en adelante. Vamos a usar estas herramientas que vimos para montar programas estructurados, que el
Shell te permite. Entonces tu verás por qué hasta en la TV ya hubo un programa llamado "El
Shell es el Límite".
- Para comenzar vamos a hablar de los comandos de la familia
grep.
-
grep? No conozco ningún término en inglés con este nombre...
- Por supuesto,
grep es un acrónimo
Global Regular Expression Print, que usa expresiones regulares para buscar la ocurrencia de cadenas de caracteres en la entrada definida (si bien que hay una leyenda sobre como este comando fue bautizado: en el editor de textos "ed", el abuelo del "vim", el comando usado para buscar era g/_expresión regular_/p, en inglés g/_re_/p). Por hablar en expresiones regulares (o
regexp), el portal
Aurélio Marinho Jargas tiene todas las dicas en su página (inclusive tutoriales) que abordan el tema. Si estás realmente con ganas de aprender a programar en
Shell,
Perl,
Python, ... debes leer estos artículos que te ayudarán a resolver lo que nos espera más adelante.
Me quedo con el grep, tú con la gripe
Eso de la gripe es un sólo un decir! Simplemente un pretexto para pedir unas bebidas fuertes. Volviendo a nuestro tema, te dije que que el
grep busca cadenas de caracteres dentro de una entrada definida, pero en realidad... que viene a ser una "entrada definida"? Bueno, existen varias formas de definir la entrada del comando
grep. Veamos:
Buscando en un archivo:
%TERMINAL_INI%
$ grep rafael /etc/passwd
%TERMINAL_FIM%
buscando en varios archivos:
%TERMINAL_INI%
$ grep grep *.sh
%TERMINAL_FIM%
Buscando en la salida de un comando:
%TERMINAL_INI%
$ who | grep Pelegrino
%TERMINAL_FIM%
En el 1º ejemplo, el más simple, busqué la palabra rafael en qualquier lugar del archivo
/etc/passwd. Si quisiera buscarla como un login name, o sea, solamente en el inicio de los registros de este archivo, deberia hacer:
%TERMINAL_INI%
$ grep '^rafael' /etc/passwd
%TERMINAL_FIM%
Y para que sirve este acento circunflejo y los apóstrofes? me vas a preguntar. El circunflejo (
^), si hubieras leído los artículos anteriores sobre expresiones regulares que te dije, sabrías que sirven para limitar la búsqueda al inicio de cada línea, y los apóstrofes (
') sirven para que el
Shell no interprete este circunflejo, dejándolo pasar intacto para el comando
grep.
Mira que bien! El
grep acepta como entrada, la salida de otro comando redireccionado por un
pipe (esto es muy común en
Shell y es un tremendo acelerador de la ejecución de comandos, ya que actúa como si la salida de un programa fuera guardada en disco y el segundo programa leyera este archivo generado), de esta forma, en el 3º ejemplo, el comando
who listó las personas "logadas" en la misma máquina que tú (no te olvides jamás: el
Linux es multiusuario) y el
grep fue usado para verificar si Pelegrino estaba trabajando o simplemente "haciendo sebo".
La familia grep
Este comando
grep es muy conocido, pues es usado con mucha frecuencia. Algo que muchas personas desconocen es que existen tres comandos en la familia
grep, que son:
Las principales diferencias entre los 3 son:
- El
grep puede o no, usar expresiones regulares simples, sin embargo en caso de no usarlas, el fgrep es mejor, por ser más rápido;
- El
egrep ("e" de extended, extendido) es muy poderoso en el uso de expresiones regulares. Por ser el más lento de la familia, solo debe ser usado cuando sea necesario la construcción de una expresión regular que no sea aceptada por el grep;
- El
fgrep ("f" de fast, rápido, o de "file", archivo) como el nombre dice, es el rapidito de la familia, ejecuta el servicio de forma muy veloz (a veces es cerca de 30% más rápido que el grep y 50% más que el egrep), sin embargo no permite el uso de expresiones regulares en la búsqueda.
%ATENCIÓN_INI%
Todo lo que fue dicho arriba sobre la velocidad, solamente se aplica a la familia de comandos
grep del
Unix. En
Linux el
grep es siempre más veloz, ya que los otros dos (
fgrep y
egrep) son
scripts en
Shell que llamam al primero y, ya estoy adelantando,
no me gusta nada esta solución.
%ATENCIÓN_FIM%
- Ahora que ya conoces las diferencias entre los miembros de la familia, dime: que te parecen los tres ejemplos que te dí antes de las explicaciones?
- Me pareció que el
fgrep resolvería tu problema de forma más veloz que el
grep.
- Perfecto! Estoy notando que estás bien atento! Estás entendiendo lo que te estoy explicando! Entonces vamos a ver más ejemplos, para aclarar de una vez por todas las diferencias del uso de los miembros de esta familia.
Ejemplos
Yo sé que en un archivo existe un texto hablando sobre
Linux solo no sé si está escrito con
L mayúsculo o
l minúsculo. Puedo hacer la búsqueda de dos formas:
%TERMINAL_INI%
$ egrep (Linux | linux) archivo.txt
%TERMINAL_FIM%
ou
%TERMINAL_INI%
$ grep [Ll]inux archivo.txt
%TERMINAL_FIM%
En el primer caso, la expresión regular compleja
"(Linux | linux)" usa los paréntesis para agrupar las opciones y la barra vertical (
|) como un "o" lógico, o sea, estoy buscando
Linux el
linux.
En el segundo, la expresión regular
[Ll]inux significa: comenzando por
L o
l seguido de
inux. Como esta expresión es más simple, el
grep consigue resolverla, por lo tanto creo mejor usar la segunda forma, ya que el
egrep dejaría la búsqueda más lenta.
Otro ejemplo. Para listar todos los subdirectórios del directório corriente, basta hacer:
%TERMINAL_INI%
$ ls -l | grep '^d'%OUT_INI%
drwxr-xr-x 3 root root 4096 Dec 18 2000 doc
drwxr-xr-x 11 root root 4096 Jul 13 18:58 freeciv
drwxr-xr-x 3 root root 4096 Oct 17 2000 gimp
drwxr-xr-x 3 root root 4096 Aug 8 2000 gnome
drwxr-xr-x 2 root root 4096 Aug 8 2000 idl
drwxrwxr-x 14 root root 4096 Jul 13 18:58 locale
drwxrwxr-x 12 root root 4096 Jan 14 2000 lyx
drwxrwxr-x 3 root root 4096 Jan 17 2000 pixmaps
drwxr-xr-x 3 root root 4096 Jul 2 20:30 scribus
drwxrwxr-x 3 root root 4096 Jan 17 2000 sounds
drwxr-xr-x 3 root root 4096 Dec 18 2000 xine%OUT_FIM%
%TERMINAL_FIM%
En el ejemplo que acabamos de ver, el circunflejo (
^) sirvió para limitar la pesquisa a la primera posición de la salida del
ls detallado. los apóstrofes fueron colocados para que el
Shell no "viera" el circunflejo (
^).
Vamos a ver otro. Sabemos que las cuatro primeras posiciones posibles de un
ls -l de un archivo común (archivo común! No directório, ni
link, ni ...) deben ser:
Así, para descubrir todos los archivos ejecutables en un determinado directório deberia hacer:
%TERMINAL_INI%
$ ls -la | egrep '^-..(x|s)'%OUT_INI%
-rwxr-xr-x 1 root root 2875 Jun 18 19:38 rc
-rwxr-xr-x 1 root root 857 Aug 9 22:03 rc.local
-rwxr-xr-x 1 root root 18453 Jul 6 17:28 rc.sysinit%OUT_FIM%
%TERMINAL_FIM%
Donde nuevamente usamos el circunflejo (
^) para limitar la búsqueda al inicio de cada línea, entonces las líneas listadas serán las que comiezan por un trazo (
-), seguido de cualquier cosa (el punto cuando usado como una expresión regular significa cualquier cosa), nuevamente seguido de cualquer cosa, y siguiendo un
x o un
s.
Obtendríamos el mismo resultado si hicieramos:
%TERMINAL_INI%
$ ls -la | grep '^-..[xs]'
%TERMINAL_FIM%
y agilizaríamos la búsqueda.
Vamos a montar una "CDteca"
Vamos a comenzar a desarrollar programas, me parece que montar un banco de datos de músicas es muy útil y didático (y además práctico, en estos tiempos de
downloads de mp3 y "quemadores" de CDs). No te olvides que, de la misma forma que vamos desarrollar una cantidad de programas para organizar tus CDs de música, con pequeñas adaptaciones, puedes hacer lo mismo con los CDs de
software que vienen con la
Linux Magazine y otros que compres o quemes, si disponibilizas este banco de
software para todos los que trabajan contigo (el
Linux es multiusuario, y como tal debe ser explotado), ganarás muchos puntos con tu adorado jefe.
- Un momento! De donde voy la recibir los datos de los CDs?
- Inicialmente, te voy a mostrar como tu programa puede recibir parámetros de quién lo esté ejecutando y en breve, te enseñaré a leer los datos por la pantalla o de un archivo.
Pasando parámetros
El visual del archivo musicas será el siguiente:
nombre del álbum^intérprete1~nombre de la música1:..:intérprete~nombre de la música
o sea, el nombre del álbum será separado por un circunflejo (
^) del resto del registro, que está formado por diversos grupos compuestos por el intérprete de cada música del CD y la respectiva música interpretada. Estos grupos son separados entre sí por dos-puntos (
:) e internamente, el intérprete será separado por un til (
~) del nombre de la música.
Escribiré un programa llamado
musinc, que incluirá registros en mi archivo músicas. Pasaré el contenido de cada álbum como parâmetro en la llamada del programa de la siguiente forma:
%TERMINAL_INI%
$ musinc "álbum^interprete~música:interprete~música:..."
%TERMINAL_FIM%
De esta forma el programa
musinc estará recibiendo los datos de cada álbum como si fuera una variable. La única diferencia entre un parámetro recibido y una variable es que los primeros reciben nombres numéricos (nombre numérico suena algo raro, no?). Lo que quise decir es que sus nombres son formados por un y solamente un algarismo), el sea
$1, $2, $3, ..., $9. Vamos, antes de todo, hacer un test:
Ejemplos
%TERMINAL_INI%
$ cat test
%OUT_INI%#!/bin/bash
# Programa para verificar el pasaje de parámetros
echo "1o. param -> $1"
echo "2o. param -> $2"
echo "3o. param -> $3"%OUT_FIM%
%TERMINAL_FIM%
Vamos la ejecutarlo:
%TERMINAL_INI%
$ test pasando parámetros para verificar
%OUT_INI%bash: test: cannot execute%OUT_FIM%
%TERMINAL_FIM%
Opa! Me olvidé de hacerlo ejecutable. Voy a hacerlo de forma que permita que todos puedan ejecutarlo y en seguida voy a testarlo:
%TERMINAL_INI%
$ chmod 755 test
$ test pasando parámetros para verificar
%OUT_INI%1o. param -> pasando
2o. param -> parámetros
3o. param -> para%OUT_FIM%
%TERMINAL_FIM%
Repare que la palabra
verificar, que sería el cuarto parámetro, no fue listada. Esto sucedió justamente porque el programa test solo listaba los tres primeros parámetros. Vamos ejecutarlo de otra forma:
%TERMINAL_INI%
$ test "pasando parámetros" para verificar%OUT_INI%
1o. param -> pasando parámetros
2o. param -> para
3o. param -> verificar%OUT_FIM%
%TERMINAL_FIM%
Las comillas no dejaron que el
Shell viese el espacio en blanco entre las palabras y las consideró como un único parámetro.
Observaciones sobre paramétros
Ya que estamos hablando en pasaje de parámetros observa bien lo siguiente:
| Significado de las Principales Variables Referentes a los Parámetros |
| Variable |
Significado |
$0 |
Contiene el nombre del programa |
$# |
Contiene la cuantidad de parámetros pasados |
$* |
Contiene el conjunto de todos los parámetros (muy parecido con $@) |
Ejemplos
Vamos a alterar el programa
test para usar las variables que acabamos de ver. Vamos hacerlo así:
%TERMINAL_INI%
$ cat test
%OUT_INI%#!/bin/bash
# Programa para verifivar el pasaje de parámetros (2a. Versao)
echo El programa $0 recibió $# parámetros
echo "1o. param -> $1"
echo "2o. param -> $2"
echo "3o. param -> $3"
echo Todos de una sola \"bolada\": $*%OUT_FIM%
%TERMINAL_FIM%
Repare que antes de las comillas usé una barra invertida para esconderlas de la interpretación del
Shell (si no usase las contrabarras las comillas no aparecerian). Vamos a ejecutarlo:
%TERMINAL_INI%
$ test pasando parámetros para verificar
%OUT_INI%El programa test recibió 4 parámetros
1o. paarm -> pasando
2o. param -> parámetros
3o. param -> para
Todos de una sola "bolada": pasando parámetros para verificar%OUT_FIM%
%TERMINAL_FIM%
Como ya dije, los parámetros reciben números de
1 a
9, pero eso no significa que no puedo usar más de 9 parámetros, significa solamente que solo puedo direccionar 9. Vamos a verificar eso:
Ejemplo:
%TERMINAL_INI%
$ cat test
%OUT_INI%#!/bin/bash
# Programa para verificar el pasaje de parámetros (3a. Versión)
echo El programa $0 recebió $# parámetros
echo "11o. param -> $11"
shift
echo "2o. param -> $1"
shift 2
echo "4o. Param -> $1"%OUT_FIM%
%TERMINAL_FIM%
Vamos a ejecutarlo:
%TERMINAL_INI%
$ test pasando parámetros para verificar
%OUT_INI%El programa test recebió 4 parámetros que son:
11o. param -> pasando1
2o. param -> parámetros
4o. param -> verificar%OUT_FIM%
%TERMINAL_FIM%
Dos cosas muy interesantes en este
script:
- Para mostrar que los nombres de los parámetros varían de
$1 a $9 hice un echo $11 y que pasó? El Shell interpretó como que era $1 seguido del algarismo 1 y listó pasando1;
- El comando
shift cuya sintáxis es shift n, pudiendo el n asumir cualquier valor numérico (sin embargo su default es 1, como en el ejemplo dado), desprecia los n primeros parámetros, devolviendo el parámetro de orden n+1, el primero o sea, el $1.
Bueno, ahora que ya sabes más sobre pasaje de parámetros que yo mismo, vamos a voltar a nuestra "CDteca" para hacer el
script para incluir los CDs en mi banco llamado
musicas. El programa es muy simple (como todo en
Shell) y voy a listarlo para que lo veas:
Ejemplos
%TERMINAL_INI%
$ cat musinc
%OUT_INI%#!/bin/bash
# Incluye CDs (versión 1)
#
echo $1 >> musicas%OUT_FIM%
%TERMINAL_FIM%
El
script es fácil y funcional, limítome a anexar al fin del archivo
musicas el parámetro recibido. Vamos a incluir 3 álbunes para ver si funciona (para no hacerlo muy aburrido, voy a suponer que en cada CD existem solamente 2 músicas):
%TERMINAL_INI%
$ musinc "album 3^Artista5~Musica5:Artista6~Musica5"
$ musinc "album 1^Artista1~Musica1:Artista2~Musica2"
$ musinc "album 2^Artista3~Musica3:Artista4~Musica4"
%TERMINAL_FIM%
Muestro ahora el contenido de musicas.
%TERMINAL_INI%
$ cat musicas
%OUT_INI%album 3^Artista5~Musica5:Artista6~Musica6
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4%OUT_FIM%
%TERMINAL_FIM%
No está funcional como esperaba que quedase... podía haber quedado mejor. Los álbunes están fuera de orden, dificultando la búsqueda. Vamos a alterar nuestro
script y después probarlo nuevamente:
%TERMINAL_INI%
$ cat musinc
%OUT_INI%#!/bin/bash
# Incluye CDs (versión 2)
#
echo $1 >> musicas
sort musicas -o musicas%OUT_FIM%
%TERMINAL_FIM%
Vamos a cadastrar un álbum más:
%TERMINAL_INI%
$ musinc "album 4^Artista7~Musica7:Artista8~Musica8"
%TERMINAL_FIM%
Ahora vamos a ver lo que pasó con el archivo
musicas:
%TERMINAL_INI%
$ cat musicas
%OUT_INI%album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica5
album 4^Artista7~Musica7:Artista8~Musica8%OUT_FIM%
%TERMINAL_FIM%
Simplemente incluí una línea que clasifica el archivo
musicas dándole salida en él mismo (para eso sirve la opción
-o), después de que cada álbum fue incluído.
Opa! Ahora está quedando bien y casi funcional. Pero atención, no se desespere! Esta no es la versión final. El programa quedará mucho mejor y más amigable, en una nueva versión que haremos después que aprendamos la adquirir los datos de la pantalla y a formatear la entrada
Ejemplos
Usar el comando
cat para listar no es uma buena idea, vamos a hacer un programa llamado=muslist=, para listar un álbum cuyo nombre será pasado como un parámetro:
%TERMINAL_INI%
$ cat muslist
%OUT_INI%#!/bin/bash
# Consulta CDs (versión 1)
#
grep $1 musicas%OUT_FIM%
%TERMINAL_FIM%
Vamos a ejecutarlo, buscando el
album 2. Como ya vimos anteriormente, para pasar la cadena de caracteres
album 2 es necesario protegerla de la interpretación del
Shell, así él no la interpreta como dos parámetros separados. Vamos a hacer de la siguiente forma:
%TERMINAL_INI%
$ muslist "álbum 2"
%OUT_INI%grep: can't open 2
musicas: album 1^Artista1~Musica1:Artista2~Musica2
musicas: album 2^Artista3~Musica3:Artista4~Musica4
musicas: album 3^Artista5~Musica5:Artista6~Musica6
musicas: album 4^Artista7~Musica7:Artista8~Musica8%OUT_FIM%
%TERMINAL_FIM%
Que desorden! Donde está el error?. Tuve el cuidado de colocar el parámetro pasado entre comillas, para que el
Shell no lo diviera en dos!
Si, pero nota ahora como el
grep está siendo ejecutado:
grep $1 musicas
Aunque coloque
álbum 2 entre comillas, para que fuera visto como un único parámetro, cuando el
$1 fue pasado por el
Shell para el comando
grep, lo transformó en dos argumentos. Así, el contenido final de la línea que el comando
grep ejecutó fue el siguiente:
grep album 2 musicas
Como la sintáxis del
grep es:
=grep
[arch1, arch2, ..., archn]=
el grep entendió que debería procurar la cadena de caracteres album en los archivos 2 y musicas, Por no existir el archivo 2 generó el error, y por encontrar la palabra album en todos los registros de musicas, listó todos ellos.
%DICA_INI%
Siempre que la cadena de caracteres a ser pasada para el comando grep posea blancos o TAB, mismo que dentro de variables, colóquela siempre entre comillas para evitar que las palabras después del primer espacio en blanco o TAB sean interpretadas como nombres de archivos.
%DICA_FIM%
Por otro lado, es mejor ignorar las mayúsculas y minúsculas en la búsqueda. Resolveríamos los dos problemas si el programa tuviera la siguiente forma:
%TERMINAL_INI%
$ cat muslist
%OUT_INI%#!/bin/bash
# Consulta CDs (versión 2)
#
grep -i "$1" musicas%OUT_FIM%
%TERMINAL_FIM%
En este caso, usamos la opción -i del grep, que como vimos, sirve para ignorar mayúsculas y minúsculas, y colocamos el $1 entre aspas, para que el grep continuara viendo la cadena de caracteres resultante de la expansión de la línea por el Shell como un único argumento de búsqueda.
%TERMINAL_INI%
$ muslist "album 2"%OUT_INI%
album2^Artista3~Musica3:Artista4~Musica4%OUT_FIM%
%TERMINAL_FIM%
Ahora, note que el grep localiza la cadena buscada en cualquier lugar del registro, entonces de la forma que estamos haciendo, podemos buscar por álbum, por música, por intérprete o hasta por un pedazo de cualquiera de estos. Cuando conozcamos los comandos condicionales, montaremos una nueva versión de muslist que permitirá especificar por cual campo buscar.
Ahí me vas a decir:
- Si, todo bien, pero es muy tedioso tener que colocar el argumento de búsqueda entre comillas cuando tengo que pasar el nombre del álbum. Esta forma no es nada amigable!
- Tienes toda la razón, y es por eso que te voy a mostrar otra forma de hacer lo que me pediste:
%TERMINAL_INI%
$ cat muslist
%OUT_INI%#!/bin/bash
# Consulta CDs (versión 3)
#
grep -i "$*" musicas
$ muslist album 2
album 2^Artista3~Musica3:Artista4~Musica4%OUT_FIM%
%TERMINAL_FIM%
De esta forma, el $*, que significa todos los parámetros, será substituído por la cadena album 2 (de acuerdo con el ejemplo anterior), haciendo lo que tu querias.
No te olvides que el problema del Shell no es si él puede o no hacer una determinada cosa. El problema es decidir cuál es la mejor forma de hacerla,ya que para realizar cualquier tarea, la cantidad de opciones es enorme.
Ah! en un dia de verano fuiste a la playa, olvidaste el CD en el automóbil, y entonces aquel "solcito" de 40 grados dobló tu CD y ahora precisas de una herramienta para borrarlo del banco de datos? No hay ningún problema, vamos a desarrollar un script llamado musexc, para excluir estos CDs.
Antes de desarrollar el programa te quiero presentar una opción bastante útil de la familia de comandos grep. Es la opción -v, que cuando es usada, lista todos los registros de la entrada, excepto el(los) localizado(s) por el comando. Veamos:
Ejemplos
%TERMINAL_INI%
$ grep -v "album 2" musicas
%OUT_INI%album 1^Artista1~Musica1:Artista2~Musica2
album 3^Artista5~Musica5:Artista6~Musica6
album 4^Artista7~Musica7:Artista8~Musica8%OUT_FIM%
%TERMINAL_FIM%
De acuerdo a lo te expliqué antes, el grep del exemplo listó todos los registros de músicas excepto los referentes al album 2, porque atendía al argumento del comando. Estamos entonces prontos para desarrollar el script para retirar aquél CD doblado de tu "CDteca". Él tiene la seguinte cara:
%TERMINAL_INI%
$ cat musexc
%OUT_INI%#!/bin/bash
# Borra CDs (versión 1)
#
grep -v "$1" musicas > /tmp/mus$$
mv -f /tmp/mus$$ musicas%OUT_FIM%
%TERMINAL_FIM%
En la primera línea mandé para /tmp/mus$$ el archivo musicas, sin los registros que atendiesen la consulta hecha por el comando grep. En seguida, moví (que, en realidad, equivale a renombrarlo) /tmp/mus$$ por encima del antiguo musicas.
Usé el archivo /tmp/mus$$ como archivo de trabajo, porque como ya habia citado en el artículo anterior, el $$ contiene el PID (Process Identification o identificación del proceso) y de esta forma cada uno que edite el archivo musicas lo hará en un archivo de trabajo diferente, de esta forma evitamos choques en su uso.
- Y entonces, amigo, estos programas que hicimos hasta aquí están muy rústicos en virtud de la falta de herramientas que todavia tenemos. Pero están bien, en cuanto me tomo otro chopp, puedes ir para casa a praticar en los ejemplos dados porque, te prometo, llegaremos a desarrollar un sistema bien bonito para control de tus CDs.
- Cuando nos encontremos la próxima vez, te voy a enseñar como funcionan los comandos condicionales y mejoraremos otro poco estos scripts.
- Por hoy es suficiente! Ya hablé demás y preciso mojar la palabra porque estoy de garganta seca!
- Mozo! Otro sin y espuma!
Cualquer duda o falta de compañia para tomar un chopp o hasta para hablar mal de los políticos lo único que tienes que hacer es mandarme un e-mail para julio.neves@gmail.com. Voy aprovechar tambiém para mandar mi aviso publicitario: puedes decirle a los amigos que quien quiera hacer un curso nota diez de programación en Shell que mande un e-mail para julio.neves@uniriotec.br para informarse.
Gracias y hasta la próxima!
-- HumbertoPina - 13 Sep 2006