El pipe (|) conecta el stdout de un proceso directamente con el stdin del siguiente, sin tocar disco. Lo que un comando imprime, el siguiente lo lee como si viniera del teclado. Esa conexión la gestiona el kernel mediante un buffer circular —en Linux, 64 KiB por defecto— que actúa como intermediario: el proceso productor escribe en él, el proceso consumidor lee de él, y el kernel bloquea a cualquiera de los dos si el buffer está lleno o vacío respectivamente.
Lo importante que mucha gente no tiene claro: los procesos de un pipeline se ejecutan en paralelo, no en secuencia. Cuando escribes ls /etc | grep ".conf" | sort | head -10, el kernel arranca los cuatro procesos casi simultáneamente. grep no espera a que ls termine; empieza a procesar en cuanto ls escribe los primeros bytes en el buffer. Esto tiene implicaciones reales de rendimiento cuando trabajas con streams grandes.
Úsalo siempre que necesites transformar o filtrar datos en tránsito sin necesidad de ficheros temporales. La alternativa —redirigir a un fichero, procesarlo, borrarlo— es más lenta, consume inodos, y añade puntos de fallo. Donde el pipe falla es cuando necesitas la salida completa de un paso antes de poder empezar el siguiente (por ejemplo, sort sobre un stream infinito), o cuando el comando de destino no lee de stdin.
Si te equivocas con el orden o asumes que los procesos son secuenciales, puedes leer datos parciales, perder líneas por race conditions sutiles, o —el problema clásico— que un pipeline falle silenciosamente porque bash, por defecto, reporta el exit code solo del último comando. Un grep que no encuentra nada en medio del pipeline devuelve exit code 1, pero si head termina bien, el pipeline completo reporta 0. Eso rompe cualquier script que dependa del exit code para detectar errores.
#!/bin/bash
# Activar pipefail ANTES de cualquier pipeline crítico.
# Sin esto, bash usa solo el exit code del último proceso.
set -o pipefail
LOGDIR="/var/log/nginx"
REPORT="/tmp/errores_nginx.txt"
# Pipeline principal: filtrar errores de las últimas 24h,
# extraer las IPs, contar ocurrencias y quedarse con el top 5.
# tee captura la salida intermedia SIN romper el flujo hacia sort.
grep "error" "$LOGDIR/access.log" \
| awk '{print $1}' \
| tee >(sort | uniq -c | sort -rn | head -5 > "$REPORT") \
| wc -l
echo "IPs únicas con errores guardadas en: $REPORT"
echo "Exit codes del pipeline: ${PIPESTATUS[*]}"
# Limpiar ficheros temporales con nombres que pueden tener espacios.
# find imprime separadores nulos (-print0), xargs los lee con -0.
# Sin -print0 / -0, un fichero llamado "mi reporte.tmp" se rompe en dos argumentos.
find /tmp -name "*.tmp" -mtime +1 -print0 | xargs -0 rm -f
set -o pipefail cambia cómo bash evalúa el exit code del pipeline entero: en lugar de tomar solo el del último proceso, toma el del último proceso que falló (el más a la derecha entre los que devolvieron non-zero). Ponlo al principio de cualquier script donde uses pipelines en condiciones if o con set -e.
El bloque tee >(...) usa process substitution para bifurcar el stream sin interrumpirlo. tee recibe las líneas de awk, las envía simultáneamente al subshell entre >(...) —que genera el reporte— y a su propio stdout, que sigue fluyendo hacia wc -l. Sin tee, tendrías que elegir entre guardar el reporte o contar las líneas; con él, haces las dos cosas en el mismo paso.
${PIPESTATUS[*]} es un array de bash que guarda el exit code de cada proceso del último pipeline ejecutado, en orden. En el ejemplo, imprimiría algo como 0 0 0 0, uno por cada comando: grep, awk, tee y wc. Si grep no encuentra nada en el log, verás 1 0 0 0, y gracias a pipefail, el pipeline completo habrá devuelto 1.
La combinación -print0 en find y -0 en xargs es el patrón correcto para nombres de fichero arbitrarios. find sustituye los saltos de línea por bytes nulos (\0) como separador, y xargs -0 los interpreta correctamente. Si usas el pipe simple sin estos flags y tienes ficheros con espacios o saltos de línea en el nombre —perfectamente válido en ext4—, xargs los parte mal y borras ficheros equivocados o fallas silenciosamente.
Un detalle de xargs que vale la pena tener presente: por defecto construye la línea de comandos más larga posible para minimizar el número de invocaciones de rm. Puedes controlarlo con -n (máximo de argumentos por invocación) o -P (invocaciones en paralelo), lo que resulta útil cuando el comando destino es costoso o tiene límites propios.
N° 42