En la interacción con Unix se puede, aparte de hacer las mismas tareas comúnes que en cualquier otro sistema operativo, tener la posibilidad de programar o automatizar tareas de una manera realmente eficiente.
En la sección anterior dejamos pendiente la
explicación del programa grep
. Vagamente recordamos que se
trata de un programa que busca cadenas de texto dentro de un archivo o
de varios. Bien, con frecuencia nos enfrentamos a éste problema:
necesitamos imprimir o alterar un archivo en particular, del cual no
recordamos el nombre, pero si que contenía una palabra o una frase en
particular. Digamos que recordamos que se trata de una carta que
enviamos a un cliente importante donde usamos la frase: ``es usted un
cretino insolente''. La manera pedestre de encontrar el archivo sería
revisar todos y cada uno de los archivos contentidos en el
subdirectorio cartas
del directorio clientes
. Pero, y
si son quinientas cartas? Bueno, en este caso comprenderemos porque
grep
es valioso. Basta con dar:
pablo@diva~/cartas/clientes$ grep "es usted un cretino insolente" * director.Banco.Nacional.25.de.junio.de.1994.txt:si cree usted que podr\'a salirse con la suya y no pagarnos el adeudo, me tomo la libertad de informarle que es usted un cretino insolente, director.regional.Nuevo.Leon.27.junio.1994.txt:y sin ningun motivo aparente, me espet\'o: el cretino insolente lo ser\'a usted. Obra en mi poder la prueba
Y vemos como grep
, rápidamente, encontró dos documentos
con la frase que entre comillamos. La razón del entre comillado, es
que grep
espera que el primer argumento sea el elemento a
buscar y los demás sean los archivos donde buscará, así que con las
doble comillas le avisamos al sistema operativo que la frase forma un
grupo y que éste deberá ser pasado al programa grep
como un
sólo argumento.
El segundo argumento, el asterisco, es en realidad un wildcard que le avisa al sistema operativo: toma todos los archivos
de éste directorio y pásaselos como argumentos al programa. Así que,
en realidad, grep
no recibe *
como segundo argumento,
sino toda la lista de archivos que contiene el directorio. Esta es
otra diferencia con respecto a MS-DOS en que, como todo programador
sabrá, el programa recibiría en efecto el asterisco y el programa debe
saber que hacer con él.
Qué pasaría si ni siquiera sabemos en que subdirectorio se
encuentra la carta? Bueno, pues desde el subdirectorio cartas
podríamos pedir:
pablo@diva~/cartas$ grep "es usted un cretino insolente" */* ./clientes/director.Banco.Nacional.25.de.junio.de.1994.txt:si cree usted que podr\'a salirse con la suya y no pagarnos el adeudo, me tomo la libertad de informarle que es usted un cretino insolente, ./clientes/director.regional.Nuevo.Leon.27.junio.1994.txt:y sin ningun motivo aparente, me espet\'o: el cretino insolente lo ser\'a usted. Obra en mi poder la prueba
Puede explicar que ocurre al utilizar "*/*"
como
último argumento?
Ahora bien, supongamos que conocemos el nombre del archivo,
pero no recordamos en que directorio está. Digamos que el nombre del
archivo es breve_carta_donde_le_recuerdo_a_mi_tia_Genoveva_que_soy_su_sobrino_favorito.txt.
De nuevo sabemos que está en algún lugar del árbol a partir del
subdirectorio cartas
. De nuevo, grep
nos puede ayudar.
pablo@diva~/cartas$ ls -R | grep breve_carta_a_mi_tia ./herencias/breve_carta_donde_le_recuerdo_a_mi_tia_Genoveva_que_soy_su_sobrino_favorito.txt
La opción -R
de ls
lista recursivamente el
contenido de archivos y subdirectorios.
O podríamos utilizar la opción -a
de du
, que nos
lista el espacio ocupado en bloques de todos los archivos:
pablo@diva~/cartas$ du -a | grep breve_carta_a_mi_tia 423 ./herencias/breve_carta_donde_le_recuerdo_a_mi_tia_Genoveva_que_soy_su_sobrino_favorito.txt
Y que además nos informa que el archivo contiene 423 kilobytes.
En realidad no necesitamos recurrir a un pipe para
hacer esto. Existe el programa find
que se utiliza para encontrar
archivos y que además permite ejecutar acciones sobre de ellos. En
este caso lo podríamos haber utilizado de la siguiente manera:
pablo@diva~/cartas$ find . -name '*Genoveva*favorito*' -print ./herencias/breve_carta_donde_le_recuerdo_a_mi_tia_Genoveva_que_soy_su_sobrino_favorito.txt
Con el primer argumento `.' le informamos a find
que la
búsqueda se realizará a partir del directorio actual, con -name
le pedimos que haga la búsqueda por nombre, con el tercero le
informamos que como parte del nombre aparecen Genoveva
y
favorito
, en ése orden, y con el cuarto le pedimos que cuando
lo encuentre, imprima el path donde lo halló. Utilizamos la
construcción '*Genoveva*favorito*'
por la siguiente razón, con
los asteriscos le informamos que antes de Genoveva
ocurre
cualquier combinación de carácteres, al igual que entre
Genoveva
y favorito
, así como después de éste
último. Utilizamos el apóstrofe para ocultar los astericos al shell, que es el programa con el que hemos estado interaccionando
todo este tiempo.
Ejercicio: De no ocultar los asteriscos al shell qué ocurriría?
Como hemos visto hasta ahora, Unix nos proveé de varias maneras de hacer las cosas y las construcciones que emplean los pipes nos permiten llevar a cabo tareas tan complejas como se nos ocurra.
Haremos un ejemplo un poco mas complicado que nos revela cúan
poderoso puede llegar a ser Unix. En éste caso sabemos que en algún
lugar de nuestro directorio existen varios archivos que como parte del
nombre tienen la palabra pericles
y queremos encontrar en
particular aquél que habla de parias nucleares
. Como ya sabemos
usar find
para encontrar archivos que cumplen con un patrón
determinado y además sabemos utilizar grep
para encontrar
frases dentro de archivos, la solución es sencilla.
pablo@diva~$ grep 'parias nucleares' `find . -name '*pericles*' -print` ./textos/poesia/oda_postmoderna_a_pericles.txt:las mujeres sucias, parias nucleares de \'esta tragedia
Sabemos bien de que se trata lo de grep
'parias
nucleares'
y también comprendemos que queremos al utilizar
find
. -name
'*pericles*'
-print
, lo único
nuevo en este caso es la construcción tan extraña y la comilla simple
que encierra a find
y sus argumentos. Sabíamos que el primer
argumento a grep
es la palabra o frase a buscar dentro del
archivo y que los demás argumentos son archivos. También sabíamos que
find
encuentra archivos que cumplen con una condición
determinada. Entonces, por qué no aprovechar ambos para realizar la
tarea? Para conseguirlo, le indicamos al shell que el segundo
argumento a grep
va a ser el resultado del find
. La
comilla antes y después son precisamente para esto. El shell
al encontrar una instrucción encerrada por comilla sencilla, sabe que
primero debe de ejecutar esa instrucción y luego el resto del
comando. En este caso ejecutará primero el find
y el resultado
lo utilizará como argumento al grep
.
Ahora vamos a hacer un corrector ortográfico para pobres. Lo
primero que necesitamos es un diccionario. Para esto, tomamos algunos
archivos que sepamos con seguridad que no contienen muchas faltas de
ortografía. Los pegamos todos a un archivo temporal con el programa
cat
:
pablo@diva~$ cat carta.txt entrevistas.virtuales.txt ipt.txt \ pgp.txt wired.txt > /tmp/borrame
Construimos el diccionario:
pablo@diva~$ cat /tmp/borrame | tr -c 'a-zA-Z\341\351\355\363\372\361' ' '| \ tr ' \t' '\n'| sort | uniq > diccionario
Y con esto ya tenemos una lista de palabras, una por
renglón, que podemos utilizar para el corrector. Veamos por partes
como se construye. Hacemos un cat
y un pipe
para pasarle
el archivo con que vamos a construir el diccionario a un programa
llamado tr
. Para comprender mejor que es lo que hace tr
,
veamos primero lo que occurre con la segunda aparición de éste
programa en la línea de comandos. La tarea de tr
es la de
traducir conjuntos de carácteres en conjuntos de carácteres en los
ordenes respectivos. Si queremos traducir todo un archivo de
minúsculas a mayúsculas, bastaría con usar tr
'a-z'
'A-Z'
. Por ejemplo, si tenemos un archivo llamado
minusculas
y queremos pasar todo su contenido a mayúsculas en
el archivo salida.mayusculas
, usaríamos:
pablo@diva~$ cat minusculas | tr 'a-z' 'A-Z' > salida.mayusculas
Regresando al ejemplo del corrector ortográfico, para
construir la lista de palabras necesitamos que estas ocurran una por
línea y además que no aparezcan signos de puntuación, números ni
símbolos. Lo que hacemos en el ejemplo con la primera ocurrencia de
tr
es, precisamente, eliminar todos los carácteres que no sean
alfabéticos aprovechando la opción -c
que indica que se tome el
complemento del primer conjunto, es decir que se traducirán todos los
carácteres que no aparezcan en el primer conjunto, que son todos los
carácteres distintos de la a
a la z
minúsculas y
mayúsculas y seis números que representan a las vocales acentuadas y a
la ñ en el estándar ISO-latin1.
Una vez hecho lo anterior, en la tubería en lugar del
archivo original, viaja un archivo donde no hay signos de puntuación,
dígitos, símbolos ni fines de línea. Sólo queda una lista de palabras
separadas por espacios, y nosotros la necesitamos separadas una por
línea, así que recurrimos nuevamente a tr
con los conjuntos
' '
y '\n'
. En este caso el primer conjunto es
' '
, que consiste en un espacio y el segundo es un
identificador especial que representa al cambio de línea. Así que
ahora en la tubería viaja un archivo que tiene una palabra por línea.
Por último, necesitamos que las palabras estén en orden y que
no aparezcan más de una vez. Para esto usamos sort
, que se
encarga de ordenarlas, y uniq
, que deja pasar sólo una
ocurrencia de cada palabra.
Estas conversiones las hicimos porque vamos a utilizar una
utilería de Unix llamada comm
, que compara línea por línea dos
archivos ordenados y en la salida escribe en la primera columna lo que
aparece exclusivamente en el primer archivo, en la segunda lo que
ocurre exclusivamente en el segundo y en la tercera lo que aparece en
ambos. Digamos que tenemos dos archivos llamados Juan
y
Pedro
, que representan a dos matemáticos amigos nuestros, y que
cada archivo contiene la lista de coches que posee cada uno. Así, si
Juan tiene: un Bentley, un Ferrari, un Jaguar, un Masserati, un
Peugeut y un Roll Royce, y Pedro tiene: un Barmann Costa, un Jaguar,
un Lamborghinni, un Mercedes Benz, un Peugeut y un Porsche; entonces
la salida será:
pablo@diva:~$ comm Juan Pedro Barmann Costa Bentley Ferrari Jaguar Lamborghinni Masserati Mercedes Benz Peugeut Porsche Roll Royce
Donde vemos que en efecto, ambos coinciden en tener un
Peugeut y un Jaguar, aunque naturalmente, en la vida real, tratándose
de personas de gran éxito económico como los matemáticos, ésta lista
sería demasiado grande para hacer la comparación a mano, lo cual
justificaría aún mas el utilizar programas como comm
.
Un último detalle, es que como salida del corrector
ortográfico queremos que se produzca la lista de palabras que tenemos
en el archivo a revisar y que no están en el diccionario, así que sólo
necesitamos como salida la tercera columna producida por
comm
. Esto lo podríamos hacer utilizando otro programa, pero
afortunadamente comm
considera este caso y como opción se le
puede decir que columnas se omiten. Esto se hace sencillamente
indicándoselas con el número de columna, en nuestro ejemplo omitiremos
las columnas segunda y tercera, es decir, las columnas que listan las
palabras en nuestro documento y las del diccionario. Así que la opción
que le daremos a comm
es -23
. Otra aclaración es que
comm
espera como entrada dos archivos y los que vamos a
utilizar son el diccionario que creamos, del mismo nombre, y el
archivo que está viajando por la tubería, el cúal no existe como
archivo con un nombre asignado, pero ésto tampoco es problema, porque
en Unix los archivos creados en base a pipes tienen
como nombre alternativo simplemente -
. Así que finalmente
estamos listos para ejecutar el corrector ortográfico de los pobres:
pablo@diva~$ cat unix.txt | tr -c 'a-zA-Z\341\351\355\363\372\361' ' '| \ tr '\t' '\n'| sort | uniq | comm -23 -diccionario | more
De haber seguido con atención nos daremos cuenta de lo limitado que resulta este corrector por varias razones. La primera es que no nos indica en que lugar del archivo ocurre la palabra, otra es que no hay ninguna garantía de que esa palabra esté mal, simplemente no está entre las que consideramos correctas y la tercera es que no hace el reemplazo adecuado, pero precisamente por eso lo llamamos el corrector ortográfico de los pobres.