En el capítulo anterior viste cómo las expresiones regulares te permiten buscar y transformar texto con una precisión casi quirúrgica. Pero te propongo una reflexión: ¿de qué sirve buscar patrones si no controlas de dónde vienen los datos ni adónde van? La respuesta a esa pregunta son las redirecciones. Cada proceso en Linux, incluidos tus scripts de Bash, nace con tres canales de comunicación abiertos: la entrada estándar, la salida estándar y la salida de error. Durante años yo usaba Bash sin prestar atención a esto. Escribía comando > archivo porque funcionaba, pero sin entender realmente qué estaba pasando. Y claro, cuando algo fallaba, no sabía por qué. La cuestión es que las redirecciones son el mecanismo que tiene Bash para controlar de dónde vienen los datos y adónde van. Puedes redirigir la entrada de un comando para que no venga del teclado, sino de un archivo. Puedes redirigir su salida para que no vaya a la pantalla, sino a un archivo, a otro comando o incluso a un descriptor que tú mismo crees.
Los descriptores de archivo (file descriptors, FD) son números enteros que representan canales de E/S abiertos. Piensa en ellos como manijas que tu script agarra para leer o escribir datos. Los tres primeros (0, 1, 2) ya vienen asignados; los demás (del 3 al 9 y más allá con {var}) los abres tú.
Cuando entiendes las redirecciones en profundidad, tu forma de escribir scripts cambia por completo. Dejas de ver la entrada y la salida como algo fijo y empiezas a manipularlas con la misma libertad con la que manipulas variables.
Los tres descriptores estándar
Todo proceso Unix recibe tres descriptores al nacer:
- 0: stdin — Entrada estándar (teclado) — Lectura
- 1: stdout — Salida estándar (pantalla) — Escritura
- 2: stderr — Salida de error (pantalla) — Escritura
Aunque los tres apunten al mismo sitio (la terminal) en una sesión interactiva, son canales independientes. Esto significa que puedes redirigir uno sin afectar a los otros.
# stdout va a un archivo, stderr sigue yendo a la terminal
ls /exists /no-existe > salida.txt
# 'ls /no-existe: No such file or directory' se muestra en pantalla
Esa independencia es la base de todo el sistema de logging y depuración que construiremos más adelante. Te puedo asegurar que una vez que interiorizas esto, empiezas a ver los scripts de otra manera. Yo recuerdo la primera vez que entendí que podía tener stdout y stderr yendo a sitios distintos… me pareció magia. Y no lo es: es diseño del sistema.
Concepto clave: cuando haces
echo "hola" > archivo, Bash abre el archivo, lo asigna al descriptor 1 (stdout) del comandoecho, y el comando escribe ahí. Elechonunca sabe que está escribiendo en un archivo; para él sigue siendo stdout.
Redirecciones básicas: >, >>, <
> — Redirigir salida (sobrescribe)
Conecta stdout del comando a un archivo, creándolo o sobrescribiéndolo:
echo "Primera línea" > salida.txt
echo "Segunda línea" > salida.txt
# salida.txt solo contiene "Segunda línea"
El truncado ocurre antes de que el comando se ejecute. Bash abre el archivo, lo vacía, y entonces lanza el comando.
>> — Redirigir salida (añade)
Igual que >, pero añade al final en lugar de sobrescribir:
echo "Línea 1" >> salida.txt
echo "Línea 2" >> salida.txt
# salida.txt contiene ambas líneas
< — Redirigir entrada
Conecta un archivo como entrada del comando:
wc -l < salida.txt # Cuenta líneas del archivo
sort < desordenado.txt # Ordena el contenido del archivo
grep "error" < log.txt # Busca en el archivo
La diferencia entre wc -l salida.txt y wc -l < salida.txt es sutil pero real. En el primer caso el comando recibe el nombre del archivo y decide cómo leerlo. En el segundo, Bash se lo pasa ya abierto como stdin, y el comando ni siquiera sabe su nombre.
wc -l salida.txt # Output: 10 salida.txt
wc -l < salida.txt # Output: 10 (sin nombre de archivo)
Redirigir stderr: 2>, 2>&1, &>
El descriptor 2 (stderr) es independiente del 1 (stdout). Redirigirlo es exactamente igual, pero usando el número:
# Errores a un archivo, salida normal a otro
find / -name "*.conf" 2> errores.txt > aciertos.txt
# Errores a /dev/null (los descartas)
find / -name "*.conf" 2> /dev/null
# Ambos al mismo archivo (dos formas)
comando > salida.txt 2>&1
comando &> salida.txt # Sintaxis abreviada (Bash >= 4)
comando >& salida.txt # Forma aún más corta
El orden importa. Cuando ves comando > archivo 2>&1, el shell procesa las redirecciones de izquierda a derecha:
> archivo— redirige stdout (1) aarchivo2>&1— redirige stderr (2) al mismo sitio que stdout (1), que en ese momento ya esarchivo
Si inviertes el orden, el resultado cambia. Y esto es una faena, porque es de esos errores que no dan mensaje, simplemente se comportan de forma incorrecta. Te puedo asegurar que más de una vez he estado media hora debugando un script pensando «pero si esto debería funcionar…» para descubrir que había puesto el 2>&1 antes que la redirección al archivo.
# Orden incorrecto
comando 2>&1 > archivo
# Paso 1: 2>&1 — stderr va al stdout original (terminal)
# Paso 2: > archivo — stdout va a archivo
# Resultado: stderr sale por terminal, stdout va a archivo
# Orden correcto
comando > archivo 2>&1
# Paso 1: > archivo — stdout va a archivo
# Paso 2: 2>&1 — stderr va a archivo (donde está stdout ahora)
Memorízala: primero el destino del 1, luego duplicas el 2 hacia el 1.
&> y >& — Atajos
Bash ofrece atajos que redirigen ambos a la vez y eliminan la confusión del orden:
comando &> archivo # stdout + stderr → archivo
comando &>> archivo # stdout + stderr → archivo (añadiendo)
Son equivalentes a > archivo 2>&1 pero en un solo token y sin ambigüedad.
Heredocs y herestrings
Here document (<<)
Un here document (heredoc) alimenta stdin de un comando con texto literal incluido en el propio script:
cat << EOF
Esto es un here document.
Puede tener varias líneas.
Las variables como $HOME se expanden.
EOF
El delimitador EOF puede ser cualquier palabra. Todo lo que hay entre las dos marcas se convierte en stdin del comando.
Comillas en el delimitador: si pones el delimitador entre comillas, las expansiones (variables, comandos) se desactivan:
cat << 'EOF'
$HOME no se expande
$(echo "esto tampoco")
EOF
Tabulador con <<-: si usas <<-, Bash ignora los tabuladores iniciales en cada línea del heredoc y en la línea del delimitador final:
if true; then
cat <<- FIN
Esta línea tiene un tabulador al inicio.
El delimitador FIN también.
FIN
fi
Esto te permite indentar heredocs dentro de bloques if, for, funciones, etc., manteniendo el código limpio.
Here string (<<<)
Un here string pasa una cadena como stdin del comando:
grep "error" <<< "$(journalctl -u nginx --since today)"
bc <<< "2 + 3 * 4"
Es equivalente a echo "cadena" | comando, pero sin crear un subshell y más eficiente.
Descriptores personalizados con exec
Hasta ahora hemos redirigido descriptores para un solo comando. Pero exec te permite redirigir para todo el resto del script. Es como cambiar el cableado eléctrico de tu casa: a partir de ese momento, todo lo que antes iba a un sitio ahora va a otro.
exec 3>/archivo — Abrir descriptor de escritura
exec 3> debug.txt
echo "Mensaje normal" # stdout → terminal
echo "Mensaje debug" >&3 # va a debug.txt a través del FD 3
echo "Otro mensaje debug" >&3
exec 3>&- # Cierra el descriptor 3
El descriptor 3 es un tercer canal que apunta a debug.txt. Puedes escribir en él cuando quieras y cerrarlo cuando termines.
exec 3<&0 — Duplicar stdin
Guarda una copia del stdin original antes de redirigirlo:
exec 3<&0 # Guarda stdin original en FD 3
exec < datos.txt # Redirige stdin a archivo
while read linea; do
echo "Leído: $linea"
done
exec 0<&3 # Restaura stdin original
exec 3>&- # Cierra FD 3
Este patrón te viene bien cuando un script necesita leer de un archivo pero luego volver a preguntar al usuario.
exec 5>&1 — Duplicar stdout
Guarda una referencia al stdout original para poder escribir en pantalla incluso después de redirigir todo:
exec 5>&1 # Guarda stdout original en FD 5
exec > log_completo.txt # Todo stdout va ahora al archivo
echo "Esto va al archivo"
echo "Esto también"
# Pero queremos mostrar algo en pantalla
echo "Progreso: 50%" >&5 # Esto sale por terminal gracias a FD 5
exec 1>&5 # Restaura stdout
exec 5>&- # Cierra FD 5
echo "Esto vuelve a la terminal"
Este es el patrón que usan los scripts profesionales que escriben un log completo pero muestran progreso al usuario.
{var}>archivo — Asignación automática de descriptor
En Bash 4.1+, puedes dejar que Bash elija el número de descriptor:
exec {fd}>salida.log
echo "Hola" >&$fd
echo "Mundo" >&$fd
exec {fd}>&- # Cierra el descriptor
echo "Descriptor usado: $fd" # Muestra, ej: 10
Bash asigna el descriptor disponible más bajo (a partir del 10). Es más robusto que elegir números fijos, porque evita colisiones con otros descriptores abiertos.
Archivos especiales: /dev/* y /dev/tcp
/dev/null — El agujero negro
Todo lo que escribes en /dev/null desaparece. Es el sumidero universal:
comando > /dev/null # Descarta stdout
comando 2> /dev/null # Descarta errores
comando &> /dev/null # Descarta todo
Usos típicos: silenciar salidas de comandos en scripts, descartar mensajes de advertencia que no te interesan, o probar solo el código de retorno.
/dev/zero y /dev/urandom
/dev/zero— produce infinitos bytes cero (útil para crear archivos vacíos de tamaño fijo)/dev/urandom— produce bytes aleatorios (no bloqueante, suficiente para la mayoría de usos)
# Crear un archivo de 1 MB lleno de ceros
dd if=/dev/zero of=basura.bin bs=1024 count=1024
# Generar una contraseña aleatoria de 32 caracteres
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32
/dev/tcp/host/puerto — Conexiones TCP nativas
Esto te va a sorprender: Bash puede abrir conexiones TCP directamente, sin necesidad de curl, wget o nc. Lo sé, suena a ciencia ficción, pero funciona. La primera vez que lo vi en un script pensé que era un truco sucio, pero no: es una funcionalidad que viene compilada en Bash si tu distribución la incluye.
Y te puede ahorrar más de un disgusto. ¿Cuántas veces has estado en un servidor minimizado que no tiene curl y necesitas hacer un health check rápido? /dev/tcp al rescate.
# Leer la página principal de google.com
exec 3<>/dev/tcp/google.com/80
echo -e "GET / HTTP/1.0\r\nHost: google.com\r\n\r\n" >&3
cat <&3
exec 3>&-
Esta característica está disponible si Bash se compiló con soporte de red (--enable-net-redirections). En sistemas modernos suele estarlo.
# Health check simple
if exec 3<>/dev/tcp/localhost/8080 2>/dev/null; then
echo "Servidor en puerto 8080 responde"
exec 3>&-
else
echo "Servidor caído o no responde" >&2
fi
/dev/tcp también soporta UDP con /dev/udp/host/puerto.
BASH_XTRACEFD — Trace a un descriptor propio
Cuando activas set -x (modo trace), Bash imprime cada comando antes de ejecutarlo. Por defecto va a stderr (descriptor 2). ¿Y qué problema hay con eso? Pues que si estás debugando un problema de producción, tu stderr se llena de trazas y no ves los errores reales. La primera vez que descubrí BASH_XTRACEFD pensé que era una broma. ¿Poder enviar el trace a un archivo aparte? Sí, existe, y es tan útil como parece.
Con BASH_XTRACEFD puedes redirigir ese trace a cualquier otro descriptor:
#!/bin/bash
# Abrimos un archivo de trace
exec 7> trace_depuracion.log
# Redirigimos el trace al FD 7
BASH_XTRACEFD=7
set -x # Activa el modo trace
# A partir de aquí, todo el trace va a trace_depuracion.log
nombre="Lorenzo"
echo "Hola, $nombre"
for i in 1 2 3; do
echo "Número $i"
done
set +x # Desactiva el modo trace
exec 7>&- # Cerramos el descriptor
Esto es increíblemente útil en producción porque:
- Separa el trace de los errores reales (ambos iban a stderr antes)
- Puedes tener un archivo de depuración gigante sin ensuciar la salida normal
- Si el archivo de trace no existe o hay problemas de permisos, Bash simplemente ignora el trace
Combinado con niveles de verbosidad:
if [[ "${DEBUG:-0}" -ge 2 ]]; then
exec 7> /tmp/${0##*/}_trace.log
BASH_XTRACEFD=7
set -x
fi
Así activas el trace completo solo cuando DEBUG=2, y todo va limpio a su archivo.
Redirigir bucles, funciones y bloques completos
Puedes redirigir la entrada y salida de estructuras enteras, no solo de comandos individuales. Dale caña a esto, que es mucho más elegante que redirigir cada comando por separado.
Redirigir un bucle for
for archivo in *.txt; do
wc -l "$archivo"
done > resumen.txt
Todo el stdout del bucle va a resumen.txt.
Redirigir un while read
while IFS= read -r linea; do
echo "Procesando: $linea"
done < datos.txt > salida.txt 2> errores.txt
datos.txt alimenta stdin, el stdout va a salida.txt, y cualquier error a errores.txt.
Redirigir una función
mi_funcion() {
echo "Inicio de función"
ls /directorio_que_no_existe
echo "Fin de función"
}
mi_funcion > salida.txt 2> errores.txt
La redirección se aplica a todo lo que la función hace internamente. Es una forma limpia de aislar la salida de un bloque lógico.
Bloque { } sin subshell
{
echo "Paso 1"
echo "Paso 2"
echo "Paso 3"
} > bloque.log 2>&1
A diferencia de ( ), las llaves { } no crean un subshell, así que las variables que definas dentro persisten fuera.
Descriptores personalizados para logging
Combinando lo que hemos visto, puedes construir un sistema de logging que use diferentes descriptores para diferentes niveles:
#!/bin/bash
# Configuración de niveles
LOG_LEVEL=${LOG_LEVEL:-info} # Valores: debug, info, warn, error
# Abrimos descriptores personalizados
exec 5> /tmp/script_debug.log # Nivel DEBUG
exec 6> /tmp/script_info.log # Nivel INFO
exec 7> /tmp/script_error.log # Nivel ERROR
# Función de log multi-descriptor
log() {
local nivel="$1"
local mensaje="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$nivel" in
debug)
[[ "$LOG_LEVEL" == "debug" ]] && echo "[$timestamp] [DEBUG] $mensaje" >&5
;;
info)
echo "[$timestamp] [INFO] $mensaje" >&6
echo "[$timestamp] [INFO] $mensaje" # También a stdout
;;
warn)
echo "[$timestamp] [WARN] $mensaje" >&6
echo "[$timestamp] [WARN] $mensaje" >&2
;;
error)
echo "[$timestamp] [ERROR] $mensaje" >&7
echo "[$timestamp] [ERROR] $mensaje" >&2
;;
esac
}
log info "Script iniciado"
log debug "Variable X vale: $X"
# Al final, cerramos descriptores
exec 5>&- 6>&- 7>&-
Cada nivel de log tiene su propio archivo. Los mensajes DEBUG solo aparecen si LOG_LEVEL=debug. Los ERROR van a su archivo dedicado para revisión rápida.
tee con sustitución de procesos
tee parte la salida en dos: una va al archivo, la otra al stdout original. Combinado con la sustitución de procesos (>()), tee se vuelve una navaja suiza:
# Escribir log y comprimir al vuelo
./script_pesado | tee >(gzip -c > salida.log.gz) >(grep "ERROR" > errores.log)
Cada >() es un destino adicional. La salida del script va a:
- stdout (para verla en terminal)
salida.log.gzcomprimidaerrores.logsolo con las líneas que contienen «ERROR»
Otro ejemplo clásico:
# Simular múltiples canales de log con tee y >()
./proceso_largo 2>&1 | tee >(grep -i "^error" > canal_errores.txt) \
>(grep -i "^warn" > canal_warns.txt) \
>(grep -iv "^(error|warn)" > canal_info.txt)
tee con >() es probablemente la forma más expresiva de bifurcar el flujo de datos en Bash.
Funciones profesionales
log_with_level — Log multi-nivel con descriptor dinámico
# Log con nivel de verbosidad y descriptor dinámico
# Uso: log_with_level <nivel> <mensaje>
# Niveles: DEBUG=0, INFO=1, WARN=2, ERROR=3
log_with_level() {
local nivel="$1"
local mensaje="$2"
local verbosity="${VERBOSITY:-1}"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
# Mapa numérico de niveles
case "$nivel" in
DEBUG) (( verbosity >= 0 )) && echo "[$ts] [DEBUG] $mensaje" >&5 ;;
INFO) (( verbosity >= 1 )) && echo "[$ts] [INFO] $mensaje" >&6
(( verbosity >= 1 )) && echo "[$ts] [INFO] $mensaje" ;;
WARN) (( verbosity >= 2 )) && echo "[$ts] [WARN] $mensaje" >&6
(( verbosity >= 2 )) && echo "[$ts] [WARN] $mensaje" >&2 ;;
ERROR) (( verbosity >= 3 )) && echo "[$ts] [ERROR] $mensaje" >&7
(( verbosity >= 3 )) && echo "[$ts] [ERROR] $mensaje" >&2 ;;
*) echo "[$ts] [????] $mensaje" >&2 ;;
esac
}
# Configuración típica
VERBOSITY=2
exec 5>>/tmp/app_debug.log
exec 6>>/tmp/app_info.log
exec 7>>/tmp/app_error.log
log_with_level INFO "Aplicación iniciada"
log_with_level DEBUG "Config cargada: $(cat config.yml | wc -l) líneas"
log_with_level WARN "Espacio en disco bajo: 85%"
log_with_level ERROR "No se pudo conectar a la base de datos"
exec 5>&- 6>&- 7>&-
redirect_block — Ejecuta un bloque de código con redirecciones temporales
# Ejecuta un callback con redirecciones aisladas
# Uso: redirect_block <callback> <stdout_file> <stderr_file>
redirect_block() {
local callback="$1"
local stdout_file="${2:-/dev/null}"
local stderr_file="${3:-/dev/null}"
# Guardamos los descriptores originales
exec 8>&1
exec 9>&2
# Redirigimos para el bloque
exec > "$stdout_file"
exec 2> "$stderr_file"
# Ejecutamos el callback
"$callback"
# Restauramos
exec 1>&8
exec 2>&9
exec 8>&-
exec 9>&-
}
# Uso:
mi_proceso() {
echo "Esto va al archivo"
ls /no/existe
echo "Esto también"
}
redirect_block mi_proceso /tmp/salida.txt /tmp/errores.txt
exec_isolated — Aísla un comando con descriptores limpios
# Ejecuta un comando con su propio juego de descriptores,
# garantizando que no hereda ninguno del padre excepto 0/1/2
# Uso: exec_isolated <comando> [args...]
exec_isolated() {
if [[ $# -eq 0 ]]; then
echo "Uso: exec_isolated <comando> [args...]" >&2
return 1
fi
local cmd=("$@")
# Abrimos /dev/null en descriptores del 3 al 9
# para que el comando hijo no herede nada
local fd
for fd in {3..9}; do
eval "exec ${fd}< /dev/null"
done
# Ejecutamos el comando
"${cmd[@]}"
local exit_code=$?
# Cerramos los descriptores extra
for fd in {3..9}; do
eval "exec ${fd}<&-" 2>/dev/null
done
return $exit_code
}
# Uso: ejecuta un comando sin heredar nuestros FDs internos
# exec_isolated mi_script_hijo.sh
fd_logger — Logger dinámico con socket a descriptor
# Logger que recibe un número de descriptor como argumento
# y escribe en él con formato consistente
# Uso: fd_logger <fd> <severidad> <mensaje>
fd_logger() {
local fd="$1"
local severity="$2"
shift 2
local message="$*"
local ts
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
if [[ ! -e "/proc/$$/fd/$fd" ]]; then
echo "[$ts] [ERROR] [fd_logger] Descriptor $fd no está abierto" >&2
return 1
fi
# Escribimos en el descriptor si está abierto y es válido
echo "[$ts] [${severity}] $message" >&"$fd"
}
# Uso:
exec 5>/tmp/mi_app.log
fd_logger 5 "INFO" "Servicio iniciado"
fd_logger 5 "ERROR" "Fallo de conexión"
exec 5>&-
Script completo: gestor_logs_pro.sh
Este script profesional integra todo lo aprendido. Te muestro la estructura principal para que no te pierdas entre tanto código; el esquema es este:
#!/usr/bin/env bash
set -euo pipefail
VERBOSITY=1
MODO_DEBUG=false
LOG_DIR="/tmp"
_inicializar_descriptores() {
local log_base="${LOG_DIR}/gestor_$$"
exec 5>> "${log_base}_debug.log"
exec 6>> "${log_base}_app.log"
exec 7>> "${log_base}_error.log"
exec 8>&1
exec 9>&2
}
log() {
local nivel="$1"; shift
local ts; ts=$(date '+%Y-%m-%d %H:%M:%S')
case "$nivel" in
DEBUG) (( VERBOSITY <= 0 )) && echo "[$ts] [DEBUG] $*" >&5 ;;
INFO) (( VERBOSITY <= 1 )) && echo "[$ts] [INFO] $*" >&6
(( VERBOSITY <= 1 )) && echo "[$ts] [INFO] $*" ;;
WARN) (( VERBOSITY <= 2 )) && echo "[$ts] [WARN] $*" >&6
(( VERBOSITY <= 2 )) && echo "[$ts] [WARN] $*" >&2 ;;
ERROR) (( VERBOSITY <= 3 )) && echo "[$ts] [ERROR] $*" >&7
(( VERBOSITY <= 3 )) && echo "[$ts] [ERROR] $*" >&2 ;;
FATAL) echo "[$ts] [FATAL] $*" >&7; echo "[$ts] [FATAL] $*" >&2; exit 1 ;;
esac
}
_cleanup() { exec 5>&- 6>&- 7>&- 8>&- 9>&- 2>/dev/null; exit 1; }
trap '_cleanup' INT TERM
main() {
_inicializar_descriptores
log INFO "Script iniciado"
# Aquí va tu lógica
log INFO "Script completado"
}
Cómo usarlo:
chmod +x gestor_logs_pro.sh
# Ejecución normal
./gestor_logs_pro.sh
# Modo DEBUG completo (trace a archivo separado)
./gestor_logs_pro.sh -v 0 -d
# Solo errores
./gestor_logs_pro.sh -v 3
Cada nivel de log tiene su propio archivo y el trace de depuración va aparte.
Errores comunes
Error 1: Confundir >& con >
# ❌ Esto NO envía stderr a archivo
comando >& archivo
# ❌ Esto redirige stderr al stdout ACTUAL (no a archivo)
comando > archivo 2>& # Falta el 1: debe ser 2>&1, no 2>&
# ✅ Formas correctas
comando > archivo 2>&1
comando &> archivo
>& sin número después es ambiguo. Bash interpreta comando >& archivo como comando > archivo 2>&1 solo si archivo no es un número.
⚠️ Atención, que esto es trampa mortal: si el archivo se llama 1, comando >&1 no redirige stdout y stderr a un archivo llamado 1, sino que redirige stdout adonde apunte el descriptor 1 (que suele ser la terminal). No es lo mismo >& archivo que >&1. El primero es un nombre de archivo, el segundo es un descriptor. Y Bash los distingue por el número. Aquí te puedes tirar un buen rato sin entender qué pasa.
Error 2: Cerrar el descriptor equivocado
# ❌ Cierra stdout, no el FD 3
exec 3>&1 # Duplica FD 1 en FD 3
exec 1>&- # CIERRA stdout — el script se queda sin salida
# ✅ Cerrar FD 3 correctamente
exec 3>&-
Regla: &N significa «duplicar desde/hacia el descriptor N». &- significa «cerrar el descriptor».
Error 3: Fuga de descriptores
# ❌ Descriptor abierto pero nunca cerrado
abrir_log() {
exec 5> /tmp/log.txt
}
abrir_log
# ... 500 líneas de código ...
# FD 5 sigue abierto hasta que el script termina
# ✅ Cerrar siempre
abrir_log() {
exec 5> /tmp/log.txt
}
abrir_log
# ... código ...
exec 5>&-
Cada descriptor abierto consume recursos del kernel. En scripts largos o que se ejecutan miles de veces (cron, workers), las fugas se acumulan y pueden agotar el límite de descriptores del proceso (ulimit -n).
Error 4: Conflictos con tabuladores en heredocs
# ❌ Error si mezclas espacios y tabuladores
if true; then
cat <<- EOF
Línea con tabulador
Línea con espacios # Esto NO funciona con <<-
EOF
fi
# ✅ Solución: todos tabuladores o todos espacios
if true; then
cat <<- EOF
Línea con tabulador
Otra línea con tabulador
EOF
fi
<<- solo elimina tabuladores, no espacios. Si tu editor inserta espacios (p.ej. expandtab en vim), el heredoc falla.
Error 5: Asumir que exec > archivo es temporal
# ❌ Después de este exec, TODO stdout va a archivo para SIEMPRE
exec > /tmp/log.txt
echo "Esto va al archivo"
# ... el resto del script no escribe en terminal nunca más
# ✅ Guardar y restaurar
exec 3>&1 # Guardar stdout original
exec > /tmp/log.txt # Redirigir
echo "Esto va al archivo"
exec 1>&3 # Restaurar
exec 3>&- # Cerrar guardado
exec sin comando cambia las redirecciones del shell actual de forma permanente hasta que las cambies de nuevo o el shell termine.
Error 6: Redirigir /dev/tcp sin verificar soporte
# ❌ Si Bash no tiene soporte de red, esto falla
exec 3<>/dev/tcp/localhost/8080
# ✅ Verificar primero
if [[ -r /dev/tcp ]]; then
# Bash con soporte de red — /dev/tcp existe virtualmente
exec 3<>/dev/tcp/localhost/8080 2>/dev/null && {
echo "Conectado" >&3
exec 3>&-
} || echo "No se pudo conectar"
else
echo "Bash sin soporte de red — usa curl o nc" >&2
fi
Error 7: Olvidar redirigir stderr en bucles
# ❌ Solo stdout va al archivo, stderr sale por terminal
for f in *.txt; do
grep "error" "$f"
done > resultados.txt
# ✅ Capturar ambos
for f in *.txt; do
grep "error" "$f"
done > resultados.txt 2> errores.txt
# ✅ O ambos al mismo archivo
for f in *.txt; do
grep "error" "$f"
done &> todo.txt
Error 8: No cerrar heredoc con el delimitador correcto
# ❌ El delimitador debe estar SOLO en la línea, sin espacios
cat << EOF
contenido
EOF # Espacio antes — no se reconoce como fin
# ✅ Correcto
cat << EOF
contenido
EOF
Además, si usas <<-, el delimitador final debe tener exactamente un tabulador al inicio.
Error 9: Espacio entre > y & cambia el significado
# ✅ Esto funciona (sin espacio después de >)
comando 2>&1
# ❌ Esto NO es 2>&1 — es redirigir FD 2 a un archivo llamado "&1"
comando 2> &1 # Error: Bash espera nombre de archivo después de >
La diferencia: 2>&1 es un solo token (redirección con duplicado). 2> &1 es «redirigir stderr a archivo llamado &1«.
Error 10: No redirigir el pipe correctamente con múltiples descriptores
# ❌ El pipe captura solo stdout, stderr se escapa
comando | tee log.txt
# ✅ Capturar ambos en el pipe
comando 2>&1 | tee log.txt
# ❌ No funciona: primero el pipe captura stdout, luego se duplica stderr
comando | tee log.txt 2>&1 # 2>&1 aquí aplica a tee, no a comando
# ✅ Con sustitución de procesos
comando > >(tee log.txt) 2>&1
Tabla resumen
cmd > archivo: Redirigir stdout (sobrescribe)cmd >> archivo: Redirigir stdout (añade)cmd < archivo: Redirigir stdincmd 2> archivo: Redirigir stderrcmd &> archivo: stdout + stderr al mismo archivocmd 2>&1: Duplicar stderr hacia stdoutexec N>&-: Cerrar descriptor Ncmd << EOF ... EOF: Heredoc (texto multilínea como stdin)cmd << 'EOF' ... EOF: Heredoc sin expansióncmd <<- TAB... EOF: Heredoc indentado (ignora tabs iniciales)cmd <<< "texto": Here stringexec N> archivo: Abrir descriptor escrituraexec N< archivo: Abrir descriptor lecturaexec M>&N: Duplicar descriptor (FD M apunta al mismo sitio que FD N)exec {var}>archivo: Descriptor automático (Bash asigna FD >= 10)/dev/tcp/host/puerto: Red TCP nativaBASH_XTRACEFD=N: Trace propio (redirigeset -xa un FD específico)> /dev/null: Descartar salidacmd | tee archivo: Bifurcar salidatee >(cmd): Bifurcar con proceso{ cmds; } > archivo: Redirigir bloque
Resumen
En este capítulo has visto como dominar el flujo de datos en Bash. Desde las redirecciones básicas >, >>, < hasta los descriptores personalizados con exec, pasando por heredocs, /dev/tcp, BASH_XTRACEFD y tee con sustitución de procesos.
Al final, no se trata más que de entender que la entrada y salida de tus scripts no es algo fijo. Puedes redirigirla, bifurcarla, duplicarla, aislarla y combinarla de formas que harán tus scripts mucho más profesionales.
Reglas de oro:
- El orden importa en
2>&1: primero redirige stdout, luego duplica stderr hacia stdout. - Siempre cierra los descriptores que abras para evitar fugas de recursos.
- Usa
{var}>archivoen Bash 4.1+ para evitar colisiones de números. execsin comando es permanente hasta que lo cambies o el shell termine.- Guarda una copia del stdout original (con
exec N>&1) antes de redirigir globalmente.
En el próximo capítulo veremos las señales y traps en Bash. Verás cómo tus scripts pueden reaccionar a eventos externos, limpiar recursos y comportarse como ciudadanos de primera clase en el sistema.
Más información,
- Páginas de manual
- bash(1) — REDIRECTION — Sección completa de redirecciones en Bash
- exec(1p) — El comando exec y sus usos
- tee(1) — Tubería bifurcada a archivos
- pipe(7) — Tuberías en Linux (a bajo nivel)
- open(2) — Llamada al sistema para abrir archivos
- Enlaces