Fork me on GitHub

klashxx    Archive    About    Talks    Feed

Django vs Sysadmin

License: CC BY-NC-SA 4.0

Prefacio

Relataremos la historia de un brujo Sysdadmin que vivía en el Reino del lejano Backend, triste y enclaustrado entre terminales, conjurando hechizos en Perl y awk, hasta que un buen día (¿o malo quizás?) se le encomendó la noble misión de pregonar en los Siete Reinos HTML los datos que emanaban de las mazmorras SQL.

Solo una gran magia podría satisfacer tan alta causa: Python.

Texto base sobre el que versará la charla Djando vs Sysadmin a impartir en Cáceres durante la PyConES 2017.

Podéis acceder a las slides aquí.

:warning: Disclaimer :warning:

Para ilustrar este post se ha creado un aplicación demo cuyo código reside en este repo de Github y se ha estructurado en ramas.

Cada una de las ramas es usable per se y demuestran cómo implementar una funcionalidad concreta.

Requisitos:

La dinámica de instalación es siempre la misma:

  • checkout de la rama.

  • Ejecución de comandos en docker-compose.

Las instrucciones detalladas se pueden encontrar en los README.md de las branches y en los vídeos linkados (¡gracias asciinema!).


Ramas:

  1. Gestión de usuarios y autenticación.

  2. Estructura y Apps. Configurar el proyecto.

  3. DRF. Diseñando nuestro Backend.

  4. Permisos. Quién y donde puede acceder.

  5. Frontend. Modernizando nuestra web.

  6. Tricks. Pequeños trucos para mejorar nuestro proyecto.


Es necesario aplicarlas en orden, desde la 01 a la x, para observar cómo va creciendo y transformándose la aplicación mediante cambios incrementales, conservando las modificaciones previas en BD.

Finalmente podremos hacer checkout al master que contiene el código de la demo completa.

:point_right: ATENCION: Material complementario. NO es necesario instalar la demo para seguir la charla :point_left:


Introducción

Me llamo Juan Diego, soy Sysadmin y hasta hace cinco meses no sabía que era HTML :grimacing:.

Bueno … quizás esta última afirmación sea un poco exagerada, para ser honesto había usado templating (jinja2) para generar mails bonitos y experimentado mínimamente con Flask.

¿Cómo me metí en este embolao?

Me ofrecí voluntario OMG :neckbeard: … lo cierto es que llevaba tiempo con inquietudes web pero nunca había dispuesto del tiempo necesario crear un side Project, así que cuando laboralmente se me se planteo la necesidad de elaborar una web app la ocasión la pintaron calva :godmode:.

Como suele pasar siempre cuando uno, o al menos yo, se encuentra, sin experiencia, ante una tarea de estas dimensiones. Se falla, se falla bastante :sob:.

El objetivo este post es claro y responde a una pregunta:

¿Cómo puedo ayudar a alguien que ya programe en Python pero sea un completo novato en el mundo web a comenzar con Django?

La respuesta es simple: compartiendo mi experiencia, lo que me sirvió y lo que no.

:warning: ATENCION: Este texto NO es un tutorial.


¿Qué Framework escojo?

La respuesta es obvia :grin: y la selección natural:

  • Hecho en Python: en mi opinión el mejor lenguaje para administrar sistemas.

  • Fiable: GitHub Starts, comunidad, webs en producción.

  • models.py: Mi portal debía hacer uso de modelos (¿?) con persistencia en BBDD y Django como ORM es una pasada.

  • Extensible: ¿os habéis dado cuenta que hablo de portal? … pues este también fue un punto fundamental ya que no solo se me solicito la programación de una aplicación, además se deseaba que el sistema fuera extensible, es decir que se pudieran acoplar nuevas apps aprovechando una infraestructura común (autenticación, plantillas, etc.) …. el famoso po ya que.

  • Django Rest Framework (lo comentaremos más adelante).

  • El Admin Site.

  • Miles de paquetes.


Consideración previa y primer error

Caí en la tentación e intenté salir por la vía rápida, instale aplicaciones y boilerplate como un loco, mi objetivo era claro intentar encontrar el santo grial, un código que me lo diera todo (o mucho) hecho.

Mi primera idea fue …¡¿lo mismo me sirve un CSM?!

Descubrí Awesome Django (highly recommended) y probé casi todos (Wagtail, Django-CMS, Mezzanine) :blush:

En ocasiones, raras, conseguí que alguno funcionara :sunglasses:, pero lo que hacía por debajo era magia negra para mí :fearful: y eso me imposibilitaba poder adaptar el CMS a mis necesidades, el zen Django todavía estaba muy lejos de mi espíritu .

Así que paré, reflexioné y decidí empezar por los basics: concretamente los tutoriales de Django Girls y Mozilla que os recomiendo fervientemente (en ese orden).

Mi conclusión: Para ser productivos con este framework debemos comprender que se está moviendo under the hood.


El entorno de desarrollo

El mínimo exigible es usar virtualenv, con esto evitaremos al menos romper cosas.

Pero ¿Qué pasa con la base de datos? ¿y al subir a producción?

Clonar un repositorio y lanzar un comando… ya tenemos nuestra web levantada con su proxy-server y su BD ¿no resulta mágico? ¡Pues esa es la idea!

Mis herramientas favoritas para servir a este objetivo son Git y Docker. El código del repositorio ilustra cómo usar diferentes técnicas (Dockerfile, docker-compose) para levantar el entorno de forma automatizada.


¿Qué motor de BBDD usaremos?

Para hacer los primeros experimentos nos vale SQLite, para entornos productivos cualquiera de las otras tres opciones: Oracle, MySQL o PostgreSQL.

Como administrador de BBDD esta última me parece la solución opensource mas óptima.


El settings.py y nuestro .env

El settings.py contiene configuración global de nuestro proyecto, podríamos verlo como el profile que se carga antes de ejecutar un script bash, es absolutamente crítico.

La regla fundamental es ocultar el valor de las variables que escondan secretos, una de las formas de hacerlo es esconderlas en un archivo .env que, por supuesto, NO DEBE ESTAR VERSIONADO.

Para acceder estos valores me ha sido realmente útil el módulo Decouple.

from decouple import config
SECRET_KEY = config('SECRET_KEY')

Otra punto a tener en cuenta es que Django dispone de un módulo para acceder a las variables fijadas en settings por lo que este fichero puede ser un buen lugar para setear ciertos valores a los que deseemos acceder desde cualquier punto del proyecto.

Como último consejo añadir que la librería DJ Database URL nos permite usar la configuración en url de nuestra BD.

.env

DB_URL=postgresql://postgres:postgres@postgres:5432/postgres

settings.py

DATABASES = {
    'default': dj_database_url.config(default=config('DB_URL')),
    }

La estructura del proyecto o segundo error

La documentación oficial propone la siguiente estructura a modo de ejemplo:

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    polls/
        __init__.py
        admin.py
        migrations/
            __init__.py
            0001_initial.py
        models.py
        static/
            polls/
                images/
                    background.gif
                style.css
        templates/
            polls/
                detail.html
                index.html
                results.html
        tests.py
        urls.py
        views.py
    templates/
        admin/
            base_site.html

Por lo que el proyecto (a partir de ahora sysgate) empezó con dos apps:

  • Gestión de la autenticación (usuarios y grupos)
  • La aplicación en si (llamémosla metrics)
sysgate/
    manage.py
    sysgate/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    account/
        __init__.py
        …
    metrics/
        __init__.py
        …
    templates/
        account/
            login.html
        metrics/
            home.html

Todo funcionaba de acuerdo a lo previsto pero se me planteo un problema a la hora de añadir una nueva aplicación manager.

Mi objetivo era el siguiente: Deseaba que el proyecto tuviera un home común donde se mostraran las aplicaciones disponibles al usuario en función de sus permisos.

¿Dónde incluyo este home?

  • ¿En account?: no, se trata de separar funcionalidades y esta aplicación tiene un propósito único especifico que es la gestión de usuarios.

  • ¿En metrics?: no way, esto nos obligaría a duplicar el código en manager, o lo que es peor usar una vista de metrics.

Finalmente opte por una de las soluciones más extendidas y que se basa en la creación de una aplicación core, donde agrupar precisamente todas las funcionalidades comunes que puedan ser llamadas desde cualquier otra aplicación, quedando la estructura así:

sysgate/
    manage.py
    sysgate/
        __init__.py
        settings.py
        urls.py
        wsgi.py
        routers.py
        account/
            __init__.py
            ...
        apps/
            __init__.py
            core/
                templatetags/
                    __init__.py
                    core_tags.py
                templates/
                    core/
                        home.html
                        ...
                __init__.py
                ...
            metrics/
                templates/
                    metrics/
                        home.html
                        ...
                __init__.py
                ...
            manager/
                templates/
                    manager/
                        home.html
                        ...
                __init__.py
                ...
        static/
            js/
                metrics.js
                ...
            css/
                metrics.css
                ...
        fixtures/
            metrics.json
            ...
        templates/
            account/
                login.html
                ...
            base.html
            ...

La vista home, se renderizará a partir de la plantilla core ¿make sense?

De esta forma puedo añadir aplicaciones de forma indefinida aprovechando la infraestructura común expuesta por ACCOUNT y CORE.

Igualmente me permite trabajar independientemente en cualquier otra aplicación sin temor a romper nada, si no tocamos el código compartido todo seguirá funcionando lo que facilita el desarrollo paralelo a múltiples miembros del equipo.

:yum: PRODUCTIVITY BOOST :yum:


El enrutamiento

Es uno de mis mecanismos favoritos del Framework y se basa en expresiones regulares.

Cuando llamamos a una url, Django en primer lugar carga el urls.py de la raíz del proyecto y a partir de ahí tratara de resolverla.

urlpatterns = [
    url(r'^', include('apps.core.urls', namespace='core')),
    url(r'^account/', include('account.urls'), name='Autenticación'),
    url(r'^metrics/', include('apps.metrics.urls'), name='Métricas'),
    url(r'^manager/', include('apps.manager.urls'), name='Manager'),
    url(r'^admin/', admin.site.urls),
]

¿Que pasa si introducimos nuestro dominio a secas?

Django recorrerá las cinco regex que tenemos parametrizadas, determinará que la única que cumple el patrón es la primera, donde le indicamos que cargue las urls de la aplicación core:

urlpatterns = [
    url(r'^$', views.Home.as_view(), name='home'),
]

En este caso solo tenemos un patrón contemplado la cadena vacía, que enrutaremos hacia la vista home del proyect.

:bulb: TIP :bulb: La idea aquí es ser lo más modular posible, cada app gestionara sus propias urls intentando que la del proyecto raíz quede lo más simple y limpia posible.


Modelos y vistas …¿Dónde está el controlador?

Para comprender Django es fundamental entender cómo funciona su MVC:

Django parece ser un framework MVC, pero ustedes llaman al Controlador «vista», y a la Vista «plantilla». ¿Cómo es que no usan los nombres estándares?

En nuestra interpretación de MVC, la «vista» describe el dato que es presentado al usuario. No es necesariamente cómo se ve el dato, sino qué dato se muestra. La vista describe cuál dato ve, no cómo lo ve. Es una distinción sutil.

Entonces ¿donde entra el «controlador»? En el caso de Django, es probable que en el mismo framework: la maquinaria que envía una petición a la vista apropiada, de acuerdo a la configuración de URL de Django.

Si busca acrónimos, usted podría decir que Django es un framework «MTV», esto es «modelo», «plantilla» y «vista». Ese desglose tiene mucho más sentido.

En la práctica nos basta con saber:

  1. Diseñar los modelos.
  2. Programar las vistas y su template.

Un modelo lo podríamos definir como una clase cuyos objetos instanciados están dotados de persistencia en BD.

Un modelo se traduce en una tabla cuyos campos se mapean con las variables de la clase modelo.

Diseñarlos requiere pensar detenidamente en los datos que se desean representar.

Para ahorrarnos curro, podemos tomar como ejemplo el modelo User.

Los modelos además pueden relacionarse entre sí, mediante campos / variables comunes (por ejemplo mediante ForeignKey).

No necesitamos SQL, ya que nos proporcionan una API query pythonica.

:bulb: TIP :bulb: Por defecto Django crea las tablas mediante las migrations, pero podemos establecer que el modelo no sea manejado y de esa forma podremos incorporar una tabla preexistente al ecosistema, es una buena forma de integrar datos provenientes de aplicaciones externas, y se nos proporcionan utilidades para inspeccionar el modelo.

:bulb: TIP :bulb: Campo único, Django se lleva mal con los modelos formados por pks en múltiples campos.

Las vistas, por norma general, se ocupan de representar ciertos datos provenientes de nuestros modelos.

class Home(View):
    def get(self, request, *args, **kwargs):
        metricas = Metrica.objects.all().order_by('tipo')
        return render(request,
                      'metrics/home.html',
                      context={'metricas': metricas})

:bulb: TIP :bulb: Usar Class Based Views … pueden parecer más complicadas pero a largo plazo compensa su aprendizaje por la potencia que ofrecen

Y finalmente el template devuelve el html.

<h1>Metrics App</h1>
<p>Bienvenid@ <strong>{{ request.user }}</strong></p>
<p>Disponibles:</p>
<table style="border: 1px solid black;">
{% for metrica in metricas %}
  <tr>
    <td>{{ metrica.nombre }}</td>
    <td>{{ metrica.get_tipo_display }}</td>
  </tr>
{% endfor %}
</table>

Autenticación y registro

En la estructura del proyecto ya hemos adelantado que lo ideal es mantener la funcionalidad común fuera del scope de las aplicaciones aislables, por lo que la primera de medida debe ser sacar la gestión de usuarios, en el ejemplo que estamos tratando se sitúa incluso en diferente nivel.

:bulb: TIP :bulb: fundamental es SIEMPRE heredar del Abstractuser, nos permitirá olvidarnos del modelo Profile. Esto debe hacerse siempre en la creación del proyecto ya que la migración de el modelo de User es muy complicada.

:bulb: TIP :bulb: usar las auth views que ya nos viene de serie y permiten avanzar rápido en el proyecto sin perjuicio de una customización posterior.


Django Rest Framework (o GraphQL)

Otra de mis grandes pifias.

Mi primera aplicación se basaba en extraer un contexto en el views.py y enviársela al template para que renderizara los resultados.

Para páginas sencillas el resultado era el esperado pero la cosa se complica si tienes que representar miles de datos.

La plantilla se convierte en un infierno de llaves y todo tiende a romperse con facilidad.

There must be a better way.

:bulb: TIP :bulb: separa el Backend (API) del Frontend.

La idea es simple, nuestro proyecto expondrá una API, en nuestro caso REST que será invocada directamente desde nuestras plantillas.

Al igual que ocurre con la separación de aplicaciones en la estructuración del proyecto, si separamos el Backend del Frontend podremos dividir el trabajo de forma más eficiente, además la API podrá ser integrada por otras aplicaciones por ejemplo móviles.

La tecnología trending es GraphQL y sin duda es el futuro, pero DRF es sin duda el mejor añadido a Django, y para ciertas querys resulta de difícil substitución, un caso claro es el de las métricas donde necesitamos todos los campos y de forma secuencial.


Frontend

Los templates son base del Frontend y la tercera pata del MVT, es donde vamos a escribir nuestro HTML (JS y demás).

Es un sistema diseñado de forma muy inteligente, teniendo en cuenta que Django no está enfocado a la construcción de single-page applications.

Cada app tendrá sus propios templates encargados de renderizar los datos proporcionados por la vista que invoca o extraídos directamente desde la API (si la hemos usado claro).

Se sitúan en la ruta template/app que debe colgar de la carpeta donde esté el código de la aplicación.

Las plantillas del proyecto raíz colgarán del primer nivel.

Ejemplo en sysgate:

├── account
├── apps
│   ├── core
│   │   ├── templates
│   │   │   └── core
│   │   │       └── home.html
│   │   ├── templatetags
│   │   │   ├── __init__.py
│   │   │   └── core_tags.py
│   └── metrics
│       ├── templates
│       │   └── metrics
│       │       └── home.html
├── templates
│   ├── 404.html
│   ├── account
│   │   ├── login.html
│   │   ├── profile.html
│   │   └── registro.html
│   ├── base.html
│   └── base_generic.html

Para el neófito es vital entender tres conceptos:

  1. La herencia
  2. Lenguaje
  3. Custom Tags y Filtros

La herencia nos permite extender (y modificar) la plantilla base que contendrá los elementos básicos y el look and feel de nuestra web, facilitándonos enormemente la tarea de construir una nueva página ya que los elementos comunes ya nos vienen dados (DRY).

Podríamos verlo, construyendo un paralelismo, como la herencia de de una clase Python, podemos heredar todo el código de la clase base, pero también podemos ampliarlo y modificarlo

Respecto al lenguaje poco que mencionar, Django proporciona su propia sintaxis y nos permite expresarnos en el template de modo programático proporcionándonos una enorme versatilidad.

Los tags y filtros nos permiten incorporar código Python y usarlo en nuestras plantillas.

Para visualizar estos conceptos, supongamos este tag:

from django import template

register = template.Library()

@register.filter('en_grupo')
def en_grupo(user, group_name):
    groups = user.groups.all().values_list('name', flat=True)
    return True if group_name in groups else False

Y el template que lo renderiza:


{% extends 'base.html' %}

{% load core_tags %}

{% block content %}

<div class="container">
  {% if request.user|en_grupo:"pycones" or user.is_superuser %}
    <div class="jumbotron container-fluid">
      <h1>Metrics</h1>
      <a class="btn" href="{% url 'metrics:home' %}">Acceder</a>
    </div>
  {% endif %}

  {% if not user.is_authenticated %}
    <div class="alert" align="center" role="alert">
      <button type="button" class="close"></button>
      Login para acceder
    </div>
  {% endif %}
</div>

{% endblock %}


Mi web no es cool o tercer error

Andaba yo muy contento con los resultados del trabajo hasta la primera reunión de seguimiento con los usuarios donde la primera pregunta fue, … ¿y esto cómo se ve en el móvil?

¿OLA K ASE?

No se me había pasado por la cabeza esa posibilidad: se veía mal, muy mal.

La cruda (o no) realidad es esta: Si deseas que tu web tenga un interfaz de usuario moderno tendrás que ir más allá de Python y un HTML básico.

Yo me he apoyado fundamentalmente en estos dos elementos:

  • Bootstrap
  • Javascript

Bootstrap es un framework web basado principalmente en HTML y CSS que mejora (o hace más vistosos) los elementos que incluimos en nuestros templates, además permite que nuestras páginas sean responsive sin demasiada dificultad (Sistema en grid)

Javascript es un lenguaje que tiene la gran ventaja de poder ejecutarse en el navegador. Existen innumerables librerías .js muchas de ellas super cool, que nos permiten hacer verdaderas viguerías con los datos devueltos por nuestro Backend, no debemos convertirnos en talibanes del lenguaje, JS es una utilidad imprescindible en el Frontend.

Ejemplo:


{% extends 'base.html' %}

{% load static %}

{% block extrajs %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
{% endblock %}

{% block content %}

<div id="container" class="container">
  <table>
    <thead>
      <tr>
        <th>Metric</th>
        <th>Tipo</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="metrica in metricas">
        <td>[[ metrica.nombre ]]</td>
        <td>[[ metrica.long_tipo ]]</td>
      </tr>
    </tbody>
  </table>
</div>

<script>
  var v = new Vue({
    delimiters: ['[[', ']]'],
    el: '#container',
    data: {
      metricas: []
    },
    mounted: function(){
      $.get('/metrics/api/v1/metricas/', function(data){
        v.metricas = data;
      })}
  })
</script>
{% endblock content %}


Gestión de permisos

Otro de los temas básicos en una aplicación en producción, la infraestructura es común pero los usuarios no deben percibirlo salvo en el look and feel de la pagina, uno solo debe ver lo que debe ver.

Podemos y debemos controlar el acceso a nuestras urls a tres niveles:

  • Templates

  • Vistas

  • Serializadores

Vistas

def super_o_esta_en_grupo(user, grupo):
    if not user.is_authenticated():
        return False

    if user.is_superuser:
        return True

    if user.groups.filter(name=grupo).exists():
        return True

    return False
@method_decorator(login_required, name='dispatch')
class Home(UserPassesTestMixin, View):
    def test_func(self):
        return super_o_esta_en_grupo(self.request.user,
                                     grupo='pycones')
    def get(self, request, *args, **kwargs):
        return render(request, 'metrics/home.html')

Serializadores

class EsSuperOTienePermisosPorGrupo(BasePermission):
    def has_permission(self, request, view):
        gr_auth_map = getattr(view, 'grupos_autorizados', {})
        grupos_autorizados = gr_auth_map.get(request.method, [])
        return any([super_o_esta_en_grupo(request.user, grupo)
                    for grupo in grupos_autorizados])
class MetricasViewSet(viewsets.ModelViewSet):
    grupos_autorizados = settings.GR_AUTH_METRICS
    permission_classes = (IsAuthenticated,
                          EsSuperOTienePermisosPorGrupo,)
    serializer_class = SerializerMetricas
    http_method_names = ['get']
    filter_backends = (OrderingFilter, SearchFilter,)
    search_fields = ('tipo',)

    def get_queryset(self):
        queryset = Metrica.objects.all().order_by('tipo')
        tipo = self.request.query_params.get('tipo', None)

        if tipo is not None:
            queryset = queryset.filter(tipo=tipo)

        return queryset

:bulb: TIP :bulb: Django tiene un control muy fino de permisos por usuario, pero podemos simplificar mucho la gestión usando los grupos como si fueran roles.


Estrategias de Deploy

El manage.py está muy bien para desarrollar, pero los tráficos serios deben ser atendidos desde un servidor web propiamente dicho, mi opción predilecta para producción es NGINX.

NGINX se encargará de servir el contenido estático y mediante un proxyserver redireccionará las peticiones de contenido dinámico a la url levantada por el servidor WSGI.

server {
    listen 80;
    server_name localhost;
    charset utf-8;

	location ~ ^/favicon.(\w*)$ {
		alias /static/img/favicon.ico;
	}

    location /static {
        autoindex on;
        root /;
    }

    location / {
        proxy_pass http://sysgate:8000;
    }
}

Necesitamos por supuesto un servidor Python que será el que mueva nuestro código, mi elegido es Gunicorn.

CMD ["gunicorn", "sysgate.wsgi", "--log-level=debug", "-w 2", "-b 0.0.0.0:8000"]

En el repo podéis encontrar un ejemplo práctico de cómo combinar estos elementos y asociarlos a otra tecnología puntera de despliegue: Docker.


The Killing machine

Suppose we need to purge the processes in our system having two conditions:

  • Running for 24 hours or more.
  • Matching a regexp

This situation could be tricky sometimes.

killall

It will be the perfect tool when there’s no need to inspect the command args, only process name is taken in consideration.

-o, –older-than Match only processes that are older (started before) the time specified. The time is specified as a float then a unit. The units are s,m,h,d,w,M,y for seconds, minutes, hours, days, weeks, Months and years respectively.

-r, –regexp Interpret process name pattern as an extended regular expression.

pkill

It solves the problem of the hiding parameters:

-f Match the pattern anywhere in the full argument string of the process instead of just the executable name.

But it does not provide a way to sort processes by elapsed time.

So … Let’s build our custom solution.

Let’s go back to our old friend ps .

From the man page:

-e Select all processes.

-o format user-defined format.

AIX FORMAT DESCRIPTORS

This ps supports AIX format descriptors, which work somewhat like the formatting codes of printf(1) and printf(3). For example, the normal default output can be produced with this: ps -eo "%p %y %x %c"

   CODE   NORMAL   HEADER
   %C     pcpu     %CPU
   %G     group    GROUP
   %P     ppid     PPID
   %U     user     USER
   %a     args     COMMAND
   %c     comm     COMMAND
   %g     rgroup   RGROUP
   %n     nice     NI
   %p     pid      PID
   %r     pgid     PGID
   %t     etime    ELAPSED
   %u     ruser    RUSER
   %x     time     TIME
   %y     tty      TTY
   %z     vsz      VSZ

First things first, To Kill a process, we need is its PID, then get how long has it been running and finally the command name and it’s args.

This be accomplished by this format string using the codes mentioned in the table above:

ps -eo "%p>~<%t>~<%a"

NOTE : It’s important to choose a complicated string as separator between our fields >~<, we don’t want to find the same one inside the command name or the args garbling our data.

To process this output let’s compose an awk1 oneliner, step by step.

How to get processes running for + 24h?

In ps man page:

etime ELAPSED elapsed time since the process was started, in the form [[dd-]hh:]mm:ss

So, a dash char in the second field means that the program has been running for at least 24 hours.

Example:

$ ps -eo "%p>~<%t>~<%a" | awk -v '$2 ~ /-/' FS='>~<'
  528>~<49-04:37:37>~</sbin/udevd -d
  746>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log800 86400
  747>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log445 86400
  748>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log1447 86400
  749>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log450 86400
 2170>~<49-04:37:14>~</sbin/rsyslogd -i /var/run/syslogd.pid -c 5
 2204>~<49-04:37:14>~<irqbalance --pid=/var/run/irqbalance.pid
 2270>~<49-04:37:14>~</usr/sbin/mcelog --daemon
 6892>~<49-04:37:01>~</usr/sbin/snmpd -LS0-6d -Lf /dev/null -p /var/run/snmpd.pid
 6920>~<49-04:37:01>~<xinetd -stayalive -pidfile /var/run/xinetd.pid

NOTE: FS is set to the string used in ps format: >~<

Does the command line match our regexp?

Last step, check if the command + args (%a) contains our regexp, for this example the rotatelogs string.

$ ps -eo "%p>~<%t>~<%a" | awk -v r="rotate.*access.*" '$2 ~ /-/ && $3 ~ r' FS='>~<'
  746>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log800 86400
  747>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log445 86400
  748>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log1447 86400
  749>~<21-08:21:52>~</dummys/apache/bin/rotatelogs -f /logs/access_log450 86400

Lets print only the pids.

$ ps -eo "%p>~<%t>~<%a" | awk -v r="rotate.*access.*" '$2 ~ /-/ && $3 ~ r{printf "%d ",$1}' FS='>~<'
  746 747 748 749

Bash command substituion will make the final trick.

$ kill $(ps -eo "%p>~<%t>~<%a" |\
  awk -v r="rotate.*access.*" '$2 ~ /-/ && $3 ~ r{printf "%d ",$1}' FS='>~<')

Interfaces en Go

Un interface en Go es un tipo, tan simple como eso.

Parce una tontería pero una vez que tenemos claro esto lo demás viene rodado.

Eso si es un tipo especial, especial porque solo puede contener métodos, es decir, las acciones que se podrán ejecutar sobre nuestros tipos, generalmente structs.

Para empezar supongamos un objeto Bici sobre el que definimos un método para establecer la marcha que vamos a usar:

package main

type Bici struct {
	marcha int
}

func (b *Bici) PonMarcha(marcha int) {
	b.marcha = marcha
}

func main() {
	vehiculo := &Bici{}
	vehiculo.PonMarcha(14)
}

:warning: NOTA: PonMarcha debe implementarse con un pointer receiver para permitir la modificación de los valores / propiedades de la estructura.1

¡Vale fácil! Ahora añadimos una Moto:

package main

type Vehiculo interface {
	PonMarcha(marcha int)
}

type Bici struct {
	marcha int
}

func (b *Bici) PonMarcha(marcha int) {
	b.marcha = marcha
}

type Moto struct {
	marcha int
}

func (m *Moto) PonMarcha(marcha int) {
	m.marcha = marcha
}

func main() {
	vehiculo := &Bici{}
	vehiculo.PonMarcha(14)
	vehiculo = &Moto{}
	vehiculo.PonMarcha(2)
}

Parece directo… pero este código no es compilable:

./main.go:29: cannot use Moto literal (type *Moto) as type *Bici in assignment

Moto y Bici no tienen nada en común … ¿o sí?

Parece evidente que estos dos objetos comparten ciertas acciones / métodos, en nuestro caso PonMarcha, por lo que podríamos definir un tipo interface que los agrupe:

type Vehiculo interface {
	PonMarcha(marcha int)
}

Veamos el código completo:

package main

type Bici struct {
	marcha int
}

func (b *Bici) PonMarcha(marcha int) {
	b.marcha = marcha
}

type Moto struct {
	marcha int
}

func (m *Moto) PonMarcha(marcha int) {
	m.marcha = marcha
}

type Vehiculo interface {
	PonMarcha(marcha int)
}

func main() {
        var vehiculo Vehiculo

	vehiculo = &Bici{}
	vehiculo.PonMarcha(14)
	vehiculo = &Moto{}
	vehiculo.PonMarcha(2)
}

¡Nuestro código ya compila! Podemos usar una variable tipo Vehiculo porque sabemos que tanto el tipo Bici como el tipo Moto satisfacen su interface.

Esa es la condición sin ecua non: todos los tipos deben implementar todos los métodos del interface, pero no quedan limitados por el, por ejemplo a Moto se le podría añadir la acción para echar gasolina:

func (m *Moto) EchaGasolina(nivel int) {
    m.nivel = nivel
}

Seguiría satisfaciendo el interface Vehiculo pero , OJO, como es lógico, no se podría acceder al nuevo método desde el:

./main.go:36: vehiculo.EchaGasolina undefined (type Vehiculo has no field or method EchaGasolina)

Stringer

Este es el ejemplo clásico del interface Stringer que nos acercará mucho más a su uso más práctico.

Está definido en el paquete fmt y su código no puede ser más simple:

type Stringer interface {
        String() string
}

Todo lo que nos importa es: si definimos un objeto que satisfaga este interface podremos aprovecharnos la funcionalidad implementada en print definida como:

The String method is used to print values passed as an operand to any format that accepts a string or to an unformatted printer such as Print.

Un tipo que implemente el interface Stringer podrá pasarse a cualquier función de impresión de fmt para obtener su representación.

Veámoslo en nuestro ejemplo:

package main

import (
  "fmt"
  "strconv"
  )

type Bici struct {
    marcha int
}

func (b *Bici) PonMarcha(marcha int) {
    b.marcha = marcha
}

func (b Bici) String() string {
    return "Bici en marcha número: "+strconv.Itoa(b.marcha)
}

type Moto struct {
    marcha int
    nivel int
}

func (m *Moto) PonMarcha(marcha int) {
    m.marcha = marcha
}

func (m Moto) String() string {
    return "Moto en marcha número: "+strconv.Itoa(m.marcha)
}

type Vehiculo interface {
    PonMarcha(marcha int)
    String() string
}

func main() {
    var vehiculo Vehiculo

    vehiculo = &Bici{}
    vehiculo.PonMarcha(14)
    fmt.Println("Vehículo: ", vehiculo)

    vehiculo = &Moto{}
    vehiculo.PonMarcha(2)
    fmt.Println("Vehículo: ", vehiculo)
}

Nos devuelve:

Vehículo:  Bici en marcha número: 14
Vehículo:  Moto en marcha número: 2

Interface{}

Es el interface vacio, no implementa ningún método y por lo tanto es satisfecho por cualquier tipo.

En base a este tipo es posible implementar una función que reciba cualquier tipo de valor, por ejemplo fmt.Print que tiene la siguiente signature:

func Print(a ...interface{}) (n int, err error)

:warning: Nota: el tipo precedido por ... indica que esta función es variadic por lo que puede ser invocada con cero o mas argumentos para es parámetro.

¿Cuál sería el tipo de a?

El tipo de a no es cualquier tipo sino interface{}, al pasar un valor como argumento go lo convertirá (si es necesario) al tipo interface{}.

Un interface^ se construye con dos palabras de dirección de memoria:

  • Una se usa para apuntar la tabla de metodos asociada al tipo “original”.
  • La otra para apuntar a los datos.

Entender representación evita bastantes confusiones.

Ahora supongamos que queremos construir un slice de tipos Vehiculo:

vehiculos := []Vehiculo{&Bici{}, &Moto{}}

Podemos añadirle directamente los tipos que lo satisfacen porque la conversión al tipo Vehiculo se lleva a cabo directamente.

En el slice vehiculos todos sus elementos son tipo Vehiculo pero, cada uno de sus valores tienen diferentes tipos de origen.

Si definimos Vehiculo como un interface vacio:

type Vehiculo interface{}

Podremos realizar la asignación, porque tanto Bicicomo Moto sadisfacen el interface vacio:

vehiculos := []Vehiculo{&Bici{}, &Moto{}}

Pero no tendremos acceso a ninguno de sus métodos.

  1. ¿Cuándo usar un pointer receiver y otro estándar? Un receiver puede considerarse como un argumente que se pasa a un método.

    • Si queremos modificar el receiver usaremos un pointer y le pasamos la referencia.
    • Si la estructura atachada es muy grande por lo que pasarla por valor es caro.
    • Por consistencia, si la mayor parte de los receiver ya son tipo pointer.

awk, power for your command line

A beginner's guide to the *nix swiss army knife


Versión en español aquí.


:warning: Disclaimer :warning:

This text refers only to awk’s GNU implementation known as gawk witch is the most used one and comes with any modern Linux / Unix distribution.

The GNU Awk User’s Guide is its reference, for the examples I used real world cases taken mainly from my Stackoverflow answers.

One more thing … English is not my first language so bear with me please :pray:.


AWK is a language similar to PERL, only considerably more elegant.

  • Arnold Robbins

What ??

AWK is a programming language designed for text processing and typically used as a data extraction and reporting tool. It is a standard feature of most Unix-like operating systems.

awkaward name …

its name is derived from the surnames of its authors – Alfred Aho, Peter Weinberger, and Brian Kernighan.

So awk

  • Searchs for lines that contain certain patterns in files or the standard input.

  • Mostly used for data extraction and reporting like summarizing information from the output of other utility programs.

  • C-like syntax.

  • Data Driven: it’s describe the data you want to work with and then what action to do when you find it.

pattern { action }
pattern { action }

The Basics

How to Run it

If the program is short:

awk 'program' input-file1 input-file2

Note: Beware of shell quoting issues1.

cmd | awk 'program'

Note: The pipe redirects the output of the left-hand command (cmd) to the input of the awk command2.

When the code is long, it is usually more convenient to put it in a file and run it with a command like this:

awk -f program-file input-file1 input-file2

cmd | awk -f program-file

Or just make it executable like a shebang:

#!/bin/awk -f

BEGIN { print "hello world!!" }

Other useful flags

-F fs Set the FS variable to fs.

-v var=val Set the variable var to the value val before execution of the program begins.

:point_right: Note: it can be used more than once, setting another variable each time.

BEGIN and END

These special patterns or blocks supply startup and cleanup actions for awk programs.

BEGIN{
    // initialize variables
}
{
    /pattern/ { action }
}
END{
    // cleanup
}

:warning: WARNING: Both rules are executed once only, BEGIN before the first input record is read, END after all the input is consumed.

$ echo "hello"| awk 'BEGIN{print "BEGIN";f=1}
                    {print $f}
                    END{print "END"}'
BEGIN
hello
END

Why grepping if you have awk ??

$ cat lorem_ipsum.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_ipsum.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
$ awk '/dolor/' lorem_ipsum.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

:point_right: Note: If the action is not given the default action is to print the record that matches the given pattern.

But… how can we find out the first and last word of each line?

Of course grep can, but needs two steps:

$ grep -Eo '^[^ ]+' lorem_ipsum.dat 
Lorem
Maecenas
Nunc
Curabitur
Lorem
Aliquam
$ grep -Eo '[^ ]+$' lorem_ipsum.dat 
elit.
condimentum.
ex.
tellus.
elit.
ultrices.

Let's see awk in action here:

$ awk '{print $1,$NF}' lorem_ipsum.dat 
Lorem elit.
Maecenas condimentum.
Nunc ex.
Curabitur tellus.
Lorem elit.
Aliquam ultrices.

Isn’t this better :sunglasses:? Yeah, but… HTF does this works?

awk divides the input for your program into Records and Fields.

Records

Records are separated by a character called the record separator RS. By default, the record separator is the unix newline character \n.

This is why records are, by default, single lines.

Additionally awk has ORS Output Record Separator to control the way records are presented to the stdout.

RS and ORS should be enclosed in quotation marks, which indicate a string constant.

To use a different character or a regex simply assign it to the RS or / and ORS variables:

  • Often, the right time to do this is at the beginning of execution BEGIN, before any input is processed, so that the very first record is read with the proper separator.
  • Another way to change the record separator is on the command line, using the variable-assignment feature.

Examples:

$ awk 'BEGIN{RS=" *, *";ORS="<<<---\n"}
       {print $0}' lorem_ipsum.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_ipsum.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.
<<<---

Fields

awk records are automatically parsed or separated into chunks called fields.

By default, fields are separated by whitespace (any string of one or more spaces, TABs, or newlines), like words in a line.

To refer to a field in an awk program, you use a dollar $ sign followed by the number of the field you want.

Thus, $1 refers to the first field, $2 to the second, and so on.

:point_right: IMPORTANT: $0 represents the whole input record.

$ awk '{print $3}' lorem_ipsum.dat 
dolor
erat
orci,
dapibus
dolor
mauris

NF is a predefined variable it's value is the number of fields in the current record. So, $NF will be always the last field of the record.

$ awk '{print NF}' lorem_ipsum.dat 
8
7
10
4
8
10

FS holds the valued of the field separator, this value is a single-character string or a regex that matches the separations between fields in an input record.

The default value is " ", a string consisting of a single space. As a special exception, this value means that any sequence of spaces, TABs, and/or newlines is a single separator.

In the same fashion that ORS we have a OFS variable to manage how our fields are going to be send to the 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

Note: Ummm … $1=$1 ????3


Keeping records and fields in mind, were now ready to understand our previous code:

$ awk '{print $1,$NF}' lorem_ipsum.dat 
Lorem elit.
Maecenas condimentum.
Nunc ex.
Curabitur tellus.
Lorem elit.
Aliquam ultrices.

NR and FNR

These are two useful built-in variables:

NR : number of input records awk has processed since the beginning of the program’s execution.

FNR : current record number in the current file, awk resets FNR to zero each time it starts a new input file.

$ 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

Fancier Printing

The format string is very similar to that in the ISO C.

Syntax:

printf format, item1, item2, …

$ awk '{printf "%20s <-> %s\n",$1,$NF}' lorem_ipsum.dat 
               Lorem <-> elit.
            Maecenas <-> condimentum.
                Nunc <-> ex.
           Curabitur <-> tellus.
               Lorem <-> elit.
             Aliquam <-> ultrices.

Redirecting Output

Output from print and printf is directed to the standard output by default but we can use redirection to change the destination.

Redirections in awk are written just like redirections in shell commands, except that they are written inside the awk program.

$ awk 'BEGIN{print "hello">"hello.dat"}'
$ awk 'BEGIN{print "world!">>"hello.dat"}'
$ cat hello.dat 
hello
world!

It is also possible to send output to another program through a PIPE:

$ awk 'BEGIN{sh="/bin/sh";print "date"|sh;close(sh)}'
dom nov 13 18:36:25 CET 2016

The streams can be pointed to the stdin, the stdout and the stderr.

For example, we can write an error message to the stderr like this:

$ awk 'BEGIN{print "Serious error detected!" > "/dev/stderr"}'
Serious error detected!

Working with arrays

In awk the arrays are associative, each one is a collection of pairs, indexvalue, where the any number or string can be an index.

No declaration is needed; new pairs can be added at any time.

Index Value
“perro” “dog”
“gato” “cat”
“uno” “one”
1 “one”
2 “two”

To refer an array:

array[index-expression]

To assign values:

array[index-expression] = value

To check if a key is indexed:

indx in array

To iterate it:

for (var in array) {
    var, array[var]
    }

Using numeric values as indexes and preserving the order:

for (i = 1; i <= max_index; i++) {
    print array[i]
    }

A complete example:

$ 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

Gives you:

Yes we have uno in dict!
No , cinco is not in dict!
uno -> one
dos -> two
tres -> three
cuatro -> four

gawk does not sort arrays by default:

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

But you can take advantage of PROCINFO for sorting:

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

Build-in functions

gensub(regexp, replacement, how [, target]) : Is the most advanced function for string replacing.

And their simpler alternatives:

gsub(regexp, replacement [, target])

sub(regexp, replacement [, target])

Having this 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.

We're going to swap the position of the words placed at the left and the right of each comma.

$ 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.

Using gensub we capture three groups and then we swap the order.

To illustrate a simpler action let's change dots for commas:

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,

Using gsub alternative:

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,

This option seems better when no group capture is needed.

Other interesting functions are index and substr.

index(in, find)

substr(string, start [, length ])

Works like this:

$ awk 'BEGIN{t="hello-world";print index(t, "-")}'
6
$ awk 'BEGIN{t="hello-world";print substr(t,index(t, "-")+1)}'
world

The split function is used to create an array from a string dividing it by a separator char, it returns the number of elements of the created array.

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

:point_right: Note: This could be done in a much more simpler way:

$ awk '{print NF,$NF}' FS=':' passwd
7 /bin/rbash
7 /bin/rbash
7 /bin/bash
7 /bin/rbash
7 /bin/rbash

Custom functions

Write a custom function is quite simple:

awk 'function test(m)
     {
        printf "This is a test func, parameter: %s\n", m
     }
     BEGIN{test("param")}'

Give us:

This is a test func, parameter: param

We can also give back an expression using a return statement:

awk 'function test(m)
     {
        return sprintf("This is a test func, parameter: %s", m)
     }
     BEGIN{print test("param")}'

Parsing by parameter is the only way to make a local variable inside a function.

Scalar values are passed by value and arrays by reference, so any change made to an array inside a function will be reflected in the global scope:

 awk 'function test(m)
      {
       m[0] = "new"
      }
      BEGIN{m[0]=1
            test(m)
            exit
      }
      END{print m[0]}'

Outputs:

new

Now let's have some fun :godmode:

Our challenges:

01. Penultimate word of a Record.

02. Replacing a Record.

03. Place a semicolon at the end of each Record.

04. Place a comma between every word.

05. All together?

06. Redirecting odd records to a file and even ones to another.

07. Given a password file get the missing field.

08. Field swapping.

09. Traceroute hacking.

10. Where are my children?

11. Data aggregation.

12. Records between two patterns.

13. Field transformation.

14. Records to columns.

15. FASTA File processing.

16. Complex reporting.

17. Files joiner.

18. Passwd and Group.

19. User connections.

20. Uptime total load average.


01. Penultimate word of a Record

Having this 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

Not too much to explain here, NF stores the number of fields in the current record, so NF-1 points to field before last and $(NF-1) will be its value.


02. Replacing a record

Our task, file record substitution, the third line must become:

This not latin

Nothing more simple, just play around NR (number of record).

Code:

$ awk 'NR==3{print "This is not latin";next}{print}' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
This is not latin
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.

Alternative solution to avoid next statement: assign the new line to the complete record $0.

Example:

$ awk 'NR==3{$0="This is not latin"}1' lorem.dat
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas pellentesque erat vel tortor consectetur condimentum.
This is not latin
Curabitur a dapibus tellus.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.

03. Place a semicolon at the end of each record

$ 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.;

As the default RS is the unix break line \n we just need to prefix the semicolon to the Output Record Separator OFS.

:warning: ATENCION: What about that strange 1?4


04. Place a comma between every word

$ 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.

The most significant part of this code is how it forces a record reconstruction with $1=$1 for the current value of the OFS.


05. All together?

$ 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.;

As simply as playing with output vars: OFS and ORS.


06. Redirecting odd records to a file and even ones to another

Let's start with the final solution:

$ awk 'NR%2{print > "even.dat";next}
           {print > "odd.dat"}' lorem.dat
$ cat even.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
$ cat odd.dat
Maecenas pellentesque erat vel tortor consectetur condimentum.
Curabitur a dapibus tellus.
Aliquam interdum mauris volutpat nisl placerat, et facilisis.

The modulo function (%) finds the remainder after division for the current Record Number NR divided by two:

$ awk '{print NR%2}' lorem.dat
1
0
1
0
1
0

As far as we now yet, in awk 1 is True and 0 False. We redirect our output evaluating this fact.

next requires an special attention, it forces awk to immediately stop the current record process and pass to next one.

In this way we elude a double condition that would look like this:

awk  'NR % 2{print > "even.dat"}
     !NR % 2{print > "odd.dat"}' lorem.dat

07. Given a password file get the missing field

$ 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

Let's assume the home directory by prefixing the fixed string "/home/" to the username:

$ 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

Our first step should be considering the field separator, a colon, for input as well as for output.

Then we need to find the void field position, 6 for this example.

Finally, we compose the required value using the given string and the user login stored in the first field.

:warning: IMPORTANT: print is not needed because $6 assignation return value will always be True and awk default action is to print the affected record.


08. Field swapping

Our goal: last field should become first and first become last.

Final code:

$ 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

We are playing with an intermediate variable used to store the first field value, the we swap its value with the last one, finally we assign last variable to $NF ($NF=last).


09. Traceroute hacking

Having this output:

$  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

We need to compute the package travelling total time.

$ traceroute -q 1 google.com 2>/dev/null|\
  awk '{total+=$(NF-1)}
       END{print "Total ms: "total}'
Total ms: 153.424

Because no condition is specified, the action is executed for all records.

total+=$(NF-1): total variable is used to accumulate the value of each Record penultimate Field $(NF-1).

Finally, we use the END rule to show the final total value.


10. Where are my children?

Our job: get our shell dependent processes.

$ echo $$
51026

First thing: Launch the background processes.

$ sleep 10 & sleep 15 & sleep 20 &
[1] 86751
[2] 86752
[3] 86753

Using ps utility, awk will look for the third field known as the PPID.

:point_right: Note: We're using -v to set ppid var before execution of the program begins.

$ ps -ef|awk -v ppid=$$ '$3==ppid'
  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

We just need the sleeps:

$ ps -ef|awk -v ppid=$$ '$3 == ppid && /slee[p]/ 
                         {print $2" -> "$5}'
86751 -> 7:57PM
86752 -> 7:57PM
86753 -> 7:57PM

The solution needs a new condition to add: find the sleep pattern in our current record /slee[p]/.

The triggered action will be to print the second field $2 with stands for the PID and the fifth $5, the time stamp.


11. Data aggregation

Having this file:

$ 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

Our task is to compute how many bytes per IP are processed.

$ 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

Bunch of things here to explain.

NR>1{ips[$1]+=$2}: The action ips[$1]+=$2 is only executed when the current record number is greater than one NR>1. This is needed to avoid the header.

ips is an array indexed by the ip value (the $1 field), for each key we are going to accumulate in the value of the second field.

Take notice of an important fact, if a key is not present in the array, awk adds a new element to the structure, otherwise is going to update the previous value pointed by that key (as in our example).

The END rule is just used to iterate the array by indexes and values.

This code could be rewritten in complete different manner to avoid the use of arrays and preserve the order taken advantage of the sorted IPs file:

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}: Bypass the header.

lip && lip != $1{print lip,sum;sum=0}: Here we use a var named lip (last-ip). lip && lip != $1 When lip is not null and it's value not equal to the first field (that holds the current ip) the triggered action will be to print lip and sum the total amount of bytes for the last IP. Then we initialize it sum=0.

The trick is clear, every time IP ($1) changes we show the stats of the previous one.

{sum+=$2;lip=$1}: To update the bytes counter sum+=$2 and assign the current IP to lip: lip=$1. It is the last step of our record processing.

The END block is used to print the pending values.

This code preserves the order, but in my opinion, this comes at the expense of significantly increased complexity.


12. Records between two patterns

Our task is two extract the lines between and OUTPUT and 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

This is a classic example used to illustrate how pattern matching works in awk and its associate actions which I dedicated a complete post.

$ awk '/END/{flag=0}flag;/OUTPUT/{flag=1}' pat.dat
top 2
bottom 1
left 0
right 0
page 66

Its based in the flag variable value, it will be True (1) when the starting pattern OUTPUT is found and False (0) when END tag is reached.

To avoid an additional step, the action order is very important, if we follow the logic sequence:

$ awk '/OUTPUT/{flag=1}flag;/END/{flag=0}' pat.dat
OUTPUT
top 2
bottom 1
left 0
right 0
page 66
END

Pattern tags are shown through the output.

The reason: after OUTPUT pattern is found the flag gets activated, as the next action depends of this flag the record is printed.

We can avoid this behavior placing the flag activation as the last step of the flow.


13. Field transformation

Let's suppose this file:

$ 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

Our job will be to calculate our records total weight in mega bytes:

$ awk '{total+= $1 / ($2=="kb" ? 1024: 1)}
       END{print total}'  space.dat
6.61365

To understand how it works one concept must be clear, the ternary operator (subject of an old post).

total will be used to accumulate the divison of the first field $1 by the second $2 that will hold the value given by the ternary operator: 1024 when $2 is equal to kb and 1 if no transformation needed.

Finally, we print total value in the END block.


14. Records to columns

Original source:

$ cat group.dat
string1
string2
string3
string4
string5
string6
string8

Our mission is to group records in blocks of three columns like this:

string1 string2 string3
string4 string5 string6
string8

It may seem complex, but becomes much simpler If we understand how to use the Output Field Separator OFS:

$ awk 'ORS = NR%3 ? FS : RS; END{print "\n"}' group.dat
string1 string2 string3
string4 string5 string6
string8

If we set the ORS to a blank character, the FS default value, all the output will become a single line:

$ awk 'ORS=FS; END{print "\n"}' group.dat
string1 string2 string3 string4 string5 string6 string7

ORS = NR%3 ? FS : RS: Finally we use the ternary operator (explained just before) to evaluate the modulo NR%3 result of the division of the current field number NR by three.

If the remainder is True the ORS becomes FS, a blank space, otherwise the RS default value will be assigned, the Unix line break \n.


15. FASTA File processing

In bioinformatics, FASTA is a text-based file format.

Having the following example:

$ cat fasta.dat
>header1
CGCTCTCTCCATCTCTCTACCCTCTCCCTCTCTCTCGGATAGCTAGCTCTTCTTCCTCCT
TCCTCCGTTTGGATCAGACGAGAGGGTATGTAGTGGTGCACCACGAGTTGGTGAAGC
>header2
GGT
>header3
TTATGAT

We need the total length of each sequence, and a final resume.

Should look like this:

>header1
117
>header2
3
>header3
7
3 sequences, total length 127

awk is the perfect tool for this reporting effort, for this example we will use:

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

The first action is tied to the header detection /^>/, that's because all headers stars with > character.

When seqlen is not null its value, that holds the previous sequence length, is printed to the stdout attached to the new header. seqtotal is updated and seqlen initialized to serve the next sequence. Finally, we break further record processing with next.

The second action {seqlen += length($0)} is used to update seqlen summing the total record length.

The END rule purpose is to show the unprinted sequence and the totals.

Trick here is to print the previous sequence length when we found a new header.

When we process the first record seqlen has no value so we skip the visualization.


16. Complex reporting

Source:

$ 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

Our duty: create a report to visualize snaps and instance but only when snap first counter tag is greater than zero.

Expected output:

snaps1: Counter: 4966
Instance: s.1.aps.userDatabase.mount275668.attributes
snaps1: Counter: 5660
Instance: s.1.aps.userDatabase.mount275000.attributes

We are playing again around patterns and flags:

awk '{$1=$1}
     /snaps1/ && $NF>0{print;f=1}
     f &&  /Instance/ {print;f=0}'  report.dat

For every record the first action is executed, it forces awk to rebuild the entire record, using the current values for OFS 3.

This trick allows us to convert a multiple space separator to a single char, the default value for the Output Field Separator.

Let's see this:

$ awk '1' text.dat
one      two
three              four
$ awk '$1=$1' text.dat
one two
three four

Second action is triggered when the pattern is found and the last field greater than zero /snaps1/ && $NF>0.

awk prints the record and assign a True value to the flag print;f=1.

Last step: when flag is True and instance pattern in the line f && /Instance/, show the line and deactivate flag: print;f=0.


17. Files joiner

Let's suppose two archives:

$ cat join1.dat
3.5 22
5. 23
4.2 42
4.5 44
$ cat join2.dat
3.5
3.7
5.
6.5

We need the records from the first one join1.dat when the first fields are in the second one join2.dat.

Output should be:

3.5 22
5. 23

We can use unix join utility, of course, but we need to sort the first file:

$ join <(sort join1.dat) join2.dat
3.5 22
5. 23

Not needed in awk:

$ awk 'NR == FNR{a[$1];next}
       $1 in a'  join2.dat join1.dat

Let's study the filters and the actions:

NR == FNR: Record Number equal to Record File Number means that we're processing the first file parsed to awk: join2.dat.

The pair action a[$1];next will be to add a new void value to the array indexed by the first field. next statement will break the record processing and pass the flow to the next one.

For the second action NR != FNR is applied implicitly and affects only to join1.dat, the second condition is $1 in a that will be True when the first field of join1.dat is an array key.


18. Passwd and Group

These are to unix classics:

$ 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

Our goal, a report like this:

d001:dba
ag002:netadmin
jp003:dba
ms004:netadmin
rc005:netadmin

We need a multiple file flow as we studied in our last example:

$ awk -F\: 'NR == FNR{g[$3]=$1;next}
            $4 in g{print $1""FS""g[$4]}' /etc/group /etc/passwd

To process /etc/group we repeat the NR == FNR comparison then store the name of the group $1 indexed by its ID $3: g[$3]=$1. Finally, we break further record processing with next.

The second condition will target only /etc/passwd records, when the fourth field $4 (group ID) is present in the array $4 in g, we will print the login and the value pointed by the array indexed by the group id g[$4], so: print $1""FS""g[$4].


19. User connections

Users utility output example:

$ users
negan rick bart klashxx klashxx ironman ironman ironman

We're going to count logons per user.

$ users|awk '{a[$1]++}
             END{for (i in a){print i,a[i]}}' RS=' +'
rick 1
bart 1
ironman 3
negan 1
klashxx 2

The action is performed for all the records.

a[$1]++: This is the counter, for each user $1 it increments the pointed value (uninitialized vars have the numeric value zero).

In the END block iterate the array by key and the stored value to present the results.


20. Uptime total load average

A typical output:

$ uptime
 11:08:51 up 121 days, 13:09, 10 users,  load average: 9.12, 14.85, 20.84

How can we get the total load average mean?

$ uptime |awk '{printf "Load average mean: %0.2f\n", 
                ($(NF-2)+$(NF-1)+$(NF))/3 }' FS='(:|,) +'
Load average mean: 14.94

Here's a new technique.

We’re using a regex as the field separator (:|,) +, so the FS can be a colon and a comma followed by zero or more blank spaces.

We just need the last three fields to perform the arithmetic required, then we use printf attached to a proper mask.


:warning: Disclaimer 2 :warning:

If you are still here, THANKS!!

From my point of view, awk is an underrated language and needs much love :heart:.

If you’re hungry for more let me know it in the comment section bellow and I will consider a second part to finish my mission … bore you to death :neckbeard:.

Happy coding!

  1. A Guide to Unix Shell Quoting

  2. Wikipedia on pipelines

  3. There are times when it is convenient to force awk to rebuild the entire record, using the current values of the FS and OFS.

    To do this, we use the seemingly innocuous assignment: $1 = $1  2

  4. Quick answer, It's just a shortcut to avoid using the print statement.

    In awk when a condition gets matched the default action is to print the input line.

    $ echo "test" |awk '1'

    Is equivalent to:

    echo "test"|awk '1==1'

    echo "test"|awk '{if (1==1){print}}'

    That's because 1 will be always true

awk, power para tu command line

Guía de uso de la navaja suiza de los *nix para principiantes


English version here.


:warning: Disclaimer :warning:

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.

awkaward 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

:warning: 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 awk2.

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.

:point_right: 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.

:warning: 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 :sunglasses:? , 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 el ORS) 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

:warning: Nota: ¿¿Y ese?? … $1=$13


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

:point_right: 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 :godmode:

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.


:astonished: ¡Markdown no permite acentos en los anchors! :astonished:


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

:warning: 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 ips 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.


:warning: Disclaimer 2 :warning:

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 :heart:.

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!


  1. A Guide to Unix Shell Quoting

  2. Wikipedia sobre pipelines

  3. En ocasiones es conveniente forzar que awk reconstruya el registro completo, usando los valores actuales para el FS y OFS.

    Para hacerlo usamos esta asignación inocua: $1 = $1 

  4. 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}}'

    El motivo es que 1 siempre va a ser verdadero

© Juan Diego Godoy Robles