3.6.5. awk en una línea

awk es todo un lenguaje de propósito general orientado al tratamiento de textos, pero lo habitual es que se use dentro de scripts de la shell de forma muy sucinta como alternativa o complemento a cut, sed y grep. Por ello, dedicamos este epígrafe a explicar cuáles son los modos de uso más útiles relativos a esta función. La mayoría ya están explicados en el epígrafe que se le dedicó al presentar la manipulación de textos, pero procuraremos aquí ser más sistemáticos.

Advertencia

awk es más extenso y complicado de lo que aquí trataremos. De hecho, dispone de estructuras de control y pueden llegar a programarse con él manipulaciones bastante complejas.

3.6.5.1. Principios de funcionamiento

  1. Lee un documento registro a registro y, dentro de cada registro, distingue campos. En principio un registro se corresponde con una línea y cada campo con el conjunto de caracteres de no-espaciado que está separado del anterior y el posterior por uno o más caracteres de espaciado[1].

  2. Cada registro puede manipularse y puede generarse a partir de él una o ninguna salida. En este aspecto, actúa del mismo modo que cut, grep o sed.

  3. Dispone de una serie de variables predefinidas que pueden usarse para su programación. Las más socorridas son:

    NR

    Almacena el número de registro que está siendo procesado. Obviamente aumenta su valor en 1 cada vez que se pasa al siguiente registro. Para el primer registro el valor es 1 (y no 0).

    NF

    Es el número de total de campos que contiene el registro que se está procesando.

    $0

    Contenido completo del registro en procesamiento.

    $1, $2, etc.

    Contenido individual de cada campo del registro en procesamiento. Todas (y también $0) son de lectura y escritura, por lo que puede alterarse su contenido.

    RS

    Contiene el separador de registros. Por defecto, es el cambio de línea, de ahí que cada registro se corresponda con una línea. Sin embargo, si alteramos el valor, podemos hacer que awk interprete un registro como otra cosa. Cuando no es un único caracter, sino una cadena, la cadena se interpreta como una expresión regular.

    ORS

    Contiene el separador de registros para la salida. Cuando awk acaba de manipular un registro e imprime la salida correspondiente, añade al final el valor de esta variable. Por defecto es el cambio de línea.

    FS

    Contiene el separador de campos. Al igual que RS admite como valor una expresión regular. Su valor predeterminado es \s+.

    OFS

    Contiene el separador de campos para la salida. Por defecto, es el caracter de espacio.

3.6.5.2. Manipulaciones de una línea

  1. La más sencilla es hacer que awk haga de cat:

    $ awk '{print $0}' fichero.txt
    

    Lo interesante es ver que las instrucciones aplicables a cada registro se introducen dentro de un bloque {}. En nuestro caso, lo unico que hacemos es imprimir el contenido del propio registro.

  2. Avancemos un poco más emulando cat -n:

    $ awk '{print NR, $0}' fichero.txt
    

    De esta línea es interesante notar que hemos separado el número de registro, del contenido del mismo mediante una coma. Esto implica que en la salida se separen ambas variables mediante OFS, que como no lo hemos redefinido es el espacio. Una variante de lo anterior, podría ser esta:

    $ awk '{print NR ":", $0}' fichero.txt
    

    En este caso añadimos después del número de registro el carácter «:». Al no haber usado nada para separarlos, en la salida se yuxtapondrá el número de registro al carácter «:».

  3. Imprimimos el primero y el último campo de cada línea:

    $ awk '{print $1, $NF}' fichero.txt
    

    Obsérvese que, para imprimir el último campo, nos ha bastando con $NF.

  4. Listamos los nombres de usuarios existentes:

    $ getent passwd | awk -F: '{print $1}'
    

    En este caso necesitamos alterar el contenido de FS para lo cual existe específicamente una opción. También puede usarse -v que sería la forma general de pasar valores a los variables:

    $ getent passwd | awk -v FS=: '{print $1}'
    

    Si son varias las variables, basta con repetir varias veces la opción -v.

  5. Ídem, pero escribimos los nombres en mayúsculas:

    $ getent passwd | awk -v FS=: '{print toupper($1)}'
    

    La utilidad real de esto es muy reducida, pero nos sirve para ilustrar cómo awk dispone de funciones que permiten presentar un contenido modificado. Hay muchas funciones para la manipulación de cadenas.

  6. Filtrar registros: mostrar los usuarios cuya shell sea bash:

    $ getent passwd | awk -F: '$NF == "/bin/bash" {print $1}'
    

    La forma de hacerlo es incluir la condición, tal cual, antes del bloque. Si nuestra intención es mostrar toda la información de esos usuarios, la solución a la vista de la anterior es trivial:

    $ getent passwd | awk -F: '$NF == "/bin/bash" {print $0}'
    

    Ahora bien, cuando se introduce una condición y no se especifica cuál es la acción, se sobreentiende que esta es mostrar el registro. Por tanto, podríamos haber simplificado a:

    $ getent passwd | awk -F: '$NF == "/bin/bash"'
    
  7. Filtrar registros: mostrar sólo los usuarios cuyo nombre empieza por «u»:

    $ getent passwd | awk -F: '$1 ~ /^u/ {print $1}'
    

    La novedad es que usamos una expresión regular para lo cual necesitamos emplear el operador «~» y encerrar la expresión entre barras.

    Una variante de lo anterior podría haber sido:

    $ getent passwd | awk -F: '$0 ~ /^u/ {print $1}'
    

    o de forma más simple:

    $ getent passwd | awk -F: '/^u/ {print $1}'
    

    porque, cuando no se expresa con qué se compara, se sobreentiende que es el registro completo, o sea, $0.

  8. Mostrar la información gecos de un usuario cuyo nombre tenemos definido fuera de awk, es decir, en el script de la shell que usa awk.

    Para esta tarea podemos usar dos estrategias:

    • Pasar la variable con -v:

      $ USUARIO=pepito
      $ getent passwd | awk -F: -v USU=$USUARIO '$1 == USU {print $5}'
      
    • Hacer que la shell sustituya directamente en el código de awk:

      $ USUARIO=pepito
      $ getent passwd | awk -F: '$1 == "'$USUARIO'" {print $5}'
      
  9. Aplicar distintos filtros a distintos bloques:

    Ya se ha visto que al aplicar un filtro de la manera antes expuesta, las líneas que no cumplen el filtro desaparecen. Sin embargo, awk permite definir distintos bloques, de manera que cada registro aplicará todos aquellos bloques con los que cumpla. Para ilustrarlo supongamos que queremos poner «coleguitas» (con gid 110) como grupo principal de todos los usuarios que empiezan por «u», y no hacer nada con el resto. La siguiente orden generaría un nuevo /etc/passwd que cumple con ello:

    $ awk -F: -v OFS=: '/^u/ {$4=110; print $0} /^[^u]/'
    

    Nota

    Por supuesto, awk posee estructuras condicionales (if) que pueden usarse dentro de un bloque (como también tiene bucles, p.e.). Pero su uso implica líneas demasiado largas que son difíciles de leer por lo que quedan fuera de esta ridícula guía. Por ejemplo, lo anterior se podría haber resuelto así:

    awk -F: -v OFS=: '{if(/^u/) $4=110; print $0}' /etc/passwd
    

    que en este caso particular es adminisible, pero no es lo habitual.

3.6.5.3. Sabores

Hay tres versiones principales de awk[2]:

  1. nawk, que es la versión mantenida por Brian Kernighan, coautor del awk original. Es la usada por las distribuciones BSD (incluido Mac Os X).

  2. mawk, que es una versión optimizada para ser rápida. Es la que trae de serie debian.

  3. gawk, que es la versión del proyecto GNU. Incluye muchas extensiones inexistentes en las dos versiones anteriores.

Para una comparación de las versiones y en qué grado soportan el estándar POSIX, consulte esta entrada en reddit.

Notas al pie