Módulo 3: Tuberías, redirección y composición
5. Tuberías: conectar la salida de un comando con la entrada de otro
Descripción
Al terminar esta lección vas a poder construir tuberías de varios comandos —dos, tres, cuatro eslabones— para responder preguntas sobre archivos que nunca podrías revisar a mano: cuántas líneas de error hay en un log de decenas de miles de renglones, cuántos archivos de cierto tipo existen en todo el sistema, cuáles de los miles de resultados de una búsqueda valen la pena mirar de cerca. Vas a construirlas con un método concreto —por partes, verificando cada eslabón— y vas a saber qué hacer cuando el siguiente comando de la cadena se niega a leer lo que le mandas.
Esto es exactamente lo que hace un desarrollador o un administrador de sistemas cuando un servidor falla a las tres de la mañana: nadie abre un archivo de log de dos millones de líneas en un editor de texto a buscar a ojo. Se conectan tres o cuatro comandos pequeños, cada uno haciendo una sola cosa bien, hasta que la pregunta queda reducida a una línea de respuesta. Eso es literalmente la filosofía Unix de la que hablaste en la segunda lección de este módulo, puesta en práctica con un solo símbolo.
Conexión con el módulo: en la lección anterior conectaste la salida de un comando con un
archivo —>, >>, tee—. Ahora vas a conectar la salida de un comando directamente con la
entrada de otro comando, sin que nada toque el disco en el medio. Es la misma idea de
"mover un flujo a otro destino" que ya conoces; el destino, esta vez, es un proceso vivo en
vez de un archivo quieto.
El operador |: una cadena de montaje, no un depósito intermedio
Piensa en una cadena de montaje de fábrica. Una máquina corta la tela, la siguiente la cose, la siguiente le pone botones. Ninguna pieza a medio hacer se guarda en un depósito entre máquina y máquina —pasa directo de una banda transportadora a la siguiente, en el momento en que sale de una y antes de que la otra la necesite—. Si quisieras guardar cada pieza a medio terminar en una caja, etiquetarla, y que el siguiente obrero fuera a buscarla a la caja, el proceso sería el mismo al final, pero mucho más lento y con un paso extra que nadie pidió.
El operador | (la barra vertical, arriba de la tecla Enter en la mayoría de los teclados)
hace exactamente eso con comandos: toma la salida estándar (descriptor 1) del comando de la
izquierda y la conecta directamente a la entrada estándar (descriptor 0) del comando de la
derecha. Los dos comandos arrancan casi al mismo tiempo, como procesos independientes, y el
sistema operativo va empujando el texto de uno al otro a medida que se produce —sin escribir
nada en disco, sin un archivo temporal que después tengas que borrar—.
comando1 | comando2
Compáralo con la alternativa sin tubería, usando lo que ya sabes de la lección anterior:
comando1 > temp.txt
comando2 < temp.txt
rm temp.txt
Tres líneas, un archivo intermedio que existe solo para ser borrado un segundo después, y un
nombre (temp.txt) que tienes que inventar y recordar limpiar. La tubería reemplaza las tres
líneas por una sola, y no deja ningún archivo suelto atrás.
Ejemplo trabajado
Tienes el mismo access.log que usaste en la lección de grep del módulo anterior:
INFO 2026-07-18 09:12:03 User 42 logged in
ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
WARN 2026-07-18 09:15:02 Retry scheduled for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188
INFO 2026-07-18 09:20:44 User 17 logged in
ERROR 2026-07-18 09:22:19 Database connection refused
La pregunta de hoy es más específica que "muéstrame los errores": quieres saber cuántos de esos errores son, puntualmente, del gateway de pagos. Construyes la tubería de a un eslabón, verificando cada uno antes de agregar el siguiente.
Paso 1 — el primer comando, solo, verificado.
grep ERROR access.log
Qué esperar:
ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188
ERROR 2026-07-18 09:22:19 Database connection refused
Tres líneas. Revisas que sean las que esperabas —sí, son los tres errores— antes de seguir.
Paso 2 — agregas el segundo eslabón y vuelves a verificar.
grep ERROR access.log | grep Payment
Qué esperar:
ERROR 2026-07-18 09:14:51 Payment gateway timeout for order 1188
ERROR 2026-07-18 09:15:10 Payment gateway timeout for order 1188
Las tres líneas de grep ERROR entraron, una por una, a la entrada estándar del segundo
grep, que se quedó solo con las que además contienen "Payment". Bajaste de tres a dos.
Exactamente lo que buscabas: la línea de "Database connection refused" no tenía nada que ver
con pagos, y quedó afuera.
Paso 3 — agregas el último eslabón, el que responde la pregunta con un solo número.
grep ERROR access.log | grep Payment | wc -l
Qué esperar:
2
Tres comandos, cada uno haciendo una sola cosa: filtrar por "ERROR", filtrar por "Payment", contar líneas. Ninguno sabe nada del otro más allá de "recibo texto, lo proceso, entrego texto". Esa ignorancia mutua es la razón por la que puedes combinarlos en cualquier orden sensato y con cualquier cantidad de eslabones.
Constrúyela por partes: verifica cada eslabón antes de seguir
Lo que acabas de hacer arriba no fue casualidad de este ejemplo puntual — es el método que quieres usar siempre que la tubería tenga más de un eslabón, sobre todo si alguno de ellos puede borrar, mover o sobrescribir algo:
- Ejecuta el primer comando solo, sin ningún
|todavía, y confirma que su salida es la que esperabas. - Agrega un solo eslabón más y vuelve a mirar el resultado. Si algo salió distinto de lo que pensabas, el problema está en ese último eslabón que acabas de agregar —no tienes que sospechar de toda la cadena.
- Repite hasta llegar al comando final.
- Recién ahí, si el resultado final va a alimentar algo destructivo o irreversible (un
rm, unmv, un script que modifica producción), corre la tubería completa una vez más para confirmarla antes de dejarla suelta.
Escribir la tubería de una sola vez, de punta a punta, y recién ahí correrla, es la forma más común de perder media hora buscando en cuál de los cuatro comandos está el error. Escribir un eslabón, correr, mirar, agregar el siguiente, es más lento la primera vez y muchísimo más rápido en total.
Combinaciones inmediatas con lo que ya sabes
No necesitas herramientas nuevas para que una tubería te sea útil desde hoy mismo. Con lo que ya conoces de los módulos anteriores, estas tres combinaciones resuelven preguntas reales todo el tiempo:
Contar cuántas entradas produjo un comando, sin contarlas a ojo:
ls -l /etc | wc -l
Qué esperar: un número —la cantidad de líneas que imprimió ls -l /etc— en vez del
listado completo desplazándose por la pantalla.
Revisar con calma los resultados de una búsqueda recursiva, cuando son demasiados para que quepan en una sola pantalla:
grep -r "TODO" src/ | less
Qué esperar: en vez de que cientos de coincidencias inunden la terminal de golpe, less
te las muestra de a una pantalla, y puedes avanzar con la barra espaciadora o buscar dentro de
ese resultado con /.
Quedarte solo con los primeros resultados de una búsqueda que puede tardar y producir miles de líneas:
find / -name "*.py" 2> /dev/null | head
Qué esperar: las primeras diez líneas (el valor por defecto de head) de la lista de
archivos .py de todo el sistema, en vez de esperar a que find termine de recorrer cada
directorio para recién entonces ver algo en pantalla.
En los tres casos pasó lo mismo: un comando que ya conocías produjo demasiada salida para mirarla directamente, y una tubería de un solo eslabón la convirtió en algo manejable.
El error clásico: comandos que no leen la entrada estándar
No todos los comandos leen su entrada estándar. Algunos —rm, cp, mv entre los más
comunes— solo miran sus argumentos de línea de comandos (lo que técnicamente se llama
argv): la lista de nombres que escribiste después del comando, separados por espacios. Para
esos comandos, lo que les llega por una tubería es invisible; ni siquiera lo miran.
Imagina un directorio reports/ con archivos temporales que un script dejó atrás:
ls reports/
Qué esperar:
monthly-summary.tmp
weekly-summary.tmp
sales-summary.tmp
Intentas borrarlos encadenando find con rm, como si rm fuera a "recibir" los nombres por
la tubería:
find reports/ -name "*.tmp" | rm
Qué esperar: un mensaje de uso o de "falta un operando", y ningún archivo borrado. El texto
exacto varía según el sistema —algo como rm: missing operand en Linux (GNU coreutils), o un
usage: rm [-f | -i] ... file ... en macOS (BSD rm)— pero el resultado es idéntico en los
dos: rm se queja de que le falta un argumento obligatorio, porque, en efecto, no se le pasó
ninguno. El texto que find mandó por la tubería llegó a la entrada estándar de rm, pero
rm jamás mira su entrada estándar para decidir qué borrar: solo mira argv, y argv vino
vacío.
La solución es xargs. Lee lo que le llega por la entrada estándar, línea por línea, y lo
convierte en argumentos de línea de comandos para el comando que le sigue:
find reports/ -name "*.tmp" | xargs rm
Qué esperar: nada en pantalla —rm recibió monthly-summary.tmp weekly-summary.tmp sales-summary.tmp como argumentos reales, uno por uno, y los tres archivos desaparecieron sin
queja alguna—.
El nombre raro que rompe todo: espacios en un nombre de archivo
Ahora un compañero deja un archivo con un nombre que no sigue la convención del equipo:
find reports/ -name "*.tmp"
Qué esperar:
reports/old report.tmp
Repites la misma tubería que acabas de aprender:
find reports/ -name "*.tmp" | xargs rm
Qué esperar:
rm: reports/old: No such file or directory
rm: report.tmp: No such file or directory
Nada se borró, y los dos "archivos" que rm intentó borrar ni siquiera existen. Por defecto,
xargs separa los argumentos por espacios en blanco —igual que separarías palabras al
escribir en la línea de comandos—. La única línea que le llegó, reports/old report.tmp, la
partió en dos: reports/old y report.tmp. Ninguno de los dos es el archivo real.
La solución exacta: find puede terminar cada resultado con un byte nulo (\0) en vez de
un salto de línea, con -print0. Y xargs -0 le dice a xargs que use ese mismo byte nulo
como separador, en vez de espacios o saltos de línea:
find reports/ -name "*.tmp" -print0 | xargs -0 rm
Qué esperar: sin salida en pantalla —y reports/old report.tmp, el archivo completo, con
el espacio adentro, desapareció—.
¿Por qué el byte nulo y no cualquier otro separador? Porque es el único byte que un nombre de
archivo no puede contener en un sistema Unix (la barra / tampoco puede aparecer dentro de
un nombre, pero esa ya la usas para separar directorios). Un espacio, un tab, e incluso un
salto de línea sí pueden ser parte legítima de un nombre de archivo real —raro, pero legal—.
El byte nulo, no. Por eso -print0 junto con xargs -0 es la única combinación que garantiza
que ningún nombre, por más raro que sea, se corte donde no debe.
Cómo se comportan los errores dentro de una tubería
El operador | conecta exactamente un flujo: la salida estándar (descriptor 1) de la
izquierda con la entrada estándar (descriptor 0) de la derecha. Nunca toca la salida de error
(descriptor 2) de ningún comando de la cadena. Esto tiene una consecuencia que sorprende la
primera vez que la ves:
grep ERROR missing-file.log | wc -l
Qué esperar:
grep: missing-file.log: No such file or directory
0
Dos cosas pasaron, y ninguna tiene que ver con la otra. grep no pudo abrir un archivo que no
existe, y mandó ese error directo a la pantalla por su descriptor 2 —la tubería nunca lo tocó,
porque la tubería solo mueve el descriptor 1—. Al mismo tiempo, como grep no produjo ninguna
línea por su descriptor 1 (no tenía nada que buscar), wc -l recibió una entrada
completamente vacía, y contó, correctamente, cero líneas en ella. El 0 que ves no significa
"no hubo errores en el log" —significa "el log, que ni siquiera existe, no le mandó ninguna
línea a wc"—. Son dos resultados independientes, mezclados en la misma pantalla por
casualidad de que ambos comandos escriben ahí por defecto.
Cada comando de una tubería corre como un proceso separado, casi en simultáneo, y cada uno
termina con su propio resultado interno de éxito o fracaso —de eso, y de cómo usarlo para
encadenar decisiones, se ocupa la última lección de este módulo—. Por ahora, quédate con la
idea central: una tubería nunca oculta ni redirige los errores por sí sola. Si quieres que
también viajen por la tubería, necesitas pedirlo explícitamente con 2>&1, como viste en la
lección anterior.
Errores comunes
1. Pensar que | también combina los errores, igual que 2>&1 (conceptual). Qué pasa:
corres comando1 | comando2 esperando que, si comando1 falla, ese error quede filtrado o
"absorbido" dentro de la tubería, y en cambio lo sigues viendo en pantalla, mezclado con lo
que sea que imprima comando2. Por qué: | conecta un único flujo —descriptor 1 con
descriptor 0—, nunca el descriptor 2. Combinar los errores requiere pedirlo aparte, con
2>&1 antes del |. Cómo detectarlo: ves mensajes de error en la terminal que "no deberían
estar ahí" porque asumías que ya estaban filtrados o redirigidos. Cómo corregirlo: si de
verdad quieres que los errores entren a la tubería, escribe comando1 2>&1 | comando2.
2. Canalizar hacia un comando que no lee la entrada estándar. Qué pasa: escribes
find . -name "*.bak" | rm (o el mismo patrón con cp o mv) esperando que el comando final
"reciba" los nombres por la tubería, y en cambio falla de inmediato con un mensaje de uso
(usage: ...) sin tocar ningún archivo. Por qué: rm, cp y mv toman los nombres de
archivo como argumentos de línea de comandos, no desde la entrada estándar; lo que les llega
por el pipe es invisible para ellos. Cómo detectarlo: el comando termina casi al instante con
un mensaje de "uso" o "falta un operando" en vez de quejarse de un archivo puntual. Cómo
corregirlo: intercala xargs entre los dos comandos (find . -name "*.bak" | xargs rm), que
sí lee la entrada estándar y la convierte en argumentos.
3. Usar xargs sin -0 cuando los nombres de archivo pueden tener espacios o saltos de
línea. Qué pasa: find directorio -name "*.tmp" | xargs rm falla con "No such file or
directory" para archivos que sí existen, o —peor— borra algo que nunca quisiste tocar porque
uno de los fragmentos cortados coincidió por accidente con otro nombre real. Por qué: sin
-0, tanto find como xargs usan espacios en blanco y saltos de línea como separadores; si
un nombre real contiene alguno de esos caracteres, se parte en el lugar equivocado y deja de
ser un solo argumento. Cómo detectarlo: antes de canalizar hacia un comando destructivo,
revisa si algún nombre del directorio tiene espacios (ls -l ya te lo muestra, aunque sea
fácil no prestarle atención). Cómo corregirlo: usa siempre find ... -print0 | xargs -0 ...
cuando el comando final borra, mueve o modifica archivos —el byte nulo es el único carácter
que ningún nombre de archivo puede contener, así que nunca corta donde no debe.
Ejercicios
1. Tienes un archivo orders.log con miles de líneas que empiezan con INFO, WARN o
ERROR. Quieres saber cuántas líneas de error mencionan la palabra timeout. Nunca escribiste
esta tubería antes. Describe, paso a paso, cómo la construirías y verificarías —no solo cuál es
el comando final.
Ver solución
Paso 1: grep ERROR orders.log solo, sin nada más, para confirmar que las líneas que
aparecen son en efecto los errores que esperas ver.
Paso 2: agregar el segundo filtro y volver a mirar: grep ERROR orders.log | grep timeout
— confirmas que el subconjunto que queda tiene sentido (menos líneas que en el paso 1, todas
mencionando "timeout").
Paso 3: agregar el conteo final: grep ERROR orders.log | grep timeout | wc -l — ahí sí
tienes la respuesta a la pregunta original, un solo número.
Por qué funciona: verificar cada eslabón antes de agregar el siguiente significa que, si algo sale mal, sabes que el problema está en el último comando que agregaste —no tienes que sospechar de toda la cadena de una vez.
2. Ejecutas grep ERROR sales.log | wc -l en un directorio donde sales.log no existe.
¿Qué aparece exactamente en la pantalla, y por qué el número que imprime wc -l no significa
lo que a primera vista parece significar?
Ver solución
Aparecen dos cosas independientes:
grep: sales.log: No such file or directory
0
El mensaje de error de grep sale directo a la pantalla por su descriptor de error (2) —la
tubería nunca lo toca, porque | solo conecta el descriptor 1 con la entrada del siguiente
comando—. Como grep no produjo ninguna línea por su descriptor 1 (no había archivo que leer),
wc -l recibió una entrada vacía y contó, correctamente, cero líneas en ella.
Por qué funciona (o, en este caso, por qué engaña): el 0 no significa "no hay errores en
las ventas" —significa "no le llegó ninguna línea a wc", que es una situación completamente
distinta y mucho más grave: el archivo que querías revisar ni siquiera existe.
3. Este comando falla y no borra nada:
find backups/ -name "*.old" | rm
¿Por qué falla exactamente, y cómo lo corriges?
Ver solución
Falla porque rm no lee la entrada estándar en absoluto: solo mira los argumentos de línea de
comandos que le pasas directamente. El texto que find manda por la tubería llega a la
entrada estándar de rm, pero rm nunca la consulta para decidir qué borrar —por eso termina
quejándose de que le falta un argumento, sin tocar ningún archivo.
La corrección es intercalar xargs, que sí lee la entrada estándar y la convierte en
argumentos:
find backups/ -name "*.old" | xargs rm
Por qué funciona: xargs es el traductor entre "texto que llega por una tubería" y
"argumentos de línea de comandos", que es el único idioma que rm entiende.
4. Un directorio archive/ tiene, entre otros, un archivo llamado final report.old (con
un espacio en el nombre). Corres find archive/ -name "*.old" | xargs rm y ves este error:
rm: archive/final: No such file or directory
rm: report.old: No such file or directory
¿Qué salió mal, y cuál es la tubería correcta para borrar ese archivo sin importar qué caracteres raros tenga su nombre?
Ver solución
Salió mal porque, sin ningún indicador especial, tanto find como xargs usan espacios en
blanco y saltos de línea como separadores. La única línea que produjo find,
archive/final report.old, xargs la partió en dos argumentos por el espacio que contiene:
archive/final y report.old —ninguno de los dos es el nombre real del archivo.
La tubería correcta usa el byte nulo como separador, con -print0 en find y -0 en
xargs:
find archive/ -name "*.old" -print0 | xargs -0 rm
Por qué funciona: el byte nulo es el único carácter que un nombre de archivo real no puede
contener, así que usarlo como separador es la única forma de garantizar que un nombre con
espacios, tabs o incluso saltos de línea llegue completo a rm, sin cortarse en un lugar
equivocado.
Resumen y siguiente paso
Ya puedes conectar la salida de un comando directamente con la entrada del siguiente usando
|, sin pasar por un archivo intermedio. Sabes construir esa tubería por partes —verificando
cada eslabón antes de agregar el próximo— en vez de escribirla entera y recién ahí descubrir
dónde falló. Reconoces cuándo el comando final de una cadena no lee la entrada estándar y
necesita xargs para recibir los nombres como argumentos, y sabes por qué -print0 junto con
xargs -0 es la única combinación segura cuando los nombres de archivo pueden tener espacios o
caracteres raros. Y entendiste que una tubería nunca oculta los errores por sí sola: cada
comando de la cadena sigue mandando su descriptor de error directo a la pantalla, a menos que
se lo pidas explícitamente.
Todo lo que conectaste en esta lección fueron piezas que ya conocías de módulos anteriores:
ls, find, grep, wc, head, less. La tubería en sí misma es solo la plomería —conecta,
pero no transforma nada por su cuenta—. Lo que multiplica su poder real son las herramientas
diseñadas específicamente para vivir dentro de una tubería: tomar texto crudo por un lado y
entregar texto ordenado, contado o traducido por el otro. Eso es exactamente lo que sigue:
sort, uniq, cut, tr y sed, el kit de texto que convierte una tubería de dos comandos
en un instrumento real de análisis.
Antes de avanzar deberías poder:
- explicar con tus propias palabras qué conecta exactamente el operador
|, y qué es lo que no conecta (el descriptor de error de ningún comando de la cadena); - construir una tubería de al menos tres comandos, agregando y verificando un eslabón a la vez, sin escribirla entera de un tirón;
- reconocer cuándo un comando no lee la entrada estándar y necesita
xargs, y usar-print0junto conxargs -0cuando los nombres de archivo pueden tener espacios o caracteres poco comunes.
Recursos
- Pipelines — Bash Reference Manual
— la definición oficial del operador
|en bash: cómo se conectan los descriptores y cómo se ejecutan los comandos de una cadena. - Invoking xargs — GNU Findutils Manual
— documentación oficial de
xargs: cómo convierte la entrada estándar en argumentos y qué opciones controla. - xargs(1) — Linux manual page — la
página de manual completa de
xargs, incluida la opción-0y su relación confind -print0. - find(1) — Linux manual page — referencia
completa de
find, con la definición exacta de-print0en la sección de acciones. - BashFAQ/020 — Greg's Wiki — por qué los nombres de
archivo con espacios o saltos de línea rompen las tuberías ingenuas, y por qué
-print0/-0es la solución recomendada frente a otras alternativas.