Módulo 3: Tuberías, redirección y composición
3. Los tres flujos: stdin, stdout y stderr
Descripción
Imagina esta escena: dejas corriendo un script de respaldo por la noche y guardas su salida en un archivo de registro para revisarla en la mañana. Al día siguiente abres ese archivo y se ve perfecto — ni una sola línea de error. Pero el respaldo nunca se hizo. ¿Cómo puede un archivo de registro estar limpio y el trabajo haber fallado de todos modos? La respuesta no está en el script: está en algo que todo proceso tiene desde el instante en que arranca, y que hasta ahora usaste sin nombrarlo.
En esta lección vas a poder explicar por qué un programa tiene tres tubos de comunicación separados —uno de entrada y dos de salida—, identificar por cuál de esos tres viaja cualquier mensaje que veas en tu terminal, y diagnosticar el caso de arriba: por qué un error sigue apareciendo en pantalla (o desaparece sin dejar rastro) aunque hayas guardado "la salida" del programa. Esto no es una curiosidad técnica. Es la razón por la que un pipeline de datos, un despliegue automatizado o un cron job pueden fallar en silencio: si no sabes que existen dos salidas distintas, capturas una y das por hecho que capturaste ambas.
Conexión con el módulo: en la lección anterior viste que cada programa de Unix produce texto plano para que otro programa pueda consumirlo — la interfaz universal que hace posible la composición. Lo que faltaba precisar es que ese texto no sale por un solo lugar: sale por dos, y cada uno tiene un trabajo distinto. Los números que vas a conocer aquí (0, 1 y 2) no son trivia: son literalmente la sintaxis que vas a escribir en la próxima lección (2>, 1>, &>). Sin este modelo mental, esa sintaxis se memoriza como un conjuro; con él, se deduce.
Un proceso nace con una entrada y dos salidas
Piensa en un proceso como si fuera un asistente sentado en un escritorio con tres canastas. Una canasta de entrada, donde le llegan instrucciones o datos para trabajar. Y dos canastas de salida — no una: dos. En la primera pone el resultado terminado, lo que le pediste. En la segunda pone algo distinto: una nota aparte cuando algo no salió bien, un archivo que no encontró, un permiso que le faltó. Nunca mezcla las dos canastas de salida entre sí, aunque las dos estén sobre el mismo escritorio y, si no las separas tú, ambas terminen frente a tus ojos en la misma pantalla.
Eso es exactamente lo que hace un proceso en la terminal. Cuando cualquier programa arranca, el sistema operativo le abre automáticamente tres canales, sin que el programa tenga que pedirlo:
| Nombre | Nombre técnico | Número (descriptor) | Para qué sirve |
|---|---|---|---|
| Entrada estándar | stdin | 0 | De ahí lee el programa lo que necesita procesar (normalmente, lo que tú escribes en el teclado) |
| Salida estándar | stdout | 1 | Ahí escribe el programa su resultado normal — lo que pediste |
| Salida de error estándar | stderr | 2 | Ahí escribe el programa sus quejas: errores, advertencias, mensajes de diagnóstico |
Como diagrama, la forma de un proceso cualquiera es siempre esta:
entrada estándar ┌─────────────┐
(stdin, descriptor 0) ───────► │ │
normalmente: tu teclado │ proceso │
│ │──────► salida estándar
│ │ (stdout, descriptor 1)
│ │ el resultado que pediste
│ │
│ │──────► salida de error
└─────────────┘ (stderr, descriptor 2)
quejas y diagnósticos
Esos números —0, 1 y 2— no son una convención decorativa: son el descriptor de archivo (file descriptor) que el sistema operativo asigna a cada canal. Un descriptor de archivo es solo un número con el que el sistema identifica un canal de entrada o salida abierto; los tres primeros que recibe cualquier proceso, sin excepción, son siempre estos mismos. Grábate los números ahora, aunque todavía no hagas nada con ellos: en la próxima lección vas a escribir literalmente 2> para decir "descriptor 2, redirígelo hacia allá", y 1> (que casi siempre se abrevia >) para decir lo mismo del descriptor 1. La sintaxis de mañana es, letra por letra, esta tabla.
Un dato de contexto que ayuda a que esto se sienta menos arbitrario: stderr no siempre existió. En los primeros Unix de los años setenta, los errores se mezclaban con la salida normal en un único flujo, y en los sistemas de la época eso literalmente desperdiciaba papel: máquinas de fotocomposición imprimían mensajes de error como si fueran parte del resultado. Separar un tercer canal solo para diagnósticos fue la solución, y se quedó para siempre como parte del diseño de Unix.
Ejemplo trabajado
Vamos a verlo, no solo a leerlo. Corre este comando tal cual —no necesitas crear ningún archivo, /etc/hosts existe en macOS, Linux y en WSL— pidiéndole a ls que liste un archivo real junto a uno que no existe:
ls /etc/hosts /etc/hosts-fantasma
Qué esperar: en tu pantalla vas a ver dos líneas mezcladas, algo así:
ls: /etc/hosts-fantasma: No such file or directory
/etc/hosts
Fíjate en un detalle que no es casualidad: en el comando escribiste primero la ruta que sí existe y después la que no, pero en pantalla el error aparece primero. Eso es evidencia directa de que no es un solo flujo de texto en orden: son dos canales independientes, cada uno entregado a la pantalla con su propio ritmo, y solo coinciden ahí porque nadie les dijo lo contrario todavía.
Ahora la parte que responde la pregunta de la introducción. Guarda solo el resultado normal en un archivo, con > (el símbolo que vas a estudiar a fondo en la próxima lección; por ahora solo necesitas saber que le dice a la shell "lo que salga por el descriptor 1, en vez de mostrarlo en pantalla, escríbelo en este archivo"):
ls /etc/hosts /etc/hosts-fantasma > listing.txt
Qué esperar: en la pantalla ves solo esto —el error, y nada más—:
ls: /etc/hosts-fantasma: No such file or directory
Y si abres el archivo que acabas de crear:
cat listing.txt
/etc/hosts
Ahí está la respuesta al escenario del respaldo nocturno. El archivo quedó impecable —contiene exactamente el resultado limpio, ni una queja— porque > solo agarra el descriptor 1. El error nunca tuvo a dónde ir más que a la pantalla, y si nadie estaba mirando esa pantalla a las tres de la mañana (por ejemplo, porque el script corrió en un cron job sin que nadie tuviera una sesión abierta), ese mensaje se perdió sin dejar rastro en ningún archivo. Un registro limpio no significó que todo saliera bien: significó que solo capturaste una de las dos salidas.
Por qué existe esta separación
La razón de fondo es exactamente la que acabas de comprobar: separar los canales te permite decidir, con precisión, qué hacer con cada uno. Si stdout y stderr fueran un único flujo, no habría forma de guardar "el resultado limpio" sin que se colara cualquier queja en medio — y tampoco habría forma de silenciar solo las quejas sin perder también el resultado que sí necesitabas. Un pipeline de análisis de datos que espera un archivo CSV bien formado se rompe en el instante en que una línea de error se cuela entre los datos; un proceso automatizado que necesita saber si algo falló se queda ciego si esa señal se pierde entre el resultado normal.
Esto es exactamente el problema que resuelve tener dos salidas separadas por diseño: puedes capturar el resultado limpio en un archivo y al mismo tiempo dejar que las quejas sigan siendo visibles (o mandarlas a otro lado, o descartarlas a propósito). No es casualidad ni exceso de ingeniería: es la única forma de que "guarda el resultado" y "avísame si algo sale mal" sean dos decisiones independientes en lugar de una sola decisión forzada. La próxima lección te da exactamente ese control — > para el descriptor 1, 2> para el descriptor 2, y las formas de combinarlos — pero el control solo tiene sentido si entendiste primero, como ya lo hiciste, que hay dos canales distintos que controlar.
Errores comunes
"Si no vi texto en rojo, no hubo ningún error." Que muchas terminales y herramientas (como Git o algunos linters) pinten stderr en rojo es una cortesía visual de esa herramienta en particular, no una propiedad de stderr. El descriptor 2 no lleva color: es un canal, no un formato. Hay sistemas y configuraciones donde stdout y stderr se ven exactamente igual —texto blanco sobre negro, sin distinción— y el error pasa desapercibido si solo lo buscas por su color. La forma correcta de verificar es estructural, no visual: como hiciste en el ejemplo, redirige un canal y observa qué desaparece de la pantalla y qué queda.
Un comando que "se congela" y en realidad está esperando tu teclado. Escribe cat sin ningún argumento y presiona Enter. La terminal no muestra ningún prompt nuevo, el cursor parpadea, y da la sensación de que algo se rompió. No se rompió nada: cat sin un archivo que leer recurre a su entrada estándar por defecto, que normalmente es tu teclado, y se queda esperando que le mandes algo por ahí. No es un error, es el comportamiento documentado de stdin. Se detecta porque no hay ningún mensaje de error ni prompt devuelto, solo silencio; se resuelve escribiendo algo y presionando Ctrl+D al inicio de una línea vacía, que le indica al programa "fin de la entrada" (técnicamente, envía EOF — end of file) y lo hace terminar con lo que le diste. Ctrl+C también lo saca del bloqueo, pero abortando el comando en lugar de dejarlo procesar lo que escribiste.
"El archivo de registro salió limpio, entonces el programa terminó bien." Es el error que abrió esta lección, y vale la pena decirlo aparte porque es el más caro en producción: un archivo de registro limpio solo prueba que el descriptor 1 no reportó problemas. No dice nada del descriptor 2, a menos que también lo hayas dirigido explícitamente hacia ese mismo archivo (algo que vas a aprender a hacer con 2>&1 en la próxima lección). Antes de confiar en "el log no tiene errores" como prueba de éxito, verifica primero a dónde estaba apuntando cada uno de los dos canales cuando el programa corrió.
Ejercicios
1. Identifica el canal
Sin ejecutar nada todavía, mira este comando y su salida en pantalla:
wc -l /etc/hosts /etc/passwd-fantasma
wc: /etc/passwd-fantasma: open: No such file or directory
12 /etc/hosts
12 total
¿Qué línea o líneas viajaron por stdout y cuál viajó por stderr? Justifica tu respuesta con lo que cada línea representa, no solo con su posición.
Ver solución
La línea wc: /etc/passwd-fantasma: open: No such file or directory viajó por stderr (descriptor 2): es un mensaje de diagnóstico sobre algo que salió mal (un archivo que wc no pudo abrir), no un resultado. Las líneas 12 /etc/hosts y 12 total viajaron por stdout (descriptor 1): son el resultado normal que pediste — el conteo de líneas del archivo que sí existe, y el total.
Por qué funciona: el criterio para clasificar una línea nunca es su posición en la pantalla ni su formato — es qué representa. Un resultado esperado (un conteo, un listado, un dato) es stdout; un mensaje sobre un problema (un archivo ausente, un permiso denegado) es stderr, sin importar en qué orden aparezcan mezclados.
2. El comando que no responde
Un compañero te escribe angustiado: "escribí grep pendiente en la terminal, presioné Enter, y ya no hace nada. No sale ningún error, no vuelve el símbolo de que puedo escribir otro comando. ¿Se colgó mi terminal?" ¿Qué está pasando en realidad y qué le dices que haga?
Ver solución
Nada se colgó. grep patrón sin un nombre de archivo como segundo argumento no tiene de dónde leer texto para buscar el patrón, así que recurre a su entrada estándar por defecto: el teclado. Se quedó esperando que alguien le escriba líneas de texto ahí mismo, en la terminal, para buscar pendiente en lo que vaya llegando. Es exactamente el mismo comportamiento que viste con cat sin argumentos, solo que con grep en vez de cat.
Dile que escriba lo que quiera (por ejemplo, unas líneas con y sin la palabra "pendiente") y presione Ctrl+D al inicio de una línea vacía para indicarle a grep que la entrada terminó; en ese momento va a imprimir las líneas que sí contenían el patrón y devolverle el control de la terminal. Si solo quiere salir sin usarlo, Ctrl+C aborta el comando sin procesar nada.
Por qué funciona: un programa que espera en silencio, sin prompt ni mensaje de error, casi siempre está leyendo de stdin y nadie le ha dado un archivo — no es una señal de fallo, es la ausencia del argumento que evitaría esa espera.
3. Antes de ver la sintaxis, predícela
Ya sabes que el descriptor 1 es stdout y el descriptor 2 es stderr, y que > mueve el descriptor 1 hacia un archivo. Sin buscar nada todavía: ¿qué crees que hace la sintaxis 2> errores.txt? No la ejecutes — escribe primero tu hipótesis con tus propias palabras.
Ver solución
2> errores.txt debería redirigir el descriptor 2 (stderr) hacia el archivo errores.txt, dejando que el descriptor 1 (stdout) siga imprimiéndose en pantalla como siempre — exactamente lo simétrico de lo que hiciste en el ejemplo trabajado con > (que es la forma corta de 1>).
Por qué funciona: la sintaxis de redirección no es un conjunto de símbolos para memorizar por separado; es el número de un descriptor seguido de una flecha hacia un destino. Si entendiste que 1 y 2 son dos canales distintos, la sintaxis de cada uno es predecible antes de leer la documentación — que es exactamente lo que vas a confirmar en la próxima lección.
4. El orden que no era casualidad
En el ejemplo trabajado, el comando fue ls /etc/hosts /etc/hosts-fantasma, con la ruta real escrita primero. Sin embargo, en pantalla el mensaje de error apareció antes que el nombre del archivo real. ¿Por qué puede pasar esto, si el error corresponde al segundo argumento del comando?
Ver solución
Porque stdout y stderr son dos canales independientes, cada uno con su propio comportamiento de entrega a la pantalla (lo que se conoce como buffering): stderr suele entregarse de inmediato, línea por línea, mientras que stdout puede acumularse un instante antes de mostrarse. El orden en que escribiste los argumentos en el comando no determina el orden en que sus efectos llegan a tu pantalla, precisamente porque no comparten una sola tubería de salida.
Por qué funciona: si stdout y stderr fueran el mismo flujo, el orden de aparición en pantalla coincidiría siempre con el orden en que el programa los generó. Que no coincida es prueba observable —no solo teórica— de que viajan por canales separados.
Resumen y siguiente paso
Antes de avanzar deberías poder:
- nombrar los tres flujos de cualquier proceso (
stdin,stdout,stderr) y sus descriptores (0,1,2) sin dudar; - mirar una salida mezclada en pantalla y clasificar cada línea según lo que representa, no según su posición;
- reconocer un programa que espera datos por stdin (silencio, sin prompt, sin error) y saber que
Ctrl+Dtermina la entrada mientrasCtrl+Caborta el comando; - explicar por qué un archivo de registro "limpio" no es prueba de que todo salió bien, si solo capturaste el descriptor 1.
Lo que acabas de instalar es el modelo mental completo que hace obvias las siguientes cuatro lecciones de este módulo. La próxima toma exactamente los números 0, 1 y 2 que acabas de conocer y les da su sintaxis real: > y 1> para redirigir el resultado, 2> para redirigir el error, 2>&1 y &> para unir ambos a propósito, /dev/null para descartar lo que no te importa, < para alimentar la entrada desde un archivo en vez del teclado, y tee para ver algo en pantalla y guardarlo a la vez. No vas a memorizar símbolos sueltos: vas a reconocer, en cada uno, el mismo descriptor que ya sabes nombrar.
Recursos
- stdin(3) — Linux manual page — la referencia técnica de
stdin,stdoutystderr, con la confirmación explícita de que sus descriptores son 0, 1 y 2 al arrancar cualquier programa. - The Open Group Base Specifications — stdin — el estándar POSIX que define los tres flujos predefinidos y las constantes
STDIN_FILENO,STDOUT_FILENOySTDERR_FILENO. - Standard streams — Wikipedia — panorama general de los tres flujos, incluida la razón histórica por la que Unix separó stderr de stdout en los años setenta.
- Making cron notify only on failure — James Cherti — un caso real de por qué separar stdout de stderr importa en automatización: cron reenvía por correo cualquier cosa que un job escriba en cualquiera de los dos canales.