awk, power para tu command line
Guía de uso de la navaja suiza de los *nix para principiantes
English version here.
Disclaimer
Este texto se refiere exclusivamente a la implementación GNU de awk
conocida como gawk
que es la usada mayoritariamente en cualquier distribución Linux actual.
En su redacción la referencia fundamental ha sido The GNU Awk User’s Guide y mis aportaciones a Stackoverflow.
AWK es un lenguaje similar a PERL, pero considerablemente más elegante.
- Arnold Robbins
¿Qué es?
AWK
es un lenguaje de programación diseñado para procesamiento de texto, se usa normalmente como herramienta de extracción y reporting.
Es un standard en prácticamente cualquier sistema *nix.
awk
award name …
El nombre del lenguaje se deriva del apellido de sus autores: Alfred Aho, Peter Weinberger, y Brian Kernighan.
Así que awk
…
-
Busca líneas que contengan determinados patrones en ficheros o en la entrada estándar.
-
Se usa fundamentalmente para reporting y extracción de datos, por ejemplo para sumarizar la salida de otros programas.
-
Tiene una sintaxis similar a
C
. -
Orientado a los datos: se trata de describir con que datos queremos trabajar y que acción hacer al encontrarlos.
pattern { action }
pattern { action }
Lo básico
¿Cómo ejecutarlo?
Si el programa es corto:
awk 'program' input-file1 input-file2
Nota: presta atención a los posibles problemas con el shell quoting1.
cmd | awk 'program'
El pipe
redirige la salida del comando a la izquierda cmd
a la entrada del comando awk
2.
Cuando el código es largo, normalmente es preferible usar un fichero como contendor y ejecutarlo de esta forma:
awk -f program-file input-file1 input-file2
cmd | awk -f program-file
O a través de un interprete, a lo shebang
.
#!/bin/awk -f
BEGIN { print "hello world!!" }
Otros parámetros interesantes
-F fs
Establece el valor de FS
(Field Separator) fs
.
-v var=val
Pasa la variable var
con valor val
al awk
antes de que la ejecución comience.
Nota: se puede usar múltiples veces, para setear el número deseado de valores.
BEGIN y END
Estas etiquetas marcan los bloques o patrones especiales que proporcionan un espacio para las acciones de inicialización y limpieza.
Sigue el siguiente esquema:
BEGIN{
// initialize variables
}
{
/pattern/ { action }
}
END{
// cleanup
}
Ambos bloques se ejecutan solo una vez, BEGIN
antes de que se lea el primer registro, END
después de consumir todo el input.
$ echo "hello"| awk 'BEGIN{print "BEGIN";f=1}
{print $f}
END{print "END"}'
BEGIN
hello
END
Porque grepear si tenemos awk
?
$ cat lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Nunc enim orci, euismod id nisi eget, interdum cursus ex.
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.
$ grep dolor lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
$ awk '/dolor/' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nota: Cuando no se proporciona una acción el comportamiento por defecto es volcar la línea o registro macheado al stdout.
Pero … ¿como podríamos mostrar la primera y ultima palabra de cada línea?
Se puede hacer con grep
, por supuesto, pero necesitamos dos paso:
$ grep -Eo '^[^ ]+' lorem.dat
Lorem
Maecenas
Nunc
Curabitur
Lorem
Aliquam
$ grep -Eo '[^ ]+$' lorem.dat
elit.
condimentum.
ex.
tellus.
elit.
ultrices.
Veamos awk
en acción solventado este problema:
$ awk '{print $1,$NF}' lorem.dat
Lorem elit.
Maecenas condimentum.
Nunc ex.
Curabitur tellus.
Lorem elit.
Aliquam ultrices.
¿Mejor no ? , Yeah but … ¿Como funciona?
Para averiguarlo es necesario conocer dos estructuras básicas en awk
los Registros (Records) y Campos (Fields) en los que se dividen cualquier entrada al programa.
Registros
Los registros están delimitados por un carácter o expresión regular que se conoce como Record Separator RS
.
Su valor por defecto es el salto de línea unix \n
, por este motivo los registros por defecto equivalen a una línea individual.
Adicionalmente disponemos de la variable ORS
Output Record Separator que nos va a permitir controlar la delimitación de estos registros al volcarlos sobre el stdout.
Tanto el RS
como el ORS
deben estar encomillados, lo que indica que estamos ante constantes literales.
Usar un carácter o regex
distinto es tan simple como asignárselo a las variable RS
u ORS
:
-
Generalmente, el mejor momento para hacerlo es al comienzo de la ejecución, en el
BEGIN
, antes de que comience el proceso de la entrada de modo que el primer registro se lea con el separador deseado. -
La otra forma de cambiar el
RS
(o elORS
) es a través de la línea de comando mediante los mecanismos de asignación de variables.
Ejemplos:
$ awk 'BEGIN{RS=" *, *";ORS="<<<---\n"}{print $0}' lorem.dat
Lorem ipsum dolor sit amet<<<---
consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Nunc enim orci<<<---
euismod id nisi eget<<<---
interdum cursus ex.
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet<<<---
consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat<<<---
et facilisis neque ultrices.
<<<---
$ awk '{print $0}' RS=" *, *" ORS="<<<---\n" lorem.dat
Lorem ipsum dolor sit amet<<<---
consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Nunc enim orci<<<---
euismod id nisi eget<<<---
interdum cursus ex.
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet<<<---
consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat<<<---
et facilisis neque ultrices.
<<<---
Campos
Los registros awk
se dividen de forma automática en pedazos denominados campos (fields).
El separador se contiene en la variable FS
(Field Separator) su valor por defecto entre campos es el espacio blanco que en awk
se define como una cadena compuesta por uno o mas espacios, TABs o saltos de línea.
Nos referimos a un campo en awk
mediante el símbolo dólar $
seguido por el número del campo que deseamos tratar.
De esta forma $1
se referirá al primer campo, $2
al segundo y así sucesivamente.
IMPORTANTE: $0
hace referencia al registro completo.
$ awk '{print $3}' lorem.dat
dolor
erat
orci,
dapibus
dolor
mauris
NF
es una variable predefinida que devuelve el número de campos de el registro actual.
En la práctica esto trae como consecuencia que $NF
siempre apuntará al último campo de un registro.
$ awk '{print NF, $NF}' lorem.dat
8 elit.
7 condimentum.
10 ex.
4 tellus.
8 elit.
10 ultrices.
Del mismo modo que existe un ORS
también disponemos de un OFS
(Output Field Separator) para controlar la forma en que delimitaremos los campos al mandarlos al output stream.
$ cat /etc/group
nobody:*:-2:
nogroup:*:-1:
wheel:*:0:root
daemon:*:1:root
kmem:*:2:root
sys:*:3:root
tty:*:4:root
$ awk '!/^(_|#)/&&$1=$1' FS=":" OFS="<->" /etc/group
nobody<->*<->-2<->
nogroup<->*<->-1<->
wheel<->*<->0<->root
daemon<->*<->1<->root
kmem<->*<->2<->root
sys<->*<->3<->root
tty<->*<->4<->root
Nota: ¿¿Y ese?? … $1=$1
3
Una vez que interiorizamos registros y campos resulta muy sencillo entender nuestro código inicial:
$ awk '{print $1,$NF}' lorem.dat
Lorem elit.
Maecenas condimentum.
Nunc ex.
Curabitur tellus.
Lorem elit.
Aliquam ultrices.
NR y FNR
Estas son dos variables built-in muy interesantes:
NR
: Número de registros que awk
ha procesado desde el inicio de la ejecución del programa.
FNR
: Número de registro del fichero procesado actualmente, awk
resetea FNR
a cero cada vez que comienza a procesar un nuevo archivo.
$ cat n1.dat
one
two
$ cat n2.dat
three
four
$ awk '{print NR,FNR,$0}' n1.dat n2.dat
1 1 one
2 2 two
3 1 three
4 2 four
Mejorando la salida
Para conseguir una salida más adecuada podemos usar printf
:
printf format, item1, item2, …
La mascara de formato es muy similar a la del ISO C.
$ awk '{printf "%20s <-> %s\n",$1,$NF}' lorem.dat
Lorem <-> elit.
Maecenas <-> condimentum.
Nunc <-> ex.
Curabitur <-> tellus.
Lorem <-> elit.
Aliquam <-> ultrices.
Redirigiendo el Output
La salida de print
y printf
se dirige por defecto al stdout
pero podemos redireccionarla de diferentes modos.
Estas redirecciones en awk
se escriben de forma similar a como se hacen en los comandos sobre shell
, con la salvedad de que se incorporan en el código del programa.
$ awk 'BEGIN{print "hello">"hello.dat"}'
$ awk 'BEGIN{print "world!">>"hello.dat"}'
$ cat hello.dat
hello
world!
Otra posibilidad es enviar la salida a otro programa mediante pipes:
$ awk 'BEGIN{sh="/bin/sh";print "date"|sh;close(sh)}'
dom nov 13 18:36:25 CET 2016
Podemos apuntar estas redirecciones a los streams estándar:
-
/dev/stdin
: La entrada estándar (descriptor 0). -
/dev/stdout
: La salida estándar (descriptor 1). -
/dev/stder
: La salida de error estándar (descriptor 2).
Un ejemplo de como escribir mensajes de error sería:
$ awk 'BEGIN{print "Serious error detected!" > "/dev/stderr"}'
Serious error detected!
Trabajando con Arrays
En awk
los arrays son asociativos lo que en la practica se traduce en que cada array es una colección de parejas índice - valor , siendo el índice cualquier valor numérico o cadena de texto, donde el orden es irrelevante.
No necesitan de declaración previa y se pueden añadir nuevos valores en cualquier momento.
Índice | Valor |
---|---|
“perro” | “dog” |
“gato” | “cat” |
“uno” | “one” |
1 | “one” |
2 | “two” |
Para referirnos a un array usaremos la sintaxis:
array[index-expression]
Si queremos asignarle valores:
array[index-expression] = value
Para determinar si un índice está presente:
indx in array
Para recorrer los elementos del array:
for (var in array) {
var, array[var]
}
Si hemos usado valores numéricos podemos recuperar los elementos preservando el orden:
for (i = 1; i <= max_index; i++) {
print array[i]
}
O usar algo más avanzado (exclusivo de gawk
) como @ind_str_asc
.
Por ejemplo, partiendo de:
$ cat dict.dat
uno one
dos two
tres three
cuatro four
awk '{dict[$1]=$2}
END{if ("uno" in dict)
print "Yes we have uno in dict!"
if (!("cinco" in dict))
print "No , cinco is not in dict!"
for (esp in dict){
print esp, "->" ,dict[esp]
}
}' dict.dat
Devolvería:
Yes we have uno in dict!
No , cinco is not in dict!
uno -> one
dos -> two
tres -> three
cuatro -> four
Podemos ver como mantiene el orden original:
awk 'BEGIN{
a[4]="four"
a[1]="one"
a[3]="three"
a[2]="two"
a[0]="zero"
exit
}
END{for (idx in a){
print idx, a[idx]
}
}'
4 four
0 zero
1 one
2 two
3 three
La ordenación podemos controlarla mediante va variable PROCINFO
:
awk 'BEGIN{
PROCINFO["sorted_in"] = "@ind_num_asc"
a[4]="four"
a[1]="one"
a[3]="three"
a[2]="two"
a[0]="zero"
exit
}
END{for (idx in a){
print idx, a[idx]
}
}'
0 zero
1 one
2 two
3 three
4 four
Funciones Build-in
gensub(regexp, replacement, how [, target])
: Es la función más avanzada de substitución de texto.
Y sus variantes más simples:
gsub(regexp, replacement [, target])
sub(regexp, replacement [, target])
Su uso más simple permite sencillas substituciones
Partiendo de:
$ cat lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Nunc enim orci, euismod id nisi eget, interdum cursus ex.
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.
Supongamos que deseamos invertir la posición de las palabras que se encuentran a un lado y otro de las comas.
$ awk '{print gensub(/([^ ]+)( *, *)([^ ]+)/,
"\\3\\2\\1", "g")}' lorem.dat
Lorem ipsum dolor sit consectetur, amet adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Nunc enim euismod, orci id nisi interdum, eget cursus ex.
Curabitur a dapibus tellus.
Lorem ipsum dolor sit consectetur, amet adipiscing elit.
Aliquam interdum mauris volutpat nisl et, placerat facilisis.
Mediante gensub
usamos grupos para realizar tres capturas he invertir los resultados.
Una acción más simple sería la substitución de puntos por comas:
awk '$0=gensub(/\./, ",", "g")' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
Maecenas pellentesque erat vel tortor consectetur condimentum,
Nunc enim orci, euismod id nisi eget, interdum cursus ex,
Curabitur a dapibus tellus,
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
Aliquam interdum mauris volutpat nisl placerat, et facilisis,
Aunque también podríamos optar por la función simplificada gsub
:
awk 'gsub(/\./, ",")' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
Maecenas pellentesque erat vel tortor consectetur condimentum,
Nunc enim orci, euismod id nisi eget, interdum cursus ex,
Curabitur a dapibus tellus,
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
Aliquam interdum mauris volutpat nisl placerat, et facilisis,
Personalmente la considero mas adecuada cuando no es necesario la captura de grupos en regex
.
Otras funciones de procesamiento de cadenas interesantes son index
y substr
.
index(in, find)
substr(string, start [, length ])
Su funcionamiento queda claro con estos sencillos ejemplos:
$ awk 'BEGIN{t="hello-world";print index(t, "-")}'
6
$ awk 'BEGIN{t="hello-world";print substr(t,index(t, "-")+1)}'
world
La función split
permite generar un array a partir de una cadena y un separador, retorna el número de elementos del vector resultante:
split(string, array [, fieldsep [, seps ] ])
$ cat passwd
jd001:x:1032:666:Javier Diaz:/home/jd001:/bin/rbash
ag002:x:8050:668:Alejandro Gonzalez:/home/ag002:/bin/rbash
jp003:x:1000:666:Jose Perez:/home/jp003:/bin/bash
ms004:x:8051:668:Maria Saenz:/home/ms004:/bin/rbash
rc005:x:6550:668:Rosa Camacho:/home/rc005:/bin/rbash
$ awk 'n=split($0, a, ":"){print n, a[n]}' passwd
7 /bin/rbash
7 /bin/rbash
7 /bin/bash
7 /bin/rbash
7 /bin/rbash
Nota: Esto se podría hacer de otra forma mucho más simple.
$ awk '{print NF,$NF}' FS=':' passwd
7 /bin/rbash
7 /bin/rbash
7 /bin/bash
7 /bin/rbash
7 /bin/rbash
Funciones propias
Escribir funciones custom es muy simple tal y como se puede apreciar en el siguiente ejemplo:
awk 'function test(m)
{
printf "This is a test func, parameter: %s\n", m
}
BEGIN{test("param")}'
Que nos devuelve:
This is a test func, parameter: param
Análogamente podríamos devolver un valor mediante return
:
awk 'function test(m)
{
return sprintf("This is a test func, parameter: %s", m)
}
BEGIN{print test("param")}'
La única forma de que una variable sea local en una función es en la recogida de parámetros.
Los parámetros escalares se pasan por valor y los arrays por referencia así que cualquier cambio que se haga en el array en la función se reflejara en el cuerpo del programa:
awk 'function test(m)
{
m[0] = "new"
}
BEGIN{m[0]=1
test(m)
exit
}
END{print m[0]}'
Volcará al stdout
:
new
Ahora empecemos con lo divertido
Trataremos de resolver una serie de ejercicios que pondrán a prueba los conocimientos adquiridos:
01. Mostrar la penúltima palabra del fichero lorem.dat
.
02. Substituir una línea o registro.
03. Añadir un punto y coma al final de cada línea.
04. ¿Y una coma entre cada palabra?
05. ¿Todo junto?
06. Incluir los registros impares en un archivo y los pares en otro.
07. Dado un fichero de /ect/password
informa el campo del home
del usuario en base al login.
08. Cambiar el orden de los campos de modo que el primero pase a ser el final.
09. Hackeando traceroute
.
10. Procesos dependientes de un PID
padre.
11. Como agregar información a partir de una clave.
12. Mostrar los registros entre dos patrones.
13. Convertir un campo en función de su contenido a un determinado valor numérico.
14. Agrupando registros en columnas.
15. Procesando un fichero FASTA.
16. Reporting complejo.
17. Join entre ficheros.
18. Cruzando /etc/passwd
y /etc/group
.
19. Conexiones por usuario a un servidor.
20. Obteniendo la media total proporcionada por el comando uptime
.
¡Markdown no permite acentos en los anchors!
01. Mostrar la penultima palabra de un fichero
Partiendo del siguiente source file:
$ cat lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Nunc enim orci, euismod id nisi eget, interdum cursus ex.
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.
$ awk '{print $(NF-1)}' lorem.dat
adipiscing
consectetur
cursus
dapibus
adipiscing
neque
No hay mucho que explicar NF
indica el número de campos presente en el registro , luego (NF-1)
apuntara al campo anterior al último y $(NF-1)
a su correspondiente valor.
02. Substituir un registro
Supongamos que necesitamos reemplazar la tercera línea por:
Esto no es latín
Nada más simple, basta con jugar con NR
(Number of Record):
$ awk 'NR==3{print "Esto no es latín";next}{print}' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Esto no es latín
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.
Alternativamente, podemos evitar usar next
asignando el nuevo texto al registro completo $0
:
$ awk 'NR==3{$0="Esto no es latín"}1' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
Esto no es latín
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.
03. Colocar un punto y coma al final de cada registro
$ awk '1' ORS=";\n" lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.;
Maecenas pellentesque erat vel tortor consectetur condimentum.;
Nunc enim orci, euismod id nisi eget, interdum cursus ex.;
Curabitur a dapibus tellus.;
Lorem ipsum dolor sit amet, consectetur adipiscing elit.;
Aliquam interdum mauris volutpat nisl placerat, et facilisis neque ultrices.;
La solución es simple, como conocemos que el RS
por defecto es el salto de línea basta con que al de salida OFS
le antepongamos el punto y coma ;\n
ATENCIÓN: ¿Que hace ese 1
?4
04. ¿Y una coma entre cada palabra?
$ awk '{$1=$1}1' OFS=',' lorem.dat
Lorem,ipsum,dolor,sit,amet,,consectetur,adipiscing,elit.
Maecenas,pellentesque,erat,vel,tortor,consectetur,condimentum.
Nunc,enim,orci,,euismod,id,nisi,eget,,interdum,cursus,ex.
Curabitur,a,dapibus,tellus.
Lorem,ipsum,dolor,sit,amet,,consectetur,adipiscing,elit.
Aliquam,interdum,mauris,volutpat,nisl,placerat,,et,facilisis,neque,ultrices.
Los más notable de este código es como se fuerza que awk
reconstruya el registro completo, usando los valores actuales para el FS
y OFS
.
Para hacerlo usamos esta asignación inocua: $1 = $1
05. ¿Todo junto?
$ awk '{$1=$1}1' OFS=',' ORS=';\n' lorem.dat
Lorem,ipsum,dolor,sit,amet,,consectetur,adipiscing,elit.;
Maecenas,pellentesque,erat,vel,tortor,consectetur,condimentum.;
Nunc,enim,orci,,euismod,id,nisi,eget,,interdum,cursus,ex.;
Curabitur,a,dapibus,tellus.;
Lorem,ipsum,dolor,sit,amet,,consectetur,adipiscing,elit.;
Aliquam,interdum,mauris,volutpat,nisl,placerat,,et,facilisis,neque,ultrices.;
Tan simple como jugar con las dos variables de salida OFS
y ORS
.
06. Incluir los registros impares en un archivo y los pares en otro
Partimos de la solución:
$ awk 'NR%2{print >"impar.dat";next}{print >"par.dat"}' lorem.dat
$ cat par.dat
Maecenas pellentesque erat vel tortor consectetur condimentum.
Curabitur a dapibus tellus.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.
$ cat impar.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nunc enim orci, euismod id nisi eget, interdum cursus ex.
Lorem ipsum dolor sit amet, consectetur adipiscing elit
El símbolo %
es la función modulo que extrae el resto al dividir el número de registro o línea tratado entre 2:
$ awk '{print NR%2}' lorem.dat
1
0
1
0
1
0
Como sabemos en awk
1
es verdadero y 0
falso, en base a esta premisa redirigimos la salida según lo requerido.
Hay que prestar especial atención al comando next
que fuerza que awk
deje de forma inmediata de procesar ese registro y pase al siguiente. De esta forma evitamos un doble control de condición que sin next
se hubiera escrito así:
awk 'NR % 2{print > "impar.dat"}
!NR % 2{print > "par.dat"}' lorem.dat
07. Dado un fichero de password informa el campo del home del usuario en base al login
$ cat /etc/passwd
jd001:x:1032:666:Javier Diaz::/bin/rbash
ag002:x:8050:668:Alejandro Gonzalez::/bin/rbash
jp003:x:1000:666:Jose Perez::/bin/bash
ms004:x:8051:668:Maria Saenz::/bin/rbash
rc005:x:6550:668:Rosa Camacho::/bin/rbash
Supongamos que queremos componer el campo perdido anteponiendo la carpeta /home
:
$ awk '$6="/home/"$1' FS=':' OFS=':' /etc/passwd
jd001:x:1032:666:Javier Diaz:/home/jd001:/bin/rbash
ag002:x:8050:668:Alejandro Gonzalez:/home/ag002:/bin/rbash
jp003:x:1000:666:Jose Perez:/home/jp003:/bin/bash
ms004:x:8051:668:Maria Saenz:/home/ms004:/bin/rbash
rc005:x:6550:668:Rosa Camacho:/home/rc005:/bin/rbash
Lo primero que tenemos que tener en cuenta es el carácter separador que divide los campos, tanto a la hora de tratar el stdin
como el stdout
. Esto se traduce en FS=':'
y OFS=':'
.
En segundo lugar identificamos la posición en la que se encuentra el campo void
, en este caso es la número 6 y componemos su nuevo valor en base a la cadena requerida y el primero de los campos que contiene el login
, el código final: $6="/home/"$1
.
IMPORTANTE: No necesitamos print
ya que el resultado de la asignación será true
y el comportamiento por defecto de awk
cuando algo es verdadero es mostrar el registro afectado.
08. Cambiar el orden de los campos de modo que el primero pase a ser el final
Código final:
$ awk -F\: '{last=$1;$1=$NF;$NF=last}1' FS=":" OFS=':' /etc/passwd
/bin/rbash:x:1032:666:Javier Diaz:/home/jd001:jd001
/bin/rbash:x:8050:668:Alejandro Gonzalez:/home/ag002:ag002
/bin/bash:x:1000:666:Jose Perez:/home/jp003:jp003
/bin/rbash:x:8051:668:Maria Saenz:/home/ms004:ms004
/bin/rbash:x:6550:668:Rosa Camacho:/home/rc005:rc005
Aquí jugamos con la variable intermedia last
donde alojamos el valor del primer campo $1
, después intercambiamos su valor con el del último campo $1=$NF
, y finalmente asignamos last
a $NF
.
09. Hackeando traceroute
Partiendo de la salida del traceador:
$ traceroute -q 1 google.com 2>/dev/null
1 hitronhub.home (192.168.1.1) 5.578 ms
2 217.217.0.1.dyn.user.ono.com (217.217.0.1) 9.732 ms
3 10.127.54.181 (10.127.54.181) 10.198 ms
4 62.42.228.62.static.user.ono.com (62.42.228.62) 35.519 ms
5 72.14.235.20 (72.14.235.20) 26.003 ms
6 216.239.50.133 (216.239.50.133) 25.678 ms
7 mad01s24-in-f14.1e100.net (216.58.211.238) 25.019 ms
Deseamos computar el total de micro segundos que tarda un paquete en todo el recorrido investigado.
$ traceroute -q 1 google.com 2>/dev/null|\
awk '{total+=$(NF-1)}
END{print "Total ms: "total}'
Total ms: 153.424
La acción se ejecuta para todos los registros ya que no hay ningún tipo de filtraje.
total+=$(NF-1)
: acumulamos en la variable total el valor contenido en el penúltimo campo $(NF-1)
.
En la regla END
mostramos el valor final.
10. Procesos dependientes de un PID padre
En primer lugar obtenemos el PID
de nuestra shell:
$ echo $$
51026
Lanzamos procesos sleep
en segundo plano cuyo padre será nuestro PID
anterior.
$ sleep 10 & sleep 15 & sleep 20 &
[1] 86751
[2] 86752
[3] 86753
En base al comando ps
filtramos mediante awk
por el tercer campo $3
que corresponde al PPID
:
$ ps -ef|awk '$3==51026'
501 86751 51026 0 7:57PM ttys001 0:00.00 sleep 10
501 86752 51026 0 7:57PM ttys001 0:00.00 sleep 15
501 86753 51026 0 7:57PM ttys001 0:00.00 sleep 20
0 86754 51026 0 7:57PM ttys001 0:00.00 ps -ef
501 86755 51026 0 7:57PM ttys001 0:00.00 awk $3==51026
Nuestro objetivo final es volcar al terminal solo los procesos sleep:
$ ps -ef|awk '$3==51026 && /slee[p]/ {print $2" -> "$5}'
86751 -> 7:57PM
86752 -> 7:57PM
86753 -> 7:57PM
Para ello necesitamos añadir al la condición inicial mediante &&
la búsqueda del patrón /slee[p]/
en el registro.
La acción asociada será imprimir el segundo campo que corresponde al PID
del proceso y el quinto asociado al timestamp.
11. Como agregar datos a partir de una clave
Supongamos el siguiente fichero:
$ cat ips.dat
IP BYTES
81.220.49.127 328
81.220.49.127 328
81.220.49.127 329
81.220.49.127 367
81.220.49.127 5302
81.226.10.238 328
81.227.128.93 84700
Nuestro objetivo es determinar cuantos bytes han sido tratados por IP
.
$ awk 'NR>1{ips[$1]+=$2}
END{for (ip in ips){print ip, ips[ip]}}' ips.dat
81.220.49.127 6654
81.227.128.93 84700
81.226.10.238 328
Tenemos bastante que explicar:
NR>1{ips[$1]+=$2}
: Cuando el número de registro procesado es superior a 1 se ejecuta acción ips[$1]+=$2
. EL NR>1
es necesario para esquivar la cabecera del archivo, es decir, la primera línea. ips
es un array indexado por la ip (el campo $1
) al que se le asigna la sumatoria del campo 2 $2
.
Es importante tener en cuenta que awk
cuando no encuentra una key en un array añade un nuevo elemento, pero si ocurre lo lee y puede manipular su valor como en este caso particular.
En la parte del END
simplemente recorremos el array mostrando sus índices o keys y el valor contenido.
Este código se podría escribir de esta forma preservando el orden y omitiendo el uso de arrays aprovechando que las ip
s están ordenadas:
awk 'NR == 1{next}
lip && lip != $1{print lip,sum;sum=0}
{sum+=$2;lip=$1}
END{print lip,sum}' ips.dat
81.220.49.127 6654
81.226.10.238 328
81.227.128.93 84700
NR == 1{next}
: Omite la cabecera.
lip != $1 && lip != ""{print lip,sum;sum=0}
: Usamos lip
(last ip) como variable auxiliar y solo en el caso de que esta no sea nula lip
y &&
sea distinta del la ip actual lip != $1
mostraremos la ip anterior lip
y la sumatoria sum
para después inicializarla: sum=0
.
{sum+=$2;lip=$1}
: Aquí llevamos a cabo el incremento de la sumatoria de bytes sum+=$2
y asignamos el campo de ip actual a lip
.
En el bloque END
mostraremos los resultados para la última ip
así como la sumatoria final.
Este código preserva el orden pero aumenta la complejidad del programa.
12. Mostrar los registros entre dos patrones
Necesitamos extraer la líneas comprendidas entre OUTPUT
y END
.
$ cat pat.dat
test -3
test -2
test -1
OUTPUT
top 2
bottom 1
left 0
right 0
page 66
END
test 1
test 2
test 3
Este es un ejemplo clásico a la hora de ilustrar el funcionamiento del macheo de patrones y sus acciones asociadas al que ya le dedique un post (en inglés).
$ awk '/END/{flag=0}flag;/OUTPUT/{flag=1}' pat.dat
top 2
bottom 1
left 0
right 0
page 66
Como podemos ver como su funcionamiento se basa en asociar un valor para flag
verdadero al encontrar el patrón de inicio y falso al encontrar el que cierra.
Para evitar un paso adicional es importante el orden de las acciones, si lo hacemos siguiendo la secuencia lógica:
$ awk '/OUTPUT/{flag=1}flag;/END/{flag=0}' pat.dat
OUTPUT
top 2
bottom 1
left 0
right 0
page 66
END
Las etiquetas se muestran en la salida ya que después de encontrar el patrón de inicio OUTPUT
activamos el flag y la siguiente acción se realiza sobre el mismo registro, cosa que evitamos si esta activación la realizamos en el último paso de nuestro flujo.
Debemos evitar que flag
sea verdadero
cuando no deseemos mostrar los registro.
13. Convertir un campo en base a su contenido
Supongamos este fichero:
$ cat space.dat
10.80 kb
60.08 kb
35.40 kb
2.20 MB
1.10 MB
40.80 kb
3.15 MB
20.50 kb
Nuestro objetivo es determinar cuantas megas pesan nuestros registros.
$ awk '{total+= $1 / ($2=="kb" ? 1024: 1)}
END{print total}' space.dat
6.61365
Para poder entender su funcionamiento tenemos que tener claro como funciona el operador ternario al que ya dedique en su día un articulo completo (en inglés).
Sobre la variable total
acumulamos el resultado de dividir el primer campo $1
entre el segundo que gracias a la magia de este operador equivaldrá a 1024 en caso de que su valor sea kb
y a 1
si ya está en MBs.
Finalmente imprimimos el resultado total en el bloque END
.
14. Agrupando registros en columnas
Nuestro fuente original:
$ cat group.dat
string1
string2
string3
string4
string5
string6
string8
Pretendemos agrupar los registros en bloques de tres columnas para obtener la siguiente salida:
string1 string2 string3
string4 string5 string6
string8
Parece complejo , pero resulta mucho más simple si entendemos como usar el Output Field Separator OFS
:
$ awk 'ORS=NR%3?" ":RS; END{print "\n"}' group.dat
string1 string2 string3
string4 string5 string6
string8
Si establecemos el ORS
a un carácter en blanco todo la salida se agrupa en una sola línea o registro:
$ awk 'ORS=" "; END{print "\n"}' group.dat
string1 string2 string3 string4 string5 string6 string7
ORS=NR%3?" ":RS
: Finalmente usamos el operador ternario explicado anteriormente y evaluamos el modulo resultado de dividir el número de registro actual (teniendo en cuenta que el RS
no se ha tocado por lo que sigue siendo \n
) entre tres, si el resultado es true (distinto a cero) el ORS
pasara a ser un espacio en blanco, en caso contrario se le asignará el valor del RS
, es decir el salto de línea unix.
15. Procesando un fichero FASTA
En bioinformática, el formato FASTA es un formato de fichero informático basado en texto.
Supongamos el siguiente ejemplo:
$ cat fasta.dat
>header1
CGCTCTCTCCATCTCTCTACCCTCTCCCTCTCTCTCGGATAGCTAGCTCTTCTTCCTCCT
TCCTCCGTTTGGATCAGACGAGAGGGTATGTAGTGGTGCACCACGAGTTGGTGAAGC
>header2
GGT
>header3
TTATGAT
Nuestro objetivo es agregar los resultados de modo que obtengamos la longitud total de las secuencias de cada header y una sumarización total de la siguiente forma:
>header1
117
>header2
3
>header3
7
3 sequences, total length 127
awk
es una herramienta perfecta para este tipo de reporting, en este ejemplo usaremos:
awk '/^>/ { if (seqlen) {
print seqlen
}
print
seqtotal+=seqlen
seqlen=0
seq+=1
next
}
{
seqlen += length($0)
}
END{print seqlen
print seq" sequences, total length " seqtotal+seqlen
}' fasta.dat
La primera acción esta asociada a a la detección de la cabecera /^>/
ya que los header son aquellos en los que el registro comienza con el carácter >
Cuando la variable seqlen
tenga valor la mostraremos, en todo caso se volacará por el stdout
la cabecera, se acumula la longitud total en seqtotal
, el número de secuencias tratadas en seq
y se inicializará la longitud de la secuencia actual seqlen
, finalmente pasamos al siguiente registro con next
.
Las segunda acción es acumular en seqlen
la longitud de línea cuando no se trate de una cabecera.
En el bloque END
mostramos la longitud de secuencia remanente y los totales.
Este ejemplo se basa en mostrar la longitud de secuencia del header anterior, al procesar el primer registro evidentemente no tiene valor y se omite, en el END
se muestra la información disponible después de procesar la última línea.
16. Reporting complejo
Supongamos el siguiente fichero:
$ cat report.dat
snaps1: Counter: 4966
Opens: Counter: 357283
Instance: s.1.aps.userDatabase.mount275668.attributes
snaps1: Counter: 0
Opens: Counter: 357283
Instance: s.1.aps.userDatabase.test.attributes
snaps1: Counter: 5660
Opens: Counter: 37283
Instance: s.1.aps.userDatabase.mount275000.attributes
Nuestro objetivo es hacer un report
en el se muestre la línea de snaps y su instancia asociada pero unicamente cuando el valor del primer counter sea superior a cero, en este caso buscaríamos:
snaps1: Counter: 4966
Instance: s.1.aps.userDatabase.mount275668.attributes
snaps1: Counter: 5660
Instance: s.1.aps.userDatabase.mount275000.attributes
Para conseguir este resultado nuevamente jugaremos con patrones y flags:
awk '{$1=$1}
/snaps1/ && $NF>0{print;f=1}
f && /Instance/ {print;f=0}' report.dat
La primera acción {$1=$1}
se ejecuta para todos los registros y como ya hemos visto sirve para reconstruir los registros, en este caso el truco nos permite convertir los separadores basados en múltiples espacios en blanco en uno solo ya que es el valor por defecto para el OFS
.
Lo apreciamos mejor con un ejemplo:
$ awk '1' text.dat
one two
three four
$ awk '$1=$1' text.dat
one two
three four
La segunda acción se dispara cuando encontramos el patrón /snaps1/
y (&&
) el último campo es mayor que 0 $NF>0
. Mostraremos el registro con print
y daremos un valor verdadero al flag f=1
.
Por último cuando el flag es verdadero y encontramos el patrón de instancia f && /Instance/
imprimimos la línea y desactivaremos la variable: print;f=0
.
17. JOIN entre ficheros
Partimos de dos ficheros.
$ cat join1.dat
3.5 22
5. 23
4.2 42
4.5 44
$ cat join2.dat
3.5
3.7
5.
6.5
Necesitamos obtener las registros del primero cuyas columnas están presentes en join2.dat
. Es decir:
3.5 22
5. 23
Por supuesto que podríamos usar la utilidad unix join
ordenado el primer fichero:
$ join <(sort join1.dat) join2.dat
3.5 22
5. 23
Veamos como se puede hacer con awk
:
$ awk 'NR==FNR{a[$1];next}
$1 in a' join2.dat join1.dat
Como siempre descompongamos por acciones.
Cuando el número de registro procesado equivalga al número de registro del fichero actual, es decir NR==NFR
significará que estamos tratando el primer archivo.
La acción asociada será incorporar al array a
un elemento nulo indexado el contenido del primer campo $1
, es decir a[$1]
inmediatamente después pasamos a procesar el siguiente registro mediante next
.
La segunda acción se disparará de forma implícita cuando el NR
sea distinto al FNR
, es decir, cuando estemos procesando el segundo fichero, le aplicaremos el statement $1 in a
que será true cuando el primer campo del fichero join1.dat
este presente en el array formado por los campos del join2.dat
.
Como ya sabemos, cuando algo es verdadero en awk
por defecto se muestra el registro afectado por la condición por lo que obtenemos el resultado esperado.
18. Cruzando passwd y group
Supongamos estos dos clásicos unix:
$ cat /etc/group
dba:x:001:
netadmin:x:002:
$ cat /etc/passwd
jd001:x:1032:001:Javier Diaz:/home/jd001:/bin/rbash
ag002:x:8050:002:Alejandro Gonzalez:/home/ag002:/bin/rbash
jp003:x:1000:001:Jose Perez:/home/jp003:/bin/bash
ms004:x:8051:002:Maria Saenz:/home/ms004:/bin/rbash
rc005:x:6550:002:Rosa Camacho:/home/rc005:/bin/rbash
Nuestro objetivo es obtener un report login:nombre_grupo
:
d001:dba
ag002:netadmin
jp003:dba
ms004:netadmin
rc005:netadmin
Para ello volveremos a usar las técnicas de trabajo con varios ficheros vistas en el ejemplo anterior:
$ awk -F\: 'NR==FNR{g[$3]=$1;next}
$4 in g{print $1""FS""g[$4]}' /etc/group /etc/passwd
Para procesar /etc/group
volvemos a usar la comparación NR==FNR
para disparar la primera acción que almacenará en el array g
bajo el index correspondiente al ID del grupo $3
su nombre $1
, es decir g[$3]=$1
. Posteriormente rompemos cualquier procesamiento posterior del registro con next
.
La siguiente acción afectará a todos los registros del archivo /etc/passwd
, cuando el cuarto campo $4
(que corresponde al ID del grupo del usuario) este presente en el array $4 in g
mostraremos el login $1
y el valor asociado al elemento del array referido por la ID g[$4]
. En definitiva: print $1""FS""g[$4]
19. Conexiones por usuario a un servidor
Partimos de la salida del comando users
, ejemplo:
$ users
negan rick bart klashxx klashxx ironman ironman ironman
Necesitamos saber cuantos logeos tenemos por usuario
$ users|awk '{a[$1]++}END{for (i in a){print i,a[i]}}' RS=' +'
rick 1
bart 1
ironman 3
negan 1
klashxx 2
En este código la acción se ejecuta para todos los registros devueltos por el ejecutable ya que no existe ninguna filtro o patrón a procesar.
a[$1]++
: Inicializa como 1 o suma un elemento sobre la key de usuario $1
del array a
. En awk
cuando un valor no esta iniciado se considera cero en un contexto numérico.
En el bloque END
recorremos el array mostrando la key y su valor asociado.
20. Obteniendo la media total proporcionada por el comando uptime
Veamos la salida típica de esta utilidad:
$ uptime
11:08:51 up 121 days, 13:09, 10 users, load average: 9.12, 14.85, 20.84
¿Como podemos extraer la media total de las tres mediadas del load average?
$ uptime |awk '{printf "Media de carga: %0.2f\n",
($(NF-2)+$(NF-1)+$(NF))/3 }' FS='(:|,) +'
Media de carga: 14.94
Aquí entra en juego una nueva técnica, usamos como separador de campos FS
una expresión regular FS='(:|,) +'
.
Le estamos indicando al programa que el separador pueden ser tanto los dos puntos :
como las comas ,
seguidos por cero o más espacios.
Después simplemente nos quedamos con los tres últimos campos en base al NF
, realizamos la operación aritmética y mostramos el resultado usando la mascara más idónea para printf
.
Disclaimer 2
Si has llegado hasta aquí ¡gracias por el interés!
En mi opinión, awk
es un lenguaje infravalorado y que merece mucho mas cariño .
Si además te has quedado con ganas de más házmelo saber en los comentarios y considerare una segunda parte para terminar de matarte de aburrimiento.
Happy coding!
-
En ocasiones es conveniente forzar que
awk
reconstruya el registro completo, usando los valores actuales para elFS
yOFS
.Para hacerlo usamos esta asignación inocua:
$1 = $1
↩ -
La respuesta rápida, simplemente es un atajo para evitar usar la función
print
.En
awk
cuando se cumple una condición la acción por defecto es imprimir la línea actual en el input.$ echo "test" |awk '1'
Lo que es equivalente a:
echo "test"|awk '1==1'
echo "test"|awk '{if (1==1){print}}'