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 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.
Para editar archivos muy largos para editar
interactivamente.
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.
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
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
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
Caracter | Acción |
---|
a | añade texto al espacio patrón |
b | ramifica a un rotulo, se emplea de forma similar a un
goto |
c | añade texto |
d | borra texto |
i | inserta texto |
l | lista el contenido del espacio patrón |
n | añade una nueva línea al espacio patrón |
p | imprime el espacio patrón |
r | lee un archivo |
s | sustituye patrones |
w | escribe a un archivo |
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
Operador | Significado |
---|
< | 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 |