Módulo 3: Tuberías, redirección y composición

4. Redirigir salida a archivos: >, >>, 2> y /dev/null

Descripción

Al terminar esta lección vas a poder mandar la salida de cualquier comando exactamente a donde quieras: guardarla en un archivo nuevo, agregarla a uno que ya existe, separar los errores de los resultados, combinar los dos flujos cuando te conviene, descartar el ruido que no te interesa, alimentar la entrada de un programa desde un archivo, y ver una salida en pantalla mientras la guardas en disco al mismo tiempo.

Esto no es un truco de sintaxis. Es la habilidad que separa a quien corre un script y cruza los dedos de quien automatiza sin sorpresas: un cron job que falla en silencio porque alguien mandó todo a /dev/null sin pensar en qué estaba tapando, un despliegue que sobrescribió el log de la noche anterior con un > descuidado, un pipeline de análisis que mezcla errores con datos y arruina el conteo final. Los tres son bugs de redirección, no de lógica.

Conexión con el módulo: en la lección anterior viste que todo proceso nace con tres flujos —entrada estándar, salida estándar y salida de error— identificados por los descriptores 0, 1 y 2. Ahora vas a usar esos mismos números para decirle a la terminal, con precisión, a dónde va cada uno.


El modelo mental: una válvula, no un interruptor

Piensa en cada comando como una tubería con tres bocas: una de entrada y dos de salida. Por defecto, las dos salidas terminan en la pantalla y la entrada viene del teclado. Los operadores de redirección no crean flujos nuevos —nunca agregan una boca donde no había una—; giran una válvula para que un flujo que ya existía termine en otro lugar: un archivo, el mismo destino de otro flujo, o un desagüe que descarta todo.

Esa imagen —girar una válvula, no abrir una tubería nueva— es la que necesitas para entender por qué el orden importa en algunas líneas: si una válvula apunta "a donde apunta la otra ahora mismo", invertir el orden en que giras las dos cambia el resultado. Vas a ver exactamente ese caso más abajo, con 2>&1.

Todo lo que sigue funciona igual en bash y en zsh (el shell por defecto de macOS desde 2019) salvo un detalle puntual que se marca donde aparece.


> — escribir en un archivo (y el peligro real)

> toma la salida estándar de un comando y la escribe en un archivo. Si el archivo no existe, lo crea. Si existe, lo trunca y lo reemplaza sin preguntar — no hay confirmación, no hay papelera, no hay deshacer.

ls -l /etc > listing.txt

Qué esperar: nada en pantalla — la salida completa de ls -l /etc fue a parar a listing.txt — y cat listing.txt muestra el listado entero.

El peligro aparece cuando repites el mismo comando, o uno parecido, sin fijarte que el archivo de destino ya tenía algo valioso:

echo "Deploy started" > deploy.log
# ...horas después, alguien corre algo similar apuntando al mismo archivo...
echo "Debugging session" > deploy.log
cat deploy.log

Qué esperar:

Debugging session

La primera línea desapareció sin aviso. > hizo exactamente lo que se le pidió: truncar y escribir.

La protección es la opción noclobber del shell:

set -o noclobber
echo "test" > deploy.log

Qué esperar (con deploy.log ya existente, como quedó arriba):

bash: deploy.log: cannot overwrite existing file

Si de verdad quieres sobrescribir a pesar de noclobber, el operador >| fuerza la escritura una sola vez — y funciona igual en bash y en zsh, así que te sirve sin importar cuál corre tu terminal:

echo "test" >| deploy.log

noclobber no te protege de todo — no dice nada sobre >>, y solo actúa cuando el operador es > — pero elimina el error más caro: pisar un archivo que creías que ya nadie tocaba.


>> — agregar sin destruir

>> abre el archivo en modo agregar: escribe al final y, si el archivo no existe, lo crea. Es la herramienta para acumular, no para reemplazar.

echo "Deploy started at 09:00" > session.log
echo "Deploy finished at 09:04" >> session.log
cat session.log

Qué esperar:

Deploy started at 09:00
Deploy finished at 09:04

La regla práctica: usa > la primera vez que un script escribe un archivo en una ejecución —para arrancar limpio— y >> en cada escritura posterior dentro de esa misma corrida. Confundir los dos es la causa más común de "¿por qué mi log solo tiene la última línea?".


2> — separar los errores

El descriptor 2 es la salida de error. 2> la redirige sin tocar la salida estándar (descriptor 1), que sigue yendo a la pantalla.

ls -l /etc /no-such-directory 2> errors.log

Qué esperar en pantalla: el listado normal de /etc — eso siguió como salida estándar, nunca se redirigió. Y en errors.log:

ls: /no-such-directory: No such file or directory

Esto es justo lo que preparó la lección anterior: los dos flujos son independientes, y ahora tienes el operador que apunta a cada uno por separado.


2>&1, el orden, y el atajo &>

A veces quieres los dos flujos en el mismo destino: un log único con resultados y errores en el orden en que ocurrieron. 2>&1 significa "manda el descriptor 2 a donde ahora mismo apunta el descriptor 1". La palabra clave es ahora mismo: el shell procesa las redirecciones de izquierda a derecha, así que el orden decide el resultado.

ls -l /etc /no-such-directory > all.log 2>&1

Qué esperar: primero > all.log apunta la salida estándar al archivo. Después 2>&1 apunta el descriptor 2 a donde ya está el descriptor 1 — es decir, al mismo archivo. all.log termina con el listado y el error, mezclados en orden.

Invierte el orden y se rompe:

ls -l /etc /no-such-directory 2>&1 > all.log

Aquí 2>&1 se procesa primero, cuando el descriptor 1 todavía apunta a la pantalla: el error queda apuntando ahí. Recién después > all.log mueve la salida estándar al archivo. all.log solo tiene el listado; el error se imprime en pantalla, exactamente donde no lo querías.

El atajo &> (o &>> para agregar) hace lo mismo que > archivo 2>&1 en una sola pieza, sin el riesgo de invertir el orden:

ls -l /etc /no-such-directory &> all.log

/dev/null: cuándo silenciar es legítimo y cuándo es esconder un problema

/dev/null es un archivo especial que descarta cualquier cosa que le escribas. No ocupa espacio, no guarda nada, no se puede leer de vuelta. Mandar algo ahí es decirle al sistema "esto no me interesa, tíralo".

find / -name "*.conf" 2> /dev/null

Qué esperar: la lista de archivos .conf que sí pudiste leer, sin que la pantalla se llene de líneas Permission denied por cada directorio del sistema al que no tienes acceso.

El criterio para no convertir esto en un hábito peligroso: silenciar es legítimo cuando ya sabes exactamente qué error vas a recibir, por qué lo vas a recibir, y confirmaste que no cambia el resultado que te importa. En el ejemplo de find, sabes de antemano que buscar desde / como usuario normal va a chocar con directorios protegidos del sistema — ese ruido es esperado y no afecta la lista que buscas.

Es esconder un problema cuando silencias todo un comando (2>/dev/null sin pensar en qué error espera) dentro de un script de producción, un despliegue o un cron job, "porque molestaba en los logs". Ahí no eliminaste el ruido: eliminaste la evidencia. El comando puede estar fallando de verdad —una credencial vencida, un disco lleno, una base de datos caída— y no te vas a enterar hasta que el efecto llegue mucho más tarde, sin rastro de la causa.


< — alimentar la entrada desde un archivo

Así como > redirige la salida, < redirige la entrada estándar: en lugar de esperar que escribas en el teclado, el comando lee de un archivo.

wc -l < access.log

Qué esperar:

1284

Compáralo con wc -l access.log (sin <): el conteo es el mismo, pero esa segunda forma imprime también el nombre del archivo (1284 access.log), porque ahí se lo pasaste como argumento y wc sabe cuál está leyendo. Con < el comando ni se entera de que hay un archivo de por medio — solo ve una entrada estándar con contenido adentro. La diferencia importa cuando encadenas comandos que solo deben imprimir el número, sin el nombre pegado al lado.


tee y tee -a: ver y guardar a la vez

Todo lo anterior te obliga a elegir: o ves la salida en pantalla, o la guardas en un archivo. tee rompe esa disyuntiva — lee su entrada estándar y la copia hacia dos lugares a la vez: la pantalla y uno o más archivos.

ls -l /etc | tee listing.txt

Qué esperar: el mismo listado que verías con ls -l /etc normal aparece en pantalla, y simultáneamente queda guardado, completo, en listing.txt.

(El símbolo | que conecta la salida de ls con la entrada de tee es la tubería — la vas a explorar a fondo en la próxima lección. Por ahora, solo obsérvala haciendo su trabajo: pasar lo que sale de un comando a la entrada del siguiente.)

Por defecto tee trunca el archivo, igual que >. Para agregar en vez de reemplazar, usa -a:

echo "Deploy started at 09:00" | tee -a process.log
echo "Deploy finished at 09:04" | tee -a process.log

Qué esperar: ambas líneas aparecen en pantalla en el momento en que se ejecutan, y las dos quedan acumuladas en process.log — útil cuando quieres seguir un proceso largo en vivo sin perder el registro completo para revisar después.


Errores comunes

1. Creer que 2>&1 funciona igual sin importar dónde lo pongas (conceptual). Qué pasa: alguien escribe comando 2>&1 > archivo.log esperando ver errores y resultados juntos en el archivo, y los errores igual aparecen en pantalla. Por qué: 2>&1 no dice "unir stdout y stderr para siempre" — dice "apuntar el descriptor 2 a donde hoy apunta el descriptor 1", y el shell lo resuelve en el momento en que lo lee, de izquierda a derecha. Si el descriptor 1 todavía apunta a la pantalla cuando se procesa el 2>&1, ahí se queda. Cómo detectarlo: el archivo de log tiene menos líneas de las que esperabas y los errores siguen saliendo en la terminal. Cómo corregirlo: pon 2>&1 al final (> archivo 2>&1) o usa el atajo &>, que no te deja invertir el orden por accidente.

2. Sobrescribir un archivo importante con > sin darte cuenta. Qué pasa: corres un comando con > apuntando a un archivo que ya tenía contenido valioso —un log, un backup, una config— y lo pierdes sin ningún aviso. Por qué: > trunca de inmediato, antes de ejecutar el comando, y no compara ni pregunta. Cómo detectarlo: revisa el tamaño o la fecha de modificación del archivo (ls -l archivo) apenas sospeches — si tiene menos contenido o una fecha más reciente de la que esperabas, ya se sobrescribió. Cómo corregirlo hacia adelante: activa set -o noclobber en tu sesión (o en tu .bashrc/.zshrc) para los archivos que de verdad no quieres perder, y reserva >| para las veces que sí quieres forzar la escritura a sabiendas.

3. Usar /dev/null para "que el script deje de quejarse" sin saber qué error estás tapando. Qué pasa: un script de despliegue o un cron job termina con 2>/dev/null pegado al final de cada línea porque en algún momento generaba ruido molesto, y meses después una falla real —permisos, conexión, disco lleno— pasa completamente inadvertida. Por qué: /dev/null no distingue el ruido esperado del error grave; descarta todo lo que le llega al descriptor 2, sin criterio. Cómo detectarlo: si no puedes explicar, comando por comando, qué mensaje específico esperas que aparezca ahí y por qué es inofensivo, es señal de que estás tapando en vez de filtrar. Cómo corregirlo: redirige a un archivo (2>> errors.log) en lugar de a /dev/null mientras no tengas certeza, revisa ese archivo periódicamente, y solo entonces decide, error por error, cuáles sí son ruido esperado.


Ejercicios

1. Tienes este comando, que se supone debe guardar tanto el resultado como los errores de grep en search.log:

grep -r "TODO" src/ 2>&1 > search.log

Al revisarlo, notas que los errores (por ejemplo, "Permission denied" en algún directorio) siguen apareciendo en la terminal en vez de quedar en search.log. ¿Por qué pasa esto, y cómo lo corriges de dos formas distintas?

Ver solución

Pasa porque el orden importa: 2>&1 se procesa primero, cuando el descriptor 1 (stdout) todavía apunta a la terminal, así que el descriptor 2 (stderr) queda apuntando también a la terminal. Recién después > search.log mueve stdout al archivo, pero stderr ya quedó fijado en pantalla.

Dos formas de corregirlo:

grep -r "TODO" src/ > search.log 2>&1

o, más corto y sin riesgo de invertir el orden:

grep -r "TODO" src/ &> search.log

Por qué funciona: en los dos casos, para el momento en que se procesa 2>&1 (o el equivalente &>), el descriptor 1 ya está apuntando al archivo — así que el descriptor 2 termina en el mismo lugar.

2. Quieres contar cuántas líneas de un archivo sales.csv contienen la palabra "refund", sin que el resultado incluya el nombre del archivo al lado del número. Escribe el comando usando redirección de entrada (<) en vez de pasar el archivo como argumento.

Ver solución
grep -c "refund" < sales.csv

Por qué funciona: < conecta el contenido de sales.csv a la entrada estándar de grep, así que grep nunca sabe que hay un archivo involucrado — solo ve una entrada estándar con líneas adentro, y por eso su salida es solo el número, sin el nombre del archivo pegado al lado (que sí aparecería con grep -c "refund" sales.csv).

3. Un compañero corre este comando en un servidor de producción y te pregunta por qué "no hace nada":

./backup.sh > /dev/null 2>&1

¿Qué está pasando realmente, y qué cambiarías para poder diagnosticar un fallo si el backup empieza a fallar mañana?

Ver solución

El comando no es que "no haga nada": está enviando tanto la salida normal como los errores de backup.sh a /dev/null, es decir, descartando por completo cualquier evidencia de éxito o fallo. Si el script falla mañana, no va a quedar ningún rastro de por qué.

Un cambio razonable es reemplazar el agujero negro por un archivo que sí puedas revisar:

./backup.sh &> backup.log

o, si de verdad hay una parte del output que es ruido esperado y conocido, silenciar solo esa parte de forma específica en vez de todo el comando.

Por qué funciona: conservas evidencia diagnosticable —un archivo con fecha y contenido— en lugar de descartar todo a ciegas, que es exactamente la diferencia entre silenciar con criterio y esconder un problema.


Resumen y siguiente paso

Ya puedes decidir, con precisión, a dónde va cada flujo de un comando: crear un archivo con > —y protegerte de pisarlo con noclobber—, agregar con >>, separar errores con 2>, combinarlos en el orden correcto con 2>&1 o con el atajo &>, descartar ruido esperado en /dev/null sin esconder fallas reales, alimentar la entrada desde un archivo con <, y ver y guardar a la vez con tee y tee -a.

Todo lo que hiciste en esta lección movió flujos entre un comando y un archivo. Lo que viene es mover un flujo directamente de un comando a otro, sin pasar por disco: la tubería, |.

Antes de avanzar deberías poder:

  • explicar, sin mirar esta lección, por qué comando 2>&1 > archivo no hace lo mismo que comando > archivo 2>&1;
  • decidir si un caso concreto de "silenciar con /dev/null" es legítimo o está escondiendo un problema;
  • escribir de memoria la forma correcta de guardar stdout y stderr juntos en un archivo, con el atajo y con la forma explícita.

Recursos