Conceptos de editores fuera de línea

Existen ocasiones en las que el uso de un editor de textos como el vi o emacs puede ser demasiado o no cumplen con lo que queremos realizar. Tal vez se tengan que hacer cambios repetitivos a archivos o tener que procesar datos de una fuente y enviarlos a otro archivo en un orden correcto o tal vez solo queremos una forma simple de hacer el trabajo desde un script o la línea de comandos. Es aquí donde entran a trabajar los editores fuera de línea y para nombrar solo 2 veremos el editor de flujo Sed y el lenguaje de programación Awk (por Alfred Aho,Peter Weinberg y Brian Kernighan). Lo especial de Awk es que es un lenguaje con todas las letras y muy poderoso, pero en ocasiones nos facilitará tareas tediosas de procesamiento de información y por esto lo ponemos en el rango de un editor fuera de línea. Aunque esto no quiere decir que es lo único que puede hacer. Para mayor información sobre Sed y Awk les sugiero lean "Sed & AWK" de Dale Dougherty que trata el tema con una mayor profundidad.

Sed

Sed No es preciso detallar en demasía el uso de este editor fuera de línea dada que posee mucha similitud en su uso al editor de textos vi pero si es bueno mostrar algunas de sus funciones y usos tanto en el procesamiento desde un script o en la línea de comandos. Existen 3 casos específicos en los que se podrá usar Sed.

  1. Para editar archivos muy largos para editar interactivamente.

  2. Para editar archivos de cualquier tamaño cuando la secuencia de comandos de edición es muy complicada para hacer el tipeado confortable en ambiente interactivo.

  3. Para usar múltiples funciones globales de edición eficientemente en un solo paso.

Existen diferentes formas de realizar una edición con el Sed. Puede hacerse desde la línea de comando o simplemente guardándolos en un archivo para poder ser usado luego. Sed lee la entrada línea a línea y realiza los cambios a estas de acuerdo a lo que se le especifique. Luego de hacer estos cambios dirige su salida hacia stdout, la cual puede ser redireccionada. Sed actúa en forma similar a filtros como el grep y sort pero con la diferencia que con Sed se podrán crear programas mas complicados y con una mayor cantidad de funciones.

Para definir una estructura de uso del Sed veremos 2 formas distintas. La primera es la utilizada en la línea de comandos a través de una canalización y que tendrá esta estructura:

primer_comando | sed <opciones> <descripción_de_la_edición>
A continuación veremos la segunda opción para su uso a partir de la línea de comandos:
sed -f script_de_edición <archivo>
Con la opción "-f" le decimos a sed que a continuación le pasaremos un archivo donde encontrara las reglas a aplicar en <archivo>. Sed tiene mucho que ver con el editor vi en el área de búsqueda y reemplazo donde la estructura es la siguiente:

[direccion1 [,direccion2]] descripción_de_la_edición [argumentos]

Las direcciones serán reglas que el sed tendrá que encontrar en el texto, si se omitieran estas sed realizara en caso de ser posible los cambios en forma global. La descripción_de_la_edición indica a sed los cambios que tiene que hacer. Para esto, tema que veremos a continuación, pueden usarce varios argumentos. Es posible hacer varios cambios a una línea, sed los realiza de a uno y cuando ya no haya mas cambios a realizar en una línea dada, sed enviara el resultado a la salida estándar. Sed posee un contador interno de líneas que se va incrementando de acuerdo al número total de líneas leídas y no a las líneas del archivo. Por esto si se editan dos archivos juntos de 50 líneas cada uno, la línea número 60 de sed seria la línea 10 del segundo archivo. Esto es bueno para tener en cuenta a la hora de realizar ediciones a múltiples archivos. Como dijimos antes cada comando de sed puede tener 0, 1, ó 2 direcciones y en caso de tener 0, se aplicara a todas las líneas de ser posible:

/shrek/ s/entrada/salida/
El comando "s" indica a sed que debe hacer una sustitución de los argumentos que son pasados. Este simple comando, que puede estar en un archivo guardado para ser pasado al sed, sustituye la primera aparición de "entrada" por "salida" en las líneas que contengan "shrek". Cuando se ingresan dos direcciones, el cambio se comenzara a efectuar en la línea que concuerde la primer dirección y se ira aplicando a todas las siguientes hasta que concuerde con la segunda dirección. Las dos direcciones se separan con coma. Por ejemplo
10,30 s/entrada/salida/
hará una sustitución de "salida" por "entrada" a partir de la línea nº 10 hasta la línea nº 30. Obsérvese que no tiene que existir ningún espacio entre el comando "s" y la segunda dirección. Si las direcciones tiene al final un signo "!" (negado) el comando solo se aplicar a aquellas líneas que no concuerden con la dirección dada. Por ejemplo
10,30! s/entrada/salida/
Este comando se aplicará a todas la líneas, menos a las que estén entre la número 10 y la 30 inclusive. Como antes dijimos podremos modificar la salida del sed. Por ejemplo si quisiéramos mostrar por la salida estándar las líneas de una archivo que están entre la 20 y la 40, podremos dar a sed los argumentos necesarios:
[shrek@pantano:~]$ cat archivo | sed -n '20,40p'
El -n indica a sed que imprima solo las líneas que concuerden con los argumentos pasados, dado que evita que se impriman todos los demás. El comando "p" es para imprimir el patrón que encuentra sed. Ya estamos en condiciones de hacer un script rudimentario para imprimir líneas de archivos
[shrek@pantano:~]$ find /home/sebas/cartas/ "*" -print | while read FILE
>do echo $FILE
>cat $FILE | sed -n '5,15p'
>done
A continuación se mostrara por la salida estándar desde la línea 5 hasta la 15 inclusive de todos los archivos del directorio /home/shrek/cartas. Existen veces que queremos tener partes de archivos que concuerden con un patrón determinado, por ejemplo, si quisiéramos mandar todos los comentarios de un archivo de shell_script a otro y sabemos que las líneas que lo son comienzan con "#" podemos usar un comando en sed que haga el trabajo por nosotros de forma muy simple
[shrek@pantano:~]$ cat archivo | sed -n '/^#/w archivo2'
Debe existir un espacio exacto entre la "w" y el "archivo2" Con el -n indicamos a sed que solo procese las líneas que concuerdan con la dirección que le pasaremos. Con el símbolo "^" le decimos a sed que tiene que encontrar la dirección al principio de la línea. Y con la w le indicamos que escriba la salida al archivo2. A continuación pondremos una tabla de los comando que se usan en sed. Se recomiendan que practiquen con ellos y que observen los resultados obtenidos.

Tabla 4. Comandos del editor Sed

CaracterAcción
aañade texto al espacio patrón
bramifica a un rotulo, se emplea de forma similar a un goto
cañade texto
dborra texto
iinserta texto
llista el contenido del espacio patrón
nañade una nueva línea al espacio patrón
pimprime el espacio patrón
rlee un archivo
ssustituye patrones
wescribe a un archivo

Awk

EL awk es un poderoso lenguaje de programación que en muchas ocasiones nos sacara de apuros a la hora de tener que hacer script complejos de tratamiento de texto. El awk al igual que el sed lee las líneas completas para realizar sus modificaciones. Uno de los aspectos mas útiles en relación al awk es que a diferencia del "sed", awk puede dividir las líneas en campos a través de un separador de campo indicado en el script o en la línea de comandos. Si no se indica ninguno se tomara como separador de campo un espacio o tabulador. Usando la opción -F de la línea de comandos o la variable FS desde un programa hecho en awk se puede especificar un nuevo separador de campo. Por ejemplo si lo que quisiéramos es ver los nombres verdaderos que aparecen en el archivo /etc/passwd primero tendríamos que saber como separar los campos. En el archivo /etc/passwd se separan por un ":". Ahora tendríamos que saber en que campo se encuentra el nombre. Es en el campo numero 5, comenzando a contar como el primero de los campos. El 0 es la línea completa y ya veremos por que

[shrek@pantano:~]$ cat /etc/passwd | awk -F : '{print $5}'
root
bin
daemon
adm
lp
sync
shutdown
halt
mail
news
uucp
operator
games
gopher
FTP
User
Nobody
X Font Server
Named
PostgreSQL Server
Shrek Ogre
Fiona Ogre
[shrek@pantano:~]$

Como vemos lo primero que hicimos fue indicarle al awk cual seria el separador de campo "-F :", luego entre comillas le indicamos que imprima a la salida estándar el campo nº 5, '{print $5}'. De esta forma vemos los nombres contenidos en el archivo /etc/passwd. Podríamos imprimir mas de un campo a la vez, por ejemplo si queremos mostrar también el directorio home de cada uno de los usuarios podríamos hacer lo siguiente:

[shrek@pantano:~]$ cat /etc/passwd | awk -F : '{print $5,$6}'
root /root
bin /bin
daemon /sbin
adm /var/adm
lp /var/spool/lpd
. . .
PostgreSQL Server /var/lib/pgsql
Shrek Ogre /home/shrek
Fiona Ogre /home/fiona
[shrek@pantano:~]$ 
De esta simple manera podremos ir completando la línea a los requerimientos del campo que queramos ver ya que tenemos la posibilidad de hacer comparaciones a un campo de la misma manera que la haríamos a una variable en cualquier otro lenguaje. Por ejemplo si quisiéramos ver las líneas del /etc/passwd de todos aquellos usuarios que pertenecen al grupo user, representado por el nº 100 en el archivo passwd, podríamos hacer que el awk comprara el número del campo en el que esta el número GUID que nosotros buscamos. En el caso particular que cada usuario tuviese su grupo, podrimos hacer que se compararan todas las líneas que posean un número mayor o igual al número de grupo de usuarios mas bajo, por ejemplo 500 es el número que por defecto pone Red Hat a al grupo del primer usuario y va incrementándose a medida que incorporamos usuarios. Tendríamos que mostrar todas las líneas que en campo donde esta el GUID, el número 4, del usuario y que sea mayor o igual a 500. Por ejemplo:

[shrek@pantano:~]$ cat /etc/passwd | awk -F :\
'$4>=500 {print $0}'
shrek:x:500:500:Shrek Ogre:/home/shrek:/bin/bash
fiona:x:501:501:Fiona Ogre:/home/fiona:/bin/bash
[shrek@pantano:~]$ 

Como verán se indico que mostrara solo aquellas líneas que tuviesen en el campo nº 4 un valor mayor o igual a 500, $4>=500.También se ve que mostramos la línea entera al poner como campo a imprimir el $0. Una acotación que tendríamos que notar. Lo que comparamos en esta oportunidad es un número y esto lo hace tremendamente poderoso al awk como lenguaje de programación. Si se quisieran compara cadenas, se tendrían que encerrar ente "". Como ejemplo, si hubiésemos encerrado entre "" al 500 lo que awk interpretaría es que queremos mostrar todas las líneas que en la posición 4 tengan un valor alfabéticamente mayor o igual a 500.

[shrek@pantano:~]$ cat /etc/passwd | awk -F :\
'$4>="500" {print $0}'
lp:x:4:7:lp:/var/spool/lpd:
nobody:x:99:99:Nobody:/:
shrek:x:500:500:Shrek Ogre:/home/shrek:/bin/bash
fiona:x:501:501:Fiona Ogre:/home/fiona:/bin/bash
[shrek@pantano:~]$ 

Como verán si se ordena alfabéticamente la posición 4 el 7 y el 99 son mayores que 500. Los operadores que awk puede manejar son los siguientes:

Tabla 5. Operadores de Awk

OperadorSignificado
<menor que
<=menor que o igual que
==igual a
!=no igual a
>=mayor que o igual que
>mayor que

Otra posibilidad es el usar expresiones regulares para efectuar la búsqueda. Pero cuidado ya que es tienen que ser ingresadas de acuerdo a sí es mayúscula o minúscula.

[shrek@pantano:~]$ cat /etc/passwd | awk -F :\
'/Shrek/ {print $0}'
shrek:x:500:500:Shrek Ogre:/home/shrek:/bin/bash
[shrek@pantano:~]$ 

Lo único que tendremos que hacer es encerrarlo entre "/" para que se tomen como expresión regular. Ahora bien, las expresiones podrán ser tan complejas como queramos. Por ejemplo si quisiéramos mostrar todas las líneas que tuviesen la cadena "se" pero que no tengan antes la letra "U" y no les siga un espacio la orden es

[shrek@pantano:~]$ cat /etc/passwd | awk -F :\
'/[^U]se[^ ]/ {print $0}'
shrek:x:500:500:Shrek Ogre:/home/shrek:/bin/bash
[shrek@pantano:~]$ 
Como ven las cadenas que tenemos que ignorar se preceden antes y después de la cadena buscada ingresando un símbolo ^ encerrado entre []. De esta manera se podrá ir usando las distintas expresiones regulares. En todos estos casos se utilizaron una única forma para imprimir en pantalla los resultados, pero es bueno saber que contamos con otra forma en la que podremos formatear el texto antes de su salida por pantalla. Para la salida formateada se utiliza el "printf". Por ejemplo si quisiéramos podrimos imprimir los datos en una forma más cómoda:
[shrek@pantano:~]$ cat /etc/passwd | awk -F :\
'$4>=500 {printf"%20s %5s\n",$5,$1}'
Shrek Ogre shrek Fiona Ogre fiona
[shrek@pantano:~]$ 
Como se puede ver, pedimos que nos mostrara el nombre completo de l usuario y el nombre de usuario. Como sabemos la extensión aproximada que tendrá cada campo le damos 20 posiciones para le primer campo a mostrar, el $5, y 5 posiciones para el segundo campo a mostrar, el $1. Si lo que quisiéramos mostrar fuesen número en lugar de la "%s" (string) iría una "%d" o "%i" (decimal).Para mas información sobre el printf buscar en las páginas de manual del awk. Existen diferentes variables integradas, a parte del FS, que permiten llevar cuentas de distintos aspectos. Por ejemplo existe la variable NR que llevara la cuenta de los registros que mostremos. Por ejemplo supongamos que necesitamos obtener un listado largo de un directorio, pero solo queremos ver los permisos, el nombre del archivo y el número de registro que a pasado por el awk.
[shrek@pantano]$ ls -l | awk '{ print NR" "$1" "$9}'
1 total
2 -rw-rw-r-- 146768
3 -rw-rw-r-- Bienvenidos
4 -rw-rw-r-- Bienv
5 -rw-rw-r-- authkey.file
6 drwxr-xr-x Desktop
7 -rw-rw-r-- LUGRo
8 drwxrwxr-x Linux
9 -rw-rw-r-- Listado
10 -rw-rw-r-- Lo
11 drwx------ Mail
. . .
49 -rw-rw-r-- pgaccess-report.ps
50 -rw-rw-r-- sed
51 -rw-rw-r-- sed.zip
52 -rw-rw-r-- smtptel.htm
53 -rw-rw-r-- vicky 
Como verán, en esta ocasión la variable NR fue llevando la cuenta de los registros que fueron pasando por el awk. De esta forma se podrá decirle al awk que me muestre de los registros 5 al 10 solamente. scr Existen muchas más variables en el awk que son de extrema utilidad. Por ejemplo, en el caso anterior sabíamos que el ultimo campo estaba en la posición número 9, pero ¿que ocurre si no sabemos la posición del último campo o esta varia? Para esto esta la variable NF que lleva la cuenta de la cantidad de campos de cada registro. Por ello en lugar de la anterior forma podrimos poner:
[shrek@pantano:~]$ ls -l | awk '{ print NR" "$1" "$NF}'
y obtendríamos idénticos resultados. Pero un momento, aquí hay algo raro. La variable NR no tiene el signo $, en cambio la variable NF si lo tiene. Esto esta dado así para que no se reemplazado por el awk. Por ejemplo si hubiésemos puesto la variable NF sin signo $ el resultado seria.
[shrek@pantano:~]$ ls -l | awk '{ print NR" "$1" "NF}'
1 total 2
2 -rw-rw-r-- 9
3 -rw-rw-r-- 11
4 -rw-rw-r-- 11
5 -rw-rw-r-- 11
6 drwxr-xr-x 9
. . . 
Lo que nos esta mostrando no es el último campo, sino la cantidad de campos que ese registro tiene. Al agregarle el signo $ se reemplazara con el número del último campo y ese campo el que será mostrado. Esto es así para todas las variables integradas. El awk puede ser usado no-solo en una línea. Podrimos usarlo también como cualquier otro lenguaje para realizar múltiples tareas en una línea o realizar algo antes de comenzar la lectura y otra después. Para demarcar el código se utiliza los pares BEGIN-END. Todo lo que aparece después de la palabra BEGIN, pero en el mismo renglón, se realiza antes de que comience el ciclo. Cualquier cosa que este después de END se realiza después de que se haya leído la última línea y cerrado el ciclo. Estas líneas tendrán que estar en un archivo que será utilizado por el awk para procesar en este ejemplo al archivo /etc/passwd. Un ejemplo seria el siguiente:

BEGIN { FS=":"}
{ printf"Nombre Completo: %s\n",$5 }
{ printf"Nombre de Usuario: %s\n",$1}
{ printf"UID: %i,GUID: %i\n\n",$3,$4 }
END { printf "\n\nTotal de usuarios: %d \n\n", NR}

Este pequeño programa realizado con el vi será guardado en el archivo awk.src, el nombre se lo damos nosotros, y nos servirá para mostrar algunos datos del /etc/passwd mas detalladamente. La forma de ejecutarlo es a través del modificado "-f" donde le decimos al awk que a continuación le pasaremos un archivo con el programa que tiene que usar para procesar el /etc/passwd.

[shrek@pantano:~]$ awk -f awk.src /etc/passwd
Nombre Completo: root Nombre de Usuario: root UID: 0,GUID: 0
Nombre Completo: bin Nombre de Usuario: bin UID: 1,GUID: 1
Nombre Completo: daemon Nombre de Usuario: daemon UID: 2,GUID: 2
Nombre Completo: adm Nombre de Usuario: adm UID: 3,GUID: 4
. . .
Nombre Completo: PostgreSQL Server Nombre de Usuario: postgres UID: 26,GUID: 26
Nombre Completo: Shrek Ogre Nombre de Usuario: shrek UID: 500,GUID: 500
Nombre Completo: Fiona Ogre Nombre de Usuario: fiona UID: 501,GUID: 501
Total de usuarios: 22
Para finalizar diremos que también se podrán hacer operaciones con estas variables, es decir, sumarlas restarlas, multiplicarlas y dividirlas. Por ejemplo si quisiéramos saber en cuantos bloques de 4 líneas podríamos formar con un archivo de texto dado podríamos hacer el siguiente programa:
BEGIN {FS=":"}
{ print $0 }
FNR%4==0 { printf"\n" }
END { printf "El archivo %s puede entrar en %i bloques enteros\
 de 4 lineas\n",FILENAME,NR/4}
Existen un par de cosas nueva en el. Por ejemplo la variable "FNR" que cuenta el número de líneas. En esta ocasión le estamos diciendo que si el modulo de FNR es igual a 4 "FNR%4" imprima un salto de linea "{ printf"\n" }". Y al finalizar el ciclo se mostrara el mensaje que nos informara cuantos bloques de 4 líneas podremos tener. Para obtener el resultado se efectúa una división del numero total de registros "NR" por el número de líneas que queremos armar el bloque, "4". La variable FILENAME indica cual es el archivo sobre el cual se realizo el proceso y es tomada cuando se la pasamos al awk como argumento. El resultado es
[shrek@pantano]$ awk -f awk2.src /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:
daemon:x:2:2:daemon:/sbin:
adm:x:3:4:adm:/var/adm:
lp:x:4:7:lp:/var/spool/lpd:
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:
news:x:9:13:news:/var/spool/news:
uucp:x:10:14:uucp:/var/spool/uucp:
operator:x:11:0:operator:/root:
games:x:12:100:games:/usr/games:
gopher:x:13:30:gopher:/usr/lib/gopher-data:
ftp:x:14:50:FTP User:/home/ftp:
nobody:x:99:99:Nobody:/:
xfs:x:43:43:X Font Server:/etc/X11/fs:/bin/false
named:x:25:25:Named:/var/named:/bin/false
gdm:x:42:42::/home/gdm:/bin/bash
postgres:x:26:26:PostgreSQL Server:/var/lib/pgsql:/bin/bash
shrek:x:500:500:Shrek Ogre:/home/shrek:/bin/bash
fiona:x:501:501:Fiona Ogre :/home/fiona:/bin/bash

El archivo /etc/passwd puede entrar en 5 bloques enteros de 4 lineas