Las expresiones regulares vienen a ser una forma sofisticada
de hacer un bucar&reemplazar. En el mundo windows no tienen mucho
sentido, despues
de todo alli casi todo va a base de clicks. Pero en el mundo Unix/Linux,
en el que casi todo son ficheros de texto, son casi una herramienta
imprescindible . No tan solo de cara al administrador, sino tambien
de cara a cualquier otro programador que puede ver como las expresiones
regulares le salvan la vida en mas de una ocasión.
Particularmente llevo un tiempecillo dedicado a la programación
y diseño web. No es de extrañar que un cliente que insistió
en que su e-mail estuviera
en cada página con un mailto: cambie de mail. Tampoco es de extrañar
que hallas escrito una palabra mal 2 veces en 50 páginas distintas
... Y tampoco
es especialmente raro que tengas que ir con cuidado para que la informacion
que sacas no esté en el tag html adecuado. Por ejemplo ... hay clientes
muy
raritos que quieren que todo lo que hasta ahora era cursiva ... se vuelve
negrita, pero solo si el contenido de la frase usa la palabra "clave".
¿Os imaginais este problema en MSWord? Busca una
frase que contenga la palabra "clave", ahora mira si esta entre
tag's de cursiva <i></i> y
ahora reemplaza <i></i> por <b></b>
Pues para estas cosas se inventaron las expresiones regulares ;)
Mientras espero con vosotros ese dia en que los clientes no me compliquen
la vida... es un consuelo saber que existen las ¡EXPRESIONES
REGULARES!
Nota: Todo lo que explico esta basado en las expresiones
regulares de PERL. Sed por ejemplo no tiene porque funcionar
exactamente igual.
Pero la idea es basicamente la misma.
[ ] cochetes
() parentesis
{} llaves
- guión
+ más
* asterisco
. Punto
^ circumflejo
$ dolar
? interrogante cerrado
| tuberia unix
\ barra invertida
(se usa para tratar de forma normal un caracter especial)
/ barra del 7
Mención aparte para / puesto que es el simbolo que se usa para indicar la búsqueda. El resto son todo modificadores y se pueden usar sin restricciones.
/[a-z]/ letras minusculas
/[A-Z]/ letras mayusculas
/[0-9]/ numeros
/[,'¿!¡;:\.\?]/ caracteres de puntuacion
-la barra invertida hace que
no se consideren como comando
ni en punto ni el interrogante
/[A-Za-z]/ letras del alfabeto (del ingles claro ;)
/[A-Za-z0-9]/ todos los caracteres alfanumericos habituales
-sin los de puntuacion, claro-
/[^a-z]/ El simbolo ^ es el de negación. Esto es decir
TODO MENOS las letras minusculas.
/[^0-9]/ Todo menos los numeros.
Para definir otros rangos, no dudeis en usar el operador de rangos "-" por ejemplo de la h a la m [h-m] ¿vale?
La expresion regular suele funcionar con que solo coincida un caracter
de los muchos listados. Pero en ocasiones queremos que la coincidencia
sea
total.
Imaginemos que nos hemos dejado todas las tildes de palabras acabadas
en ion. Se nos ocurre usar
/[ion]/ como patron de busqueda.
Pero esto daria concordancia positiva con Sol -por la o- con noche -por la n- ... etc.
Para que la concordancia sea de cada caracter y en el orden adecuado,
necesitamos el operador Punto "."
Por ejemplo [.ion] solo concordaria con cosas como camion o salsa
lionesa
Si bueno, la "ion" de lionesa no esta a final de palabra ... y eso tambien hemos de retocarlo, pero todo a su tiempo.
Aunque se os podria ocurrir que algo del tipo [.ion[^a-zA-Z]] tal vez funcionase ... ya veremos ;)
Por cierto que el punto "." no concuerda con \n \r \f \0 (newline, return, line feed, null respectivamente)
En ocasiones nos interesa usar el resultado de una parte del patrón en otro punto del patrón de concordancia.
Tomemos por ejemplo una agenda en la que alguien puso su telefono
en medio de su nombre.(Pepe 978878787 Gonzalez) Queremos extraer el
telefono de esa frase y guardarlo para introducirlo en otro sitio. O para
usarlo como patron de busqueda para otra cosa.
Lo primero que hay que hacer es agrupar. Agrupar permite que el
resultado del patron se almacene en una especie de registro para su uso posterior.
El operador de agrupamiento son los parentesis ( )
Los registros se llaman 1, 2, 3, ... 9 segun el orden del agrupamiento en el patrón.
Ejemplo (del libro de perl): /Esto es ([0-9]) prueba/
si el texto fuente es: Esto es 1 prueba.
El valor 1 seria almacenado en el registro $1
Tenemos una serie de elementos agrupados, a los cuales se les ha
asignado un valor. Ahora queremos usar ese valor en algun otro punto del
patron de
concordancia. Pues para eso existen las referencias.
\1 representa la referencia al grupo 1
Ejemplo (del libro de perl): /([0-9]) \1 ([0-9])/
si el texto es : 3 3 5
El resultado es: $1 --primer grupo-- vale 3
\1 --hace referencia al resultado del primer grupo-- vale 3
$2 --segundo grupo-- vale 5
En este caso hubiese habido concordancia, no asi si el texto hubiese sido 3 5 3
La extracción es simplemente usar el $1 en los mensajes que se generan despues de haber usado la expresion regular.
Si os habeis fijado hasta ahora todo lo que hemos visto han sido condiciones mas bien del tipo AND (Y)
Si hay un numero Y hay un texto Y no hay otro numero ... blablabla.
Siempre Y, es evidente que nos faltan las condiciones de tipo opcional
y
alternativo.
el simbolo que se emplea para indicar que una parte del patron es opcional es el interrogante. ?
/[0-9]? Ejemplo/
Esto concuerda tanto con textos del tipo
1 ejemplo, como con
ejemplo
No confundamos alternativas con opciones, la alternativa es el equivalente
a la OR no lo era asi el operador opcional. Veamos la diferencia. Por
cierto, el simbolo de la alternativa es |
/[0-9]? (EJEMPLO|ejemplo)/
a: 1 ejemplo
b: 1 EJEMPLO
c: 1
d: EJEMPLO
a,b,d concuerdan con el patron. Sin embargo c no concuerda con el
patrón! Porque ejemplo o EJEMPLO son alternavitas válidas,
pero tiene que
haber uno de los 2. No ocurre lo mismo con la opcionalidad, el 1 puede estar
o no estar. Esa es la principal diferencia.
A estas alturas te estas preguntando qué mas se puede hacer
con una expresión. Pues la verdad es que ya queda poca cosa, pero
lo de contarlas!
vamos ... eso es imprescindible.
El operador para esta ocasión son las llaves {m,n}
Donde m, n son 2 enteros positivos con n mayor o igual que m. Esto se lee asi ... la expresion debe cumplirse al menos m veces y no mas de n veces.
Podemos usar {n}o bien {n,} para indicar que queremos que se repita exactamente n veces o que se repita n veces o más respectivamente.
De tal forma que el patrón /go{1,4}l/
a: gool
b: gooooooooool
c: gooool
concordaria con a y c pero no asi con c
Tenemos el operador * asterisco que representa que el patrón
indicado debe aparecer 0 o mas veces y el operador + suma
que representa que el
patrón indicado debe aparecer 1 o más veces (pero almenos
1)
Mucho ojo con el operador asterisco!!! leeros el articulo
relacionado sobre conceptos avanzados de las expresiones regulares antes
de usarlo o no
me hago responsable de los resultados.
El operador ^ dice al compilador que empiece por el principio de
la linea y $ indica que empiece por el final de la linea. Estos operadores
cumplen
este efecto cuando estan fuera de la expresion regular, evidentemente.
Porque dentro de las barras tienen otras funciones ya comentadas anteriormente.
Y con esto creo que acabamos el listado de nuestras herramientas
de busqueda. Queda sin embargo pendiente de explicar lo referente al reemplazo,
con lo que por fin daremos solución al problema que plantee en la
primera página.
El manual dice lo siguiente
[m]/PATRON/PATRON REEMPLAZO/[g][i][m][o][s][x]
Donde mviene a ser el ambito de la busqueda.
^ inicio de linea. $ fin de linea.
% para todo el documento.
1,15 para las lineas de 1 a 15 ...
PATRON es el patron de busqueda del que hemos
estado hablando todo el rato.
PATRON REEMPLAZO es lo mismo que el patron de
busqueda solo que este se usa para el reemplazo
g (global) reemplaza TODAS las ocurrencias
en el texto.
i (insensitive) es para evitarnos problemas
con la capitalizacion. A = a
o (interpola variables solo una vez)
No me mireis asi que yo tampoco se lo que es eso XD
m (multiples lineas) Acepta strings de varias lineas
s (single line) solo mira el string de una linea.
x (extensiones) permite usar extensiones de expresion regular.
Insisto, yo tampoco se como se usa eso XD
Tal como prometi algunos ejemplos prácticos. Lo primero,
advertiros que es una parte que ire cambiando con el tiempo. He descubierto
que no todas
las expresiones regulares soportan la misma notación. Si bueno ...
deberia haberlo sabido, pero no lo sabia. Asi que hasta que me ponga las
pilas con
el sed ... voy a tocar un poco el tema del PERL
Lo primero es lo primero:
Tenemos el PERL en nuestra maquina muerto de risa asi que vamos a
cambiar eso ;-)
editad un fichero con el nombre "ejemplo.pl" -por ejemplo-
En la primera linea poneis:
!#/usr/bin/perlSi el perl no esta ahi, buscadlo y poned el path correspondiente en general /usr/local/bin/perl
#!/usr/bin/perl5
$texto="hola clave buena";
if ($texto=~ /<i>([a-z\s]{1,})<\/i>/) {
$frase=$1;
if ($1 =~ /clave/) {
$texto = "$frase";
print "$texto\n";
}
}
Se trata de buscar trozos de texto en italica. Si el texto
en italica contiene la palabra clave entonces se cambia la italica
por la negrita. La italica se
identifica por <i> y por </i>. Apuesto a que todo esto se pude
escribir en una sola linea, pero iba a ser ilegible, asi que lo he separado
en 2 trozos para
fines didacticos. Primero buscamos los tags de italica. Lo que quede por
enmedio, es decir ... caracteres en minusculas y espacios en blanco hasta
el fin
del tag de la italica se almacenan en $1. Segundo comprobamos que en lo
que habia escrito entre las italicas esta efectivamente el texto "clave"
Tercero ... procedemos a la sustitución.
Bien ahora probad esto en vuestras casas e id jugando con estos nuevos conceptos, poco a poco iremos ampliando el muestrario.
SED es un editor de textos no interactivo diseñado para funcionar en Unix y ayudar con los scripts.Editando Texto
Sed actua como un filtro, y esta orientado a lineas. La forma que tiene de trabajar suele ser un patrón y unas ordenes que aplicar a una serie de lineas.
Patrones orientados a linea
Un ejemplo simple del funcionamiento del Sed es "Borrar las 10 primeras lineas de un documento"
sed -e '1,10d'
El -e ejecuta el comando. En este sentido es realmente muy parecido
al vi. Por ejemplo, sabiendo que $ indica fin del documento. Si queremos
borrar
todo menos las primeras 10 lineas haremos:
sed -e '11,$d'
Lo que no puede hacerse es usar el sed con comandos relativos y complicados como por ejemplo:
sed -e '$-10,$d'
En este caso el sed no comprendera que intentamos hacer.
Otra forma de obtener las 10 primeras lineas es con el modificador
-n y la p. La p para que imprima esa linea, y el -n si no recuerdo mal
es para que
no haga eco en pantalla.
sed -n -e '1,10p'
Usando mas de un comando a la vez
sed -e '1,4d 6,9d' o bien sed -e '1,4d' -e '6,9d'
Usando las expresiones regulares en los patrones
El ejemplo tipico es ese fichero larguisimo de log's en el que no
sabes que lineas borrar, pero en que quieres borrar todas las lineas
que contengan
la palabra debug
sed -e '/debug/d' < log que es lo mismo que si hiceramos grep -v debug
Un ejemplo mas rebuscadillo
Ahora ya no nos basta con borrar las lineas que tienen debug, sino
que queremos borrar todas las lineas de debug que no tengan la palabra
foo en esa
misma linea. Eso se suelve hacer asi:
grep 'foo' < log | grep -v debug
Pero podemos usar el sed para ello de esta forma:
sed -n -e '/debug/d' -e '/foo/p'
Ahora las lineas que tienen debug son impresas si tienen la palabra
foo, asi que no se borran. Es una explicacion muy mala, pero bueno, estas
cosas,
nada como probarlas para acabar de entenderlas ¿vale?
Insertando texto
Se puede insertar texto con i y con a. El primero para insertar
antes de la linea en curso y el segundo para insertar despues de la linea
en curso. Por
ejemplo 1i para insertar en la primera linea y $a para insertar al final del
documento
sed -e '10i\ Texto a insertar'
Se puede reemplazar la linea en curso con la c
sed -e '10c\ Nuevo contenido de linea 10'
Sustitución de expresiones regulares
sed -e 's/patron/reemplazo/[modificadores]'
Notas Finales
Ante todo gracias al autor del minitutorial ingles en que me he
basado -no recuerdo su nombre-, no soy un plagiador, solo que me cuesta mucho
estructurar un tutorial, asi que le robo la estructura de contenidos a otra
gente XD.
Venga, ahora a practicar y a leerse las man's
El autor de estos fantásticos
ejemplos dice que no sabe mucho y que quiere el anonimato XD ... pero
si sabe mas que yo!! En fin, respetamos su
voluntad pero publicamos los ejemplos. Espero que esto anime a mas gente
a colaborar en este articulo :-)
lynx -source "http://www.servidor.com/pagina.jsp?cod_articulo=1&cantidad=10C307"
|grep 'abcd' >> ZZ
Recupera el codigo de articulo en una lista del servidor (El primer
registro en este caso). El nombre se encuentra en una línea que
contiene "abcd", y
se añade a un archivo sacándolo por grep
Lo mismo, pero con contador:
cuentatu=0
while [ $cuentatu -lt 10 ]
do
cuentatu=`expr $cuentatu "+" 1`
lynx -source "http://www.servidor.com/pagina.jsp?cod_articulo=$cuentatu
&cantidad=10C307" | grep 'zxy' | sed -e 's/ '//g | sed
-e 's/<br>'/'";
"'/g | sed -e 's/email:'//g | sed -e 's/<[^>]*>'//g >>
ZZ
done
En este caso, se hace un contador para
sacar los diez primeros datos, se incrementa de uno, se llama lynx para
ver el código fuente (-source), se saca
con grep la parte que contiene "zxy", que es donde están los datos
que se buscan, seguidamente, se quitan los con sed, y los <br>,
la palabra
"email:" y todos los demás tags html, para acabar en el archivo...
(La variable está en negrita en el link) Siempre he usado while,
pero es una cuestión
de costumbres...
Montando un servidor php básico,
es posible hacer muchas cosas, que sería larguísimo hacer
on line, subiendo, bajando archivos por ftp, para una
pequeña corrección.
Cosas así:
<?php
for($i=1 ; $i<10 ; $i++)
{
$web = "http://www.server.com/asp/ficha.php?ref=$i";
$archivo = "c:\\archiv~1\\www\\strip\\1\\$i.html";
$abre = fopen( $web, "r" );
$copia = fopen($archivo, "w");
while (!feof($abre)) {
$line = fgets($abre, 1024);
fputs($copia,$line);
}
fclose($abre);
fclose($copia);
}
?>
<?php
// meter una página web en un array e imprimirlo
$fcontents = file ('c:\\archiv~1\\easyphp\\www\\strip\\1\\13857.html');
while (list ($line_num, $line) = each ($fcontents)) {
echo "<b>Line $line_num:</b>; ", htmlspecialchars ($line),
"<br>\n";
}
?>
Otros truquillos.
-Uso ls- alt | grep ^d para ver los
directorios, no hay nada más rápido que yo sepa.
-cat correo | grep mailto Ese está claro
Extraido de
www.ciberdroide.com/misc/novato/curso/regexp.html(1)
Nos llega este otro ejemplo util para extraer cadenas determinadas de
un texto.
echo "abc1234def" | sed "s/[0-9][0-9]*/<&>/"
abc<1234>def
Extraigo del LinuxPlanet.com esta PERLa
de sabiduria ;-) ... es que lo mejor es tener estas cosas recopiladitas
y en castellano en lugar de dispersas y
en "gringo" ¿no?
Articulo de James Andrews(2)
http://www.linuxplanet.com/linuxplanet/tutorials/214/
(3)
Reemplazar una palabra por
otra en un texto: Es un problema habitual y si no sabemos lo del -p -i
nos volvemos locos porque lo unico que logramos es un eco del cambio ...
o un cambio en un
fichero temporal, pero no encima del propio fichero.
perl -print -inplace -execute
1. perl
-p -i -e 's/this/that/g' filename
El filename puede ser cualquier
cosa del tipo *.html o simplemente * para hacerlo sobre todo un directorio.
Ese ejemplo reemplaza todas las apariciones de this por that
2. perl
-e 'for (@ARGV) { rename $_, lc($_) unless -e lc($_); }' *
Renombra todos los ficheros en el directorio local a minusculas. Muy
util cuando en nuestro servidor web los clientes se quejan de que sus paginas
dejan de verse. Eso es debido a que W$
capitaliza los ficheros y luego ignora las capitalizaciones pero Linux
no.
3. perl
-e 'for (@ARGV) { rename $_,$_.'l' unless -e lc($_); }' *
Añade una
l a todas las extensiones de ficheros de un directorio, en plan .htm ->
.html
4.- Me la salto por usar librerias externas que no vienen a cuento
;-)
5. perl
-p -i -e 's/'
Convierte ficheros tipo Unix a ficheros tipo DOS. Corrige los caracteres de escape y de fin de linea de nuestros ficheros de texto.
6,7,8,9.
Otro salto por no tener que ver con las expresiones regulares en si.
10. perl
-l -e 'open(F,"/usr/dict/english"); $w=join("",sort split(//,$ARGV[0]));
print grep {chop;join("",sort split(//,$_)) eq $w} <>;'
<>
life
Encuentra todos los anagramas de la palabra life.
$ which <>Despliega en qué directorio se encuentra un
archivo.
Ejemplo: $ which ls
mount -t vfat /dev/fd0 /mnt/floppy
umount -t vfat /dev/fd0
Hacer ejecutable:
>chmod 755 script
o
>chmod a+x script
Ejecutarlo:
>./script
GREP
Usage: grep [OPTION]... PATTERN [FILE] ...
Search for PATTERN in each FILE or standard input.
Example: grep -i 'hello world' menu.h main.c
Pa sacar cosas:
grep 'http' < sucio.txt > limpio.txt
(Saca de sucio.txt todas las líneas -y solo ellas- que tengan
la palabra http y las mete en limpio.txt)
Regexp selection and interpretation:
-E, --extended-regexp PATTERN is an extended regular expression
-F, --fixed-strings PATTERN is a set of newline-separated strings
-G, --basic-regexp PATTERN is a basic regular expression
-e, --regexp=PATTERN use PATTERN as a regular expression
-f, --file=FILE obtain PATTERN from FILE
-i, --ignore-case ignore case distinctions
-w, --word-regexp force PATTERN to match only whole words
-x, --line-regexp force PATTERN to match only whole lines
-z, --null-data a data line ends in 0 byte, not newline
Miscellaneous:
-s, --no-messages suppress error messages
-v, --invert-match select non-matching lines
-V, --version print version information and exit
--help display this help and exit
--mmap use memory-mapped input if possible
Output control:
-b, --byte-offset print the byte offset with output lines
-n, --line-number print line number with output lines
-H, --with-filename print the filename for each match
-h, --no-filename suppress the prefixing filename on output
-q, --quiet, --silent suppress all normal output
--binary-files=TYPE assume that binary files are TYPE
TYPE is 'binary', 'text', or 'without-match'.
-a, --text equivalent to --binary-files=text
-I equivalent to --binary-files=without-match
-d, --directories=ACTION how to handle directories
ACTION is 'read', 'recurse', or 'skip'.
-r, --recursive equivalent to --directories=recurse.
-L, --files-without-match only print FILE names containing no match
-l, --files-with-matches only print FILE names containing matches
-c, --count only print a count of matching lines per FILE
-Z, --null print 0 byte after FILE name
Context control:
-B, --before-context=NUM print NUM lines of leading context
-A, --after-context=NUM print NUM lines of trailing context
-C, --context[=NUM] print NUM (default 2) lines of output context
unless overridden by -A or -B
-NUM same as --context=NUM
-U, --binary do not strip CR characters at EOL (MSDOS)
-u, --unix-byte-offsets report offsets as if CRs were not there (MSDOS)
`egrep' means `grep -E'. `fgrep' means `grep -F'.
With no FILE, or when FILE is -, read standard input. If less than
two FILEs given, assume -h. Exit status is 0 if match, 1 if no match,
and 2 if trouble.
Ej:
Algo mas complicado:
$ grep ^(f|b)*o(h)+. * # X* = X 0 o mas veces, X+ = X 0 o mas veces
, ^X = X
al principio de linea,
. = cualquier caracter menos \n. X | Y = X o Y. Este ejemplo haceptaria
las palabras siguientes si
se encuentra en el margen izquierdo.
ohx
fbfbohhhhaavvddf
fffohhMAMAMIA
find sirbe para listar todos los archivos de un subarbol. Puede ejecutar
intrucciones por cada archivo:
$ find /usr/include -name "*.h" -exec grep open {} \; -print
por cada archivo de include busca open con grep e imprime el nombre
de
archivo despues.
Como buscar texto en subdirectorios
?
Algunos ejemplos:
grep "string" `find . -type f`
find . -type f | xargs grep "string"
find . -type f | xargs fgrep "string" /dev/null
locate $PWD | grep "^$PWD" |xargs fgrep "string" /dev/null
find . \( -type f -name "*\.html" \) -exec grep -l "string" {} \;
Yo suelo usar el de find . -type f | xargs fgrep "string" /dev/null
11.1.7 Ejemplos
Sí, find tiene demasiadas opciones, lo sé. Pero, hay
un montón de casos preparados que vale la pena recordar, porque
son usados muy a menudo.
Veamos algunos de ellos.
$ find . -name foo\* -print
Encuentra todos los nombres de fichero que empiezan con foo. Si la
cadena de caracteres está incluida en el nombre, probablemente tiene
más sentido
escribir algo como , en vez de ``foo''.
$ find /usr/include -xtype f -exec grep foobar \
/dev/null {} \;
Es un grep ejecutado recursivamente que empieza del directorio /usr/include.
En este caso, estamos interesados tanto en ficheros regulares como en
enlaces simbólicos que apuntan a ficheros regulares, por tanto
el test ``-xtype''. Muchas veces es más simple evitar especificarlo,
especialmente si
estamos bastante seguros de cuáles ficheros binarios no contienen
la cadena de caracteres deseada. (¿Y por qué el /dev/null
en el comando? Es un
truco para forzar al grep a escribir el fichero del nombre donde se ha encontrado
un emparejamiento. El comando grep se aplica a cada fichero con
una invocación diferente, y, por lo tanto no cree que sea necesario
mostrar a la salida el nombre del fichero. Pero ahora hay dos ficheros,
esto
es : ¡el activo y /dev/null! Otra posibilidad podría ser redirigir
la salida11.9 del comando a xargs y dejar llevar a cabo el grep. Yo lo intenté,
e hice
pedazos completamente mi sistema de ficheros (junto con estas notas que
estoy intentando recuperar a mano :-( ).
$ find / -atime +1 -fstype ext2 -name core \
-exec rm {} \;
Es un trabajo clásico para la tabla de tareas preplaneadas.
Borra todos los ficheros llamados core en el sistema de ficheros del tipo
ext2 al cual no
se ha accedido en las últimas 24 horas. Es posible que alguien quiera
usar los ficheros de imagen de memoria11.10 para realizar un volcado post
mortem, pero nadie podría recordar lo que estuvo haciendo después
de 24 horas...
$ find /home -xdev -size +500k -ls > piggies
Es útil para ver quién tiene esos archivos que atascan
el sistema de ficheros. Note el uso de ``-xdev''; como sólo estamos
interesados en un sistema
de ficheros, no es necesario descender otro sistema de ficheros montado bajo
/home.
lenguaje de programación, con
una sintaxis con aspectos similares al C, y cuyo intérprete se invocacon
la instrucción awk.
En cuanto a la sintaxis del comando, casi todo está dicho ya:
# gawk --help
Usage: gawk [POSIX or GNU style options] -f progfile [--] file ...
gawk [POSIX or GNU style options] [--] 'program' file ...
POSIX options: GNU long options:
-f progfile --file=progfile
-F fs --field-separator=fs
-v var=val --assign=var=val
-m[fr] val
-W compat --compat
-W copyleft --copyleft
-W copyright --copyright
-W help --help
-W lint --lint
-W lint-old --lint-old
-W posix --posix
-W re-interval --re-interval
-W source=program-text --source=program-text
-W traditional --traditional
-W usage --usage
-W version --version
Report bugs to bug-gnu-utils@prep.ai.mit.edu,
with a Cc: to arnold@gnu.ai.mit.edu
Baste destacar que, además de incluir los programas entre comillas
sencillas (') en la línea de comandos, se pueden escribir en un
fichero que
invocamos con la opción -f, y que definiendo variables en la línea
de comandos -v var=val, podemos dotar de cierta versatilidad a los programas
que escribamos.
Awk es, básicamente, un lenguaje orientado al manejo de tablas,
en el sentido de información susceptible de clasificarse en forma
de campos y
registros, al estilo de las bases de datos más tradicionales. Con
la ventaja de que la definición del registro (e incluso del campo)
es sumamente flexible.
Pero awk es mucho más potente. Está pensado para trabajar
con registros de una línea, pero esa necesidad se puede relajar.
Para profundizar un poco
en algunos aspectos, vamos a echar un vistazo a algunos ejemplos ilustrativos
(y reales).
Imprimir tablas un poco más bonitas
Es posible que alguna vez hayamos tenido que imprimir alguna tabla
ASCII obtenida de alguna parte como, por ejemplo, las asociaciones de
números ethernet, IP y nombres de hosts. Cuando las tablas son realmente
grandes, su lectura se hace realmente difícil, y empezamos a echar
de
menos lo bien que se lee una tabla impresa con LaTeX o, al menos, formateada
algo mejor. Si la tabla es sencilla (y/o sabemos usar bien el awk),
no resulta demasiado difícil, aunque puede hacerse un poco tedioso:
BEGIN {
printf "preambulo LaTeX"
printf "\\begin{tabular}"
printf "{|c|c|...|c|}"
}
{
printf $1" & "
printf $2" & "
.
.
.
printf $n" \\\\ "
printf "\\hline"
}
END {
print "\\end{document}"
}
Ciertamente no es un programa lo que se dice genérico, pero
estamos empezando ...
(los \ dobles son necesarios por tratarse del carácter de escape
del shell)
Troceando ficheros de output
SIMBAD es una base de datos de objetos astronómicos que, entre
otras cosas, incluye las posiciones en el cielo de los mismos. En cierta
ocasión,
necesité hacer búsquedas para construir mapas alrededor de
algunos objetos. Como el interface de dicha base de datos permite guardar
los resultados
en ficheros de texto, podía hacer dos cosas: generar un fichero para
cada objeto o darle como input la lista completa, obteniendo un único
y enorme
log con la consulta. Como decidí hacer lo segundo, use awk para trocearlo.
Obviamente, para ello tuve que aprovechar ciertas características
del
output.
cada solicitud generaba una
línea de cabecera, con un formato del tipo
====> nombre : nlines <====
El primer campo me permitia saber cuando empezaba un objeto nuevo
y el cuarto cuantas entradas correspondían al mismo (aunque ese
dato no es
imprescindible) el caracter que separaba las columnas dentro de las listas
de output era '|'. Eso requería dos líneas de código
adicional para poder
enviar al output sólo los campos de mi interés.
( $1 == "====>" ) {
NomObj = $2
TotObj = $4
if ( TotObj > 0 ) {
FS = "|"
for ( cont=0 ; cont< {="">
getline
print $2 $4 $5 $3 >> NomObj
}
FS = " "
}
}
NOTA: Como en realidad no daba el nombre del objeto, era un poco más
complicado, pero pretende ser un ejemplo ilustrativo.
Jugeteando con el spool del mail
BEGIN {
BEGIN_MSG = "From"
BEGIN_BDY = "Precedence:"
MAIN_KEY = "Subject:"
VALIDATION = "[RESUMEN MENSUAL]"
HEAD = "NO"; BODY = "NO"; PRINT="NO"
OUT_FILE = "Resumenes_Mensuales"
}
{
if ( $1 == BEGIN_MSG ) {
HEAD = "YES"; BODY = "NO"; PRINT="NO"
}
if ( $1 == MAIN_KEY ) {
if ( $2 == VALIDATION ) {
PRINT = "YES"
$1 = ""; $2 = ""
print "\n\n"$0"\n" > OUT_FILE
}
}
if ( $1 == BEGIN_BDY ) {
getline
if ( $0 == "" ) {
HEAD = "NO"; BODY = "YES"
} else {
HEAD = "NO"; BODY = "NO"; PRINT="NO"
}
}
if ( BODY == "YES" && PRINT == "YES" ) {
print $0 >> OUT_FILE
}
}
Tal vez administramos una lista de correo. Tal vez, de vez en cuando,
se envían a la lista mensajes especiales (p.e. resúmenes
mensuales) con algún
formato determinado (p.e. un subject tipo '[RESUMEN MENSUAL] mes , dept').
Y de repente, se nos ocurre a fin de año recopilar todos los
resúmenes, separándolos de los demás mensajes.
Esto podemos hacerlo usando el awk con el spool del mail y el programa
que tenemos a la izquierda
Hacer que cada resumen vaya a un fichero requiere tres líneas
adicionales, y hacer también que, por ejemplo, cada departamento
vaya a un fichero
diferente supone unos pocos caracteres más.
NOTA: Todo este ejemplo está basado en cómo creo yo
que estan estructurados los mails en el spool. Realmente no se como lo
hacen, aunque me
funciona (de nuevo, en algunos casos fallará, como siempre).
Programas como éstos sólo necesitan 5 minutos pensando
y 5 escribiendo (o más de 20 minutos sin pensar, mediante ensayo
y error que es como
resulta más divertido).
Si hay alguna forma de hacerlo en menos tiempo, quiero saberla.
He usado el awk para muchas otras cosas (como generación automática
de páginas web con información obtenida de una base de datos)
y se lo
suficiente de programación como para estar seguro de que se pueden
hacer con él cosas que ni siquiera se me han ocurrido.
Sólo hay que dejar volar la imaginación.
Un problema
El único problema del awk es que necesita información
tabular perfecta, sin huecos: no puede trabajar con columnas de anchura
fija, que son de lo
mas común. Si el input del awk lo generamos nosotros mismos, no es
muy problemático: elegimos algo realmente raro para separar los
campos, lo
fijamos luego con la variable FS y ya está. Pero si solo tenemos
el input, esto puede ser más problemático. Por ejemplo, una
tabla tipo
1234 HD 13324 22:40:54 ....
1235 HD12223 22:43:12 ....
no se podria tratar con el awk. Entradas como esta a veces son necesarias,
aparte de ser bastante comunes. Aún así, rizando el rizo,
si sólo tenemos
una columna con esas características no todo está perdido (si
alguien sabe manejarse con más de una en un caso general, soy todo
oídos).
En una ocasión tuve que enfrentarme a una de esas tablas, similar
a la descrita más arriba. La segunda columna era un nombre e incluía
un número
variable de espacios. Como suele pasar yo necesitaba ordenarla por una columna
posterior a ella. Hice varios intentos con el sort +/-n.m que tenía
el
mismo problema de los espacios embebidos.
(y una solución)
Y me di cuenta de que la columna que yo quería ordenar era
la última. Y de que awk sabe cuantos campos hay en el registro actual,
por lo que
bastaba ser capaz de acceder al último (unas veces $9, otras $11,
pero siempre el NF). Total, que un par de pruebas, arrojaron el resultado
deseado:
{
printf $NF
$NF = ""
printf " "$0"\n"
}
Y obtengo un output igual al input, pero con la última columna
movida a la primera posición, y sorteo sin problemas. Obviamente,
el método es
facilmente ampliable al tercer campo empezando por el final, o al que va
despues de un campo de control que siempre tiene el mismo valor, porque
es el que usamos al extraer nuestra subtabla de la base de datos original
...
Sólo deja volar tu imaginación de nuevo.
Profundizando en el awk
Trabajando sobre lineas matcheadas
Hasta ahora, casi todos los ejemplos expuestos procesan todas las
lineas del fichero de entrada. Pero, como claramente explica la página
de manual,
es posible hacer que un cierto grupo de comandos procese tan sólo
unas ciertas líneas por el simple método de incluir la condición
antes de los
comandos, al modo del segundo de los ejemplos anteriores. La condición
que debe satisfacer la línea puede llegar a ser bastante flexible,
desde una
expresión regular, hasta un test sobre los contenidos de alguno
de los campos, pudiendo agruparse condiciones en base a operadores lógicos.
awk como lenguaje de programación
Como todo lenguaje de programación, awk implementa todas las
estructuras de control necesarias, así como un conjunto de operadores
y funciones
predefinidas, para manejar números y cadenas. Su sintaxis es en
general muy parecida a la del C, aunque difiere de él en algunos
aspectos.
Y, por supuesto, también es posible incluir funciones definidas
por el usuario, usando la palabra function, y escribiendo los comandos
como si se
tratara de procesar una línea normal del fichero de entrada. E, igualmente,
aparte de las variables escalares habituales, tambíen es capaz de
manejar
arrays de variables.
Incluyendo librerías
Como suele pasar con todos los lenguajes, hay una cierta serie de
funciones que son bastante comunes, y llega un momento en que cortar y
pegar no
es la mejor forma de hacer las cosas. Para eso se inventaron las librerías.
Y, al menos con la versión GNU de awk, es posible incluirlas dentro
del
programa awk. Pero eso es usar awk como una herramienta de trabajo mucho
más seria de lo que se pretende mostrar en este artículo,
aunque deja
claro el nivel de complejidad que puede llegar a alcanzar el awk.
Conclusiones
Ciertamente, puede no ser tan potente como numerosas herramientas
que se pueden usar con la misma finalidad. Pero tiene la enorme ventaja
de
que, en un tiempo realmente corto, permite escribir programas que, aunque
tal vez sean de un solo uso, están totalmente adaptados a nuestras
necesidades, que en muchas ocasiones son sumamente sencillas.
awk es ideal para los propósitos con los que se diseño:
leer ficheros línea por línea y procesar en base a los patterns
y cadenas que encuentre en ellas.
Ficheros del sistema como el /etc/password y muchos otros, resultan
sumamente fáciles de tratar mediante el awk, sin recurrir a nada
más.
Y desde luego que awk no es el mejor. Hay varios lenguajes de scripting
con capacidades mucho mayores. Pero awk sigue teniendo la ventaja de ser
siempre accesible en cualquier instalación, por mínima que
esta sea.
Información adicional
Este tipo de comandos tan básicos no suelen estar excesivamente
documentados, pero siempre se puede encontrar algo buscando por ahí.
la sintaxis del awk no es igual en todos los *nix, pero siempre hay
una forma de saber exactamente qué podemos hacer con el del nuestro
particular:
man awk;
Como no podía ser de otra forma, O'Reilly tiene un libro sobre
el tema: Sed & Awk (Nutshell handbook) de Dale Dougherty.
Una búsqueda en Amazon, nos proporciona algunos otros títulos
como Effective Awk Programming: A User's Guide, bastante orientado al gawk,
y
media docena más.
En general, todos los libros y manuales de unix mencionan estos comandos.
Pero sólo algunos de ellos profundizan un poco y dan información
útil.
Lo mejor, hojear todos aquellos que pasen por nuestras manos, pues nunca
se sabe donde podemos encontrar información valiosa.
Volver al Manual de Computación