Fork me on GitHub

klashxx    Archive    About    Talks    Feed

F-strings: A format system to rule them all

License: CC BY-NC-SA 4.0

El universo Python es fantástico, está en continua expansión y su naturaleza abierta hace que se nutra de las mejores ideas con independencia de si estaban ya implementadas en otros lenguajes.

Precisamente de una de estas grandes genialidades nos ocupa: las strings interpolation traducido a Python como F-strings. Un componente core presente a partir de la versión 3.6, tan simple como potente y efectivo, sin duda, otro argumento para convencer a los rezagados de Python 2.

Comentaremos la PEP-0498, enumeraremos las múltiples ventajas que nos aportan e intentaremos discernir las posibles pitfalls ¿nos pueden meter en algún lio?, ¿merece la pena migrar todo nuestro software? ¿Nuestros programas pueden verse afectados en cuando a su rendimiento?

Finalmente nos ensuciaremos las manos y pondremos ejemplos (algún jupyter notebook caerá) de los diferentes casos de uso comparándolos con las alternativas previas. ¡Acérquense! ¡Dejen que su código disfrute de una claridad sin precedentes!.

Simple is better than complex (SIEMPRE).

Texto base de la charla F-strings: A format system to rule them all comentada en Alicante durante la PyConES 2019.

Para acceder al Jupyter Notebook base de la presentación:

Binder

En el repo podéis encontrar las instrucciones para su instalación en local.

Django vs Sysadmin

License: CC BY-NC-SA 4.0

Prefacio

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 versó la charla Django vs Sysadmin impartida 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 06, 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

© Juan Diego Godoy Robles