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 > archivono hace lo mismo quecomando > 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
- Redirections — Bash Reference Manual
— la referencia oficial de todos los operadores de redirección de bash, incluidos
>|y&>. - tee invocation — GNU Coreutils Manual
— documentación oficial de
tee, sus opciones (-a,-i) y su comportamiento exacto. - NoClobber — Greg's Wiki (BashFAQ) — explica
noclobberen detalle, sus límites (no protege>>ni las tuberías) y cómo combinarlo con>|. - tee(1) — Linux manual page — página de
manual completa de
tee, útil para consultar todas sus opciones desde la terminal conman tee.