Ayer estaba revisando un script desarrollado por un compañero y me llamó la atención la manera en la que solucionaba un problema “habitual”: ¿cómo añadir una línea a un fichero sólo si no está ya presente?

La solución de mi compañero es muy ad hoc para el tipo de fichero en el que debe añadir la línea… Sin embargo, me hizo pensar en que hace tiempo había “leído por ahí” una solución mucho más genérica.

Ese “por ahí”, era en Stack Overflow, en esta pregunta Appending a line to a file only if it does not already exist (¡de hace 12 años!).

La solución es sencilla conceptualmente, a la par que elegante (nada de magia negra con expresiones regulares o algo por el estilo).

El concepto

grep busca una cadena en un fichero. Así que es la herramienta perfecta para la primera parte del problema: averiguar si una determinada línea está presente en el fichero objetivo (al que voy a llamar config.yaml).

Para ejemplificar la solución, usaré el fichero:

---
config:
  name: myapp
  path: /path/to/some/myapp.conf

Por defecto, grep devuelve (todo el contenido) de la línea en la que se encuentra coincidencias. Es decir, si buscamos path, obtenemos:

$ grep path config.yaml 
  path: /path/to/some/myapp.conf

Pero si buscamos myapp:

$ grep myapp config.yaml
  name: myapp
  path: /path/to/some/myapp.conf

En este segundo caso tenemos varias concidencias, lo que es un problema si queremos actualizar, por ejemplo, el path a la configuración de la aplicación.

Por suerte, una de las opciones de grep es -x (o en versión larga, --line-regexp), que selecciona sólo aquellas coincidencias de la línea completa. Esto es justo lo que queremos.

Por tanto, ahora podemos seleccionar sólo la línea que contiene la ruta al fichero de configuración mediante:

$ grep --line-regexp '  path: /path/to/some/myapp.conf' config.yaml 
  path: /path/to/some/myapp.conf

Observa como es necesario incluir los espacios al principio de la línea para que el match funcione.

Como vemos, en este caso usamos como patrón una cadena (no es una expresión regular); podemos indicárselo a grep mediante la opción -F (o en versión larga, --fixed-strings).

Finalmente, no es necesario que se muestre la coincidencia, así que añadimos -q (o --quiet).

Hasta ahora hemos “configurado” el comando grep para que nos indique si la línea (completa) se encuentra en el fichero.

A continuación, vemos cómo añadirla si no está presente.

echo al rescate

La manera más sencilla de añadir una línea a un fichero es mediante el humilde echo:

echo '  path: /path/to/some/myapp.conf' >> config.yaml

El problema es que de esta forma, cada vez que ejecutemos el script, echo añade la línea al fichero.

¿Cómo lo combinamos con el comando grep anterior?

En Bash, cuando un comando tiene éxito devuelve 0; si falla, devuelve 1 (o cualquier otro código numérico, hasta 255).

En Bash, el 0 también se interpreta como el valor “lógico” true, y por tanto, 1 (o cualquier otro valor), se considera false.

El operador OR (representado en Bash por ||), es true si uno (o los dos) operandos son true.

Usamos esta propiedad de manera para conseguir el resultado que queremos:

grep --quiet --fixed-strings --line-regexp '  path: /path/to/some/myapp.conf' config.yaml || echo '  path: /path/to/some/myapp.conf' >> config.yaml

Si grep devuelve 0 (todo Ok), significa que se ha encontrado la línea en el fichero. Como el operador || (or) ya tiene un operando con valor true, se evalúa como true (sin necesidad de evaluar el valor del segundo operando).

Si el primero operando devuelve 1 (false), significa que grep no ha encontrado la línea completa. En este caso, Bash debe evaluar el segundo operando (el echo) para determinar el resultado de operar los dos lados del OR.

echo imprime la cadena a stdout, pero mediante >> se redirige al fichero (al que se añade (append)). El resultado es (a no ser que pase algo raro) exitoso, por lo que echo devuelve 0, que se considera true por el operador ||, y el resultado es true.

Es decir, que el echo que añade la línea que queremos al fichero sólo se añade si no está previamente en el fichero.

Y eso, precisamente, lo que queríamos.

¡Aplausos! (como suele cerrar su sección Rodrigo Cortés en el podcast Aquí hay dragones)