Preparación de un entorno simulado
Para seguir los ejemplos, primero creemos un directorio de prueba con archivos que contengan situaciones comunes y problemáticas.
# Crear directorio de prueba
mkdir -p ~/shell-interview-test
cd ~/shell-interview-test
# Crear archivos con permisos y caracteres especiales
touch archivo_normal.txt
echo "hola mundo" > "archivo con espacios.txt"
echo "linea1" > "archivo_con_barra_invertida\\nombre.txt"
mkdir subdir
touch subdir/.archivo_oculto
chmod 000 archivo_sin_permisos.txt 2>/dev/null || touch archivo_sin_permisos.txt && chmod 000 archivo_sin_permisos.txt
# Verificar el resultado
ls -la
Salida esperada:
total 8
drwxr-xr-x 3 user staff 96 May 21 10:00 .
drwxr-xr-x 5 user staff 160 May 21 10:00 ..
-rw-r--r-- 1 user staff 12 May 21 10:00 archivo con espacios.txt
---------- 1 user staff 0 May 21 10:00 archivo_sin_permisos.txt
-rw-r--r-- 1 user staff 0 May 21 10:00 archivo_normal.txt
drwxr-xr-x 3 user staff 96 May 21 10:00 subdir
Este directorio incluye nombres de archivo con espacios, archivos sin permisos y directorios ocultos, una fuente común de errores.
Trampas básicas (Preguntas 1-5)
Pregunta 1: ¿Por qué falla la asignación de variables?
Problema: Identifica y corrige el error en el siguiente script.
#!/bin/bash
nombre = "Alicia"
echo $nombre
Solución: En Shell, no deben haber espacios en el operador de asignación. Un espacio hace que el intérprete intente ejecutar el nombre como un comando.
# ❌ Incorrecto: tiene espacios
nombre = "Alicia" # Shell ejecutará 'nombre' como un comando
# ✅ Correcto
nombre="Alicia"
echo "$nombre"
Mejor práctica: Siempre citar las variabels con dobles comillas ("$var") para evitar problemas con valores vacíos o que contengan espacios. Usar ${var} en concatenaciones es aún más seguro.
Pregunta 2: ¿Cuál es la diferencia entre [ ] y [[ ]]?
Problema: ¿Este código dará error? ¿Por qué?
#!/bin/bash
var="hola mundo"
if [ $var == "hola mundo" ]; then
echo "Coincide"
fi
Solución: Fallará. [ ] es el comando estándar test y realiza separación de palabras. El valor de $var se divide en dos argumentos, causando un error de sintaxis.
# ❌ Incorrecto
$ var="hola mundo"
$ if [ $var == "hola mundo" ]; then echo "Coincide"; fi
bash: [: too many arguments
# ✅ Correcto: citar la variable
$ if [ "$var" == "hola mundo" ]; then echo "Coincide"; fi
Coincide
# ✅ Más recomendado: usar [[ ]] (integrado en bash, sin separación de palabras)
$ if [[ $var == "hola mundo" ]]; then echo "Coincide"; fi
Coincide
Mejor práctica: Preferir [[ ]] en scripts bash por ser más seguro y conciso.
Pregunta 3: La sutil diferencia entre $@ y $*
Problema: Explica la diferencia entre $@ y $* y cuándo usar cada uno.
Solución: La diferencia clave está en cómo manejan los argumentos cuando están entre comillas.
#!/bin/bash
# probar_args.sh
echo "=== Sin comillas ==="
echo 'Sin comillas $@:'
for arg in $@; do echo " [$arg]"; done
echo "=== Con comillas ==="
echo '"$@" (cada argumento intacto):'
for arg in "$@"; do echo " [$arg]"; done
echo '"$*" (un solo argumento combinado):'
for arg in "$*"; do echo " [$arg]"; done
$ ./probar_args.sh "arg uno" "arg dos" "arg tres"
=== Sin comillas ===
Sin comillas $@:
[arg]
[uno]
[arg]
[dos]
[arg]
[tres]
=== Con comillas ===
"$@" (cada argumento intacto):
[arg uno]
[arg dos]
[arg tres]
"$*" (un solo argumento combinado):
[arg uno arg dos arg tres]
Mejor práctica: Para preservar espacios en los argumentos, usar siempre "$@".
Pregunta 4: Diferencia entre source y ejecutar un script directamente
Problema: ¿Cuál es la diferencia entre source script.sh y ./script.sh?
Solución: source (o .) ejecuta los comandos en el Shell actual, modificando su entorno. Ejecutar con ./ crea un nuevo subrpoceso, por lo que los cambios (como exportar variables) no afectan al Shell padre.
# Preparar un script
cat > test_entorno.sh << 'EOF'
#!/bin/bash
export MI_VARIABLE="hola desde el script"
echo "Script: MI_VARIABLE=$MI_VARIABLE"
EOF
chmod +x test_entorno.sh
# Ejecución directa
$ ./test_entorno.sh
Script: MI_VARIABLE=hola desde el script
$ echo $MI_VARIABLE
# ¡Vacío! La variable no persiste en el Shell actual.
# Ejecución con source
$ source test_entorno.sh
Script: MI_VARIABLE=hola desde el script
$ echo $MI_VARIABLE
hola desde el script # La variable persiste.
Mejor práctica: Usar source para cargar configuraciones o activar entornos virtuales. Usar ejecución directa para tareas independientes.
Pregunta 5: Consecuencias de usar exit o return incorrectamente
Problema: ¿Qué pasa si usas exit dentro de una función en un script?
Solución: exit termina todo el script (el proceso). return solo sale de la función y devuelve un código de estado.
#!/bin/bash
function probar_exit() {
echo "Entrando a la función"
exit 1 # 🚨 Termina todo el script aquí
echo "Esta línea nunca se ejecuta"
}
function probar_return() {
echo "Entrando a la función"
return 1 # ✅ Solo sale de la función
echo "Esta línea tampoco se ejecuta"
}
echo "=== Probando return ==="
probar_return
echo "Código de salida de return: $?"
echo "¡El script continúa!"
echo ""
echo "=== Probando exit ==="
probar_exit
echo "Esta línea nunca se verá"
Mejor práctica:
- Dentro de funciones, usar
return Npara indicar éxito (0) o fracaso (no 0). - Para terminar el script completo, usar
exit N.
Procesamiento de texto (Preguntas 6-9)
Pregunta 6: ¿Cómo leer un archivo línea a línea de forma segura?
Problema: Escribe un script que procese cada línea de un archivo de log, preservando espacios y caracteres especiales.
Solución: El bucle for con cat falla porque separa palabras y expande globos. La solución es un bucle while read con el parámetro -r y IFS definido.
# Crear archivo de prueba con trampas
cat > test_log.txt << 'EOF'
Primera línea normal
Segunda línea con espacios múltiples
Tercera línea con espacios al inicio y final
Cuarta línea con barra invertida al final\
Quinta línea con símbolo $variable
Sexta línea
Séptima línea con tabulación aquí
EOF
# ❌ Método incorrecto (bucle for)
echo "=== Método for (INCORRECTO) ==="
for linea in $(cat test_log.txt); do
echo "[$linea]"
done
# ✅ Método correcto (bucle while)
echo "=== Método while (CORRECTO) ==="
while IFS= read -r linea; do
echo "[$linea]"
done < test_log.txt
Mejor práctica: Para leer líneas de forma robusta, usar siempre while IFS= read -r line; do ... done < archivo.
Pregunta 7: Contar accesos por IP en un archivo de log
Problema: Dado un archivo access.log con formato de servidor web, muestra las 5 IPs con más accesos.
Solución: Encadenar comandos de procesamiento de flujo para eficiencia con archivos grandes.
# Crear datos de prueba
cat > access.log << 'EOF'
192.168.1.1 - - [21/May/2026:10:00:00] "GET /index.html" 200 1024
10.0.0.5 - - [21/May/2026:10:00:01] "POST /api/login" 200 512
192.168.1.1 - - [21/May/2026:10:00:02] "GET /style.css" 200 2048
172.16.0.8 - - [21/May/2026:10:00:03] "GET /index.html" 304 0
10.0.0.5 - - [21/May/2026:10:00:04] "GET /api/data" 500 128
EOF
# Solución con awk, sort, uniq
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5
Explicación de la tubería:
awk '{print $1}': Extrae el primer campo (IP).sort: Ordena las IPs.uniq -c: Cuenta líneas consecutivas iguales.sort -rn: Ordena numéricamente en orden inverso.head -5: Muestra las 5 primeras líneas.
Mejor práctica: Para grandes volúmenes de datos, preferir el procesamiento en flujo (awk, sed) sobre los bucles en Shell, ya que la utilización de memoria es constante.
Pregunta 8: Renombrado masivo de archivos - trampas en los bucles for
Problema: Renombra todos los archivos .txt en el directorio actual añadiéndoles el prefijo backup_.
Solución: Iterar directamente con el patrón glob en el bucle for, no con la salida de ls.
# Crear archivos de prueba
touch archivo1.txt archivo2.txt "archivo con espacios.txt" "archivo*.txt"
# ❌ Método incorrecto (subshell y palabra splitting)
for f in $(ls *.txt); do
mv "$f" "backup_$f" # Falla con espacios en nombres
done
# ✅ Método correcto
for f in *.txt; do
[[ -e "$f" ]] || continue # Salir si no hay coincidencia
mv "$f" "backup_$f"
echo "Renombrado: $f -> backup_$f"
done
Trampa clave: $(ls *.txt) somete los nombres de archivo a separación de palabras y expansión de globos, rompiendo los nombres con espacios o caracteres especiales.
Pregunta 9: Diferencia fundamental entre sed y awk
Problema: Explica la diferencia esencial entre sed y awk y da un ejemplo para cada uno.
Solución:
sed(Stream Editor): Está optimizado para operaciones a nivel de línea (sustituir, eliminar, insertar texto).awk: Es un lenguaje de procesamiento de texto completo, excelente para operaciones a nivel de campo (columna) y con lógica de programación.
# Datos de prueba
cat > datos.txt << 'EOF'
Ana 85 92 78
Luis 90 88 95
María 76 82 70
EOF
# Ejemplo con sed: sustituir un nombre
$ sed 's/Ana/Ana García/' datos.txt
Ana García 85 92 78
Luis 90 88 95
María 76 82 70
# Ejemplo con awk: calcular promedios
$ awk '{promedio=($2+$3+$3)/3; printf "%-6s Promedio: %.1f\n", $1, promedio}' datos.txt
Ana Promedio: 85.0
Luis Promedio: 91.0
María Promedio: 76.0
Regla general: Usa sed para ediciones simples a nivel de línea. Usa awk cuando necesites trabajar con campos, hacer cálculos o tener lógica condicional más compleja.
Prácticas avanzadas (Preguntas 10-14)
Pregunta 10: Alcance de variables en tuberías
Problema: ¿Por qué count permanece en 0 al final de este script?
#!/bin/bash
count=0
cat /etc/passwd | while read line; do
count=$((count + 1))
done
echo "Total de líneas: $count" # Imprime 0
Solución: La parte derecha de una tubería (|) se ejecuta en un subshell. Las modificaciones a variables dentro de él no afectan al shell padre.
Soluciones:
# Opción 1: Redirección (más simple)
count=0
while read line; do
count=$((count + 1))
done < /etc/passwd
echo "Opción 1: $count"
# Opción 2: Sustitución de proceso
count=0
while read line; do
count=$((count + 1))
done < <(cat /etc/passwd)
echo "Opción 2: $count"
Mejor práctica: Evitar tuberías cuando se necesite modificar variables en el shell principal. Usar redirección (<) o sustitución de proceso (< <(...)).
Pregunta 11: La "trampa oculta" de set -e
Problema: ¿Por qué set -e no hace que el script salga en todos los errores?
Solución: set -e tiene excepciones importantes:
- Ignora comandos que no son el último en una tubería.
- Ignora comandos en la parte condicional de un
ifowhile. - Ignora comandos en listas con
||o&&(salvo el último).
#!/bin/bash
set -e
# Escenario 1: El error en la tubería se ignora.
false | true
echo "Escenario 1: Esto SÍ se ejecuta."
# Escenario 2: Error en condición no aborta el script.
if grep "patrón_inexistente" /etc/passwd; then
echo "Encontrado"
else
echo "No encontrado (grep falló, pero el script continúa)"
fi
echo "Escenario 2: Esto SÍ se ejecuta."
Mejor práctica: Para un control de errores estricto, usar set -euo pipefail. La opción -o pipefail hace que una tubería falle si cualquier comando (no solo el último) devuelve un código de error.
Pregunta 12: Implementación de control de concurrencia en Shell
Problema: Ejecuta 10 tareas en paralelo, pero como máximo 3 a la vez.
Solución: Usar procesos en segundo plano y un mecanismo para esperar a que se libere una ranura.
#!/bin/bash
MAX_TAREAS=3
ejecutando=0
tarea() {
local id=$1
echo "[$(date +%H:%M:%S)] Tarea $id iniciada"
sleep $((RANDOM % 5 + 1)) # Simular trabajo
echo "[$(date +%H:%M:%S)] Tarea $id completada"
}
for i in {1..10}; do
tarea "$i" & # Ejecutar en segundo plano
ejecutando=$((ejecutando + 1))
if [[ $ejecutando -ge $MAX_TAREAS ]]; then
wait -n # Esperar a que termine CUALQUIER tarea en segundo plano
ejecutando=$((ejecutando - 1))
fi
done
wait # Esperar a las tareas restantes
echo "¡Todas las tareas finalizadas!"
Método avanzado (más simple): Usar xargs con la opción -P para el paralelismo.
seq 1 10 | xargs -P 3 -I {} bash -c 'echo "Tarea {} iniciada"; sleep 2; echo "Tarea {} finalizada"'
Pregunta 13: Manejo de señales y limpieza en scripts
Problema: ¿Cómo asegurar que un script limpie archivos temporales si es interrumpido con Ctrl+C?
Solución: Usar el comando trap para capturar señales (como SIGINT, SIGTERM) y ejecutar una función de limpieza.
#!/bin/bash
# Crear archivos temporales
ARCHIVO_TMP=$(mktemp /tmp/miscript.XXXXXX)
DIR_TMP=$(mktemp -d /tmp/miscript_dir.XXXXXX)
# Definir función de limpieza
limpiar() {
local codigo_salida=$?
echo ""
echo ">>> Limpiando..."
rm -f "$ARCHIVO_TMP"
rm -rf "$DIR_TMP"
echo ">>> Limpieza completada."
exit $codigo_salida
}
# Asignar la función de limpieza a señales de salida
trap limpiar EXIT SIGINT SIGTERM
echo "Iniciando trabajo... (Presiona Ctrl+C para probar)"
for i in {1..10}; do
echo " Procesando $i/10..."
sleep 1
done
echo "¡Trabajo finalizado!"
Mejor práctica: Cualquier script que cree archivos o directorios temporales debe usar trap limpiar EXIT para garantizar una ejecución segura.
Pregunta 14: Procesamiento de archivos grandes y gestión de memoria
Problema: ¿Por qué un script que procesa un archivo de 2GB podría consumir más de 5GB de RAM?
Solución: Generalmente es porque se carga todo el contenido del archivo en una variable de Shell.
#!/bin/bash
# ❌ Método ineficiente: carga todo en memoria
contenido=$(cat archivo_grande.log) # ¡PELIGRO! Memoria = Tamaño del archivo
for linea in $contenido; do
echo "$linea" | grep "200" > /dev/null
done
# ✅ Método eficiente: procesamiento en flujo
awk '/200/ {print $0}' archivo_grande.log > /dev/null # Memoria constante y baja
Principio fundamental: Nunca almacenar grandes conjuntos de datos en variables de Shell. Usar procesamiento en flujo con herramientas como awk, sed, grep o bucles while read con redirección. Esto mantiene la utilización de memoria constante e independiente del tamaño del archivo.
Trampas de alto nivel (Preguntas 15-17)
Pregunta 15: El desastre de un rm -rf mal escrito
Problema: ¿Cómo prevenir una catástrofe como rm -rf / causada por una variable vacía?
Solución: Implementar verificaciones de seguridad estrictas en cualquier operación destructiva.
#!/bin/bash
DIRECTORIO_APP="" # Supuestamente debería ser "/opt/miapp"
# 🚨 Si la variable está vacía, esto equivale a: rm -rf /*
# rm -rf "$DIRECTORIO_APP"/* # ¡NUNCA EJECUTAR ESTO!
# ✅ Práctica segura: verificar la variable antes de actuar
limpiar_directorio() {
local dir="$1"
# Verificaciones de seguridad
if [[ -z "$dir" ]]; then
echo "ERROR: ¡La variable del directorio está vacía!" >&2
return 1
fi
if [[ ! -d "$dir" ]]; then
echo "ERROR: $dir no es un directorio." >&2
return 1
fi
# Protección adicional: prohibir la eliminación de directorios críticos
case "$(realpath "$dir")" in
/|/root|/home|/etc|/usr|/bin|/sbin)
echo "ERROR CRÍTICO: ¡Se niega la operación en un directorio del sistema!" >&2
return 1
;;
esac
echo "Directorio seguro para limpiar: $dir"
# Comando real de limpieza (comentado por seguridad)
# rm -rf "$dir"/*
}
limpiar_directorio "$DIRECTORIO_APP"
Checklist de programación defensiva:
- Siempre verificar si las variables están vacías antes de usarlas en comandos destructivos.
- Proteger explícitamente los directorios raíz del sistema.
- Considerar usar el indicador
--no-preserve-rootderm(aunque no es una solución completa). - En producción, considerar usar
trash-clien lugar derm.
Pregunta 16: La potencia oculta de las expansiones de parámetros ${}
Problema: Explica el significado de estas sintaxis: ${var:-default}, ${var:=valor}, ${var:?error}.
Solución: Son formas de manejar valores por defecto y errores en las variables.
#!/bin/bash
# 1. ${var:-palabra} — Usar valor por defecto si la variable está vacía o no definida (no la modifica)
unset MI_VAR
echo "${MI_VAR:-valor por defecto}" # Imprime: valor por defecto
echo "Variable original: [$MI_VAR]" # Sigue vacía
# 2. ${var:=palabra} — Igual que el anterior, pero TAMBIÉN asigna el valor a la variable
unset MI_VAR
echo "${MI_VAR:=valor nuevo}" # Imprime: valor nuevo
echo "Variable original: [$MI_VAR]" # Ahora es: valor nuevo
# 3. ${var:?mensaje} — Si la variable está vacía/no definida, imprime el mensaje y sale del script
unset MI_VAR
# echo "${MI_VAR:?¡ERROR! La variable es obligatoria}" # Descomentar aborta el script
Expansión para manipulación de cadenas:
#!/bin/bash
ARCHIVO="backup_2026-05-21.tar.gz"
echo "Nombre original: $ARCHIVO"
# Eliminar el prefijo más corto que coincida con el patrón
echo "Sin prefijo 'backup_': ${ARCHIVO#backup_}" # 2026-05-21.tar.gz
# Eliminar el sufijo más corto
echo "Sin sufijo '.tar.gz': ${ARCHIVO%.tar.gz}" # backup_2026-05-21
# Eliminar el prefijo más largo (codicioso)
echo "Prefijo más largo eliminado: ${ARCHIVO##*_}" # 21.tar.gz
# Eliminar el sufijo más largo (codicioso)
echo "Sufijo más largo eliminado: ${ARCHIVO%%.*}" # backup_2026-05-21
Pregunta 17: Las "seudo-señales" de Shell: DEBUG y ERR
Problema: ¿Para qué sirven las seudo-señales DEBUG y ERR en el comando trap?
Solución: Son eventos que el propio Shell genera para la depuración y el manejo de errores.
#!/bin/bash
# Seudo-señal DEBUG: se dispara ANTES de ejecutar cada comando simple
trap 'echo "DEBUG: Voy a ejecutar la línea $LINENO"' DEBUG
x=10
y=20
z=$((x + y))
trap - DEBUG # Desactivar el trap DEBUG
echo ""
# Seudo-señal ERR: se dispara cuando un comando falla (salida no cero)
trap 'echo "ERR: Falló en la línea $LINENO con código $?"' ERR
ls /directorio_inexistente # Esto dispara ERR
echo "Esta línea SÍ se ejecuta (ERR no aborta por defecto)"
trap - ERR
# Combinación para registro de errores
trap 'echo "[ERROR] Línea $LINENO falló con código $?"' ERR
trap 'echo "[EXIT] El script ha finalizado."' EXIT
Usos comunes:
- DEBUG: Para registro detallado (logging) durante la ejecución, sin tener que poner
echoen cada línea. - ERR: Para realizar acciones de limpieza o notificación cuando ocurre cualquier error, sin necesidad de usar
set -e.
Ejercicios de entrevistas reales (Preguntas 18-20)
Pregunta 18: Script de verificación de salud HTTP con reintentos
Problema: Crea un script que haga ping a una URL, con reintentos configurables, y devuelva un código de salida claro.
Solución:
#!/bin/bash
# verificacion_salud.sh
set -euo pipefail
URL="${1:-http://localhost:8080/health}"
MAX_REINTENTOS="${2:-3}"
INTERVALO="${3:-2}"
TIMEOUT="${4:-5}"
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }
log_info "Iniciando verificación de salud para: $URL"
log_info "Parámetros: Reintentos=$MAX_REINTENTOS, Intervalo=${INTERVALO}s, Timeout=${TIMEOUT}s"
intento=0
while [[ $intento -lt $MAX_REINTENTOS ]]; do
intento=$((intento + 1))
log_info "Intento $intento/$MAX_REINTENTOS..."
# Ejecutar la verificación con curl
CODIGO_HTTP=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout "$TIMEOUT" \
--max-time "$TIMEOUT" \
"$URL" 2>/dev/null || echo "000")
log_info "Código HTTP recibido: $CODIGO_HTTP"
if [[ "$CODIGO_HTTP" == "200" ]]; then
log_info "✅ Verificación exitosa."
exit 0
fi
if [[ $intento -lt $MAX_REINTENTOS ]]; then
log_info "Esperando ${INTERVALO}s para reintentar..."
sleep "$INTERVALO"
fi
done
log_error "❌ Verificación fallida después de $MAX_REINTENTOS intentos."
exit 1
Aspectos clave evaluados: Uso correcto de curl, bucle con reintentos, códigos de salida estándar, registro con marcas de tiempo.
Pregunta 19: Limpieza y archivo de logs antiguos
Problema: Escribe un script que encuentre y archive (comprimiéndolos) los archivos .log de más de 30 días en un directorio dado.
Solución:
#!/bin/bash
# limpiar_logs.sh
set -euo pipefail
DIR_LOGS="${1:-/var/log/miapp}"
DIAS_RETENCION="${2:-30}"
DIR_ARCHIVE="${DIR_LOGS}/archive"
# Verificaciones de seguridad iniciales
if [[ ! -d "$DIR_LOGS" ]]; then
echo "ERROR: El directorio $DIR_LOGS no existe." >&2
exit 1
fi
# Bloquear directorios críticos del sistema
case "$(realpath "$DIR_LOGS")" in
/|/var|/var/log|/etc|/bin|/usr|/root|/home)
echo "ERROR CRÍTICO: Operación bloqueada para directorio del sistema." >&2
exit 1
;;
esac
echo "Buscando archivos .log con más de $DIAS_RETENCION días en $DIR_LOGS..."
ARCHIVOS_ANTIGUOS=$(find "$DIR_LOGS" -maxdepth 1 -name "*.log" -mtime "+${DIAS_RETENCION}" -type f)
if [[ -z "$ARCHIVOS_ANTIGUOS" ]]; then
echo "No se encontraron archivos antiguos para archivar."
exit 0
fi
mkdir -p "$DIR_ARCHIVE"
echo "$ARCHIVOS_ANTIGUOS" | while IFS= read -r archivo; do
[[ -z "$archivo" ]] && continue
NOMBRE_BASE=$(basename "$archivo")
NOMBRE_ARCHIVE="${NOMBRE_BASE}.$(date '+%Y%m%d').gz"
echo " Archivando: $NOMBRE_BASE -> $NOMBRE_ARCHIVE"
gzip -c "$archivo" > "$DIR_ARCHIVE/$NOMBRE_ARCHIVE"
echo " Eliminando original: $NOMBRE_BASE"
rm -f "$archivo"
done
echo "Proceso completado. Archivos en $DIR_ARCHIVE:"
ls -lh "$DIR_ARCHIVE"
Aspectos clave evaluados: Uso seguro de find con -mtime, protección contra operaciones en directorios críticos, flujo de trabajo de archivado.
Pregunta 20: Monitor de procesos con reinicio automático
Problema: Crea un script que supervise un proceso y lo reinicie si deja de ejecutarse, con protección contra bucles infinitos de reinicio.
Solución:
#!/bin/bash
# monitor_proceso.sh
set -euo pipefail
NOMBRE_PROCESO="${1:-}"
COMANDO_INICIO="${2:-}"
INTERVALO="${3:-5}"
MAX_REINICIOS=5
VENTANA_TIEMPO=60 # segundos
if [[ -z "$NOMBRE_PROCESO" || -z "$COMANDO_INICIO" ]]; then
echo "Uso: $0 <nombre_proceso> <comando_inicio> [intervalo_seg]" >&2
exit 1
fi
archivo_log="/tmp/monitor_${NOMBRE_PROCESO}.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$archivo_log"
}
en_ejecucion() {
pgrep -x "$NOMBRE_PROCESO" > /dev/null 2>&1
}
# Inicializar registro de reinicios
reinicios=()
log "=== Monitor de procesos iniciado ==="
log "Proceso: $NOMBRE_PROCESO"
log "Comando de inicio: $COMANDO_INICIO"
log "Intervalo de verificación: ${INTERVALO}s"
while true; do
if en_ejecucion; then
pid=$(pgrep -x "$NOMBRE_PROCESO" | head -1)
log "[OK] $NOMBRE_PROCESO está corriendo (PID: $pid)."
else
log "[FALLO] ¡$NOMBRE_PROCESO no está corriendo!"
# Verificar frecuencia de reinicios para evitar bucles
ahora=$(date +%s)
# Limpiar reinicios fuera de la ventana de tiempo
reinicios=($(for t in "${reinicios[@]}"; do
[[ $t -gt $((ahora - VENTANA_TIEMPO)) ]] && echo "$t"
done))
if [[ ${#reinicios[@]} -ge $MAX_REINICIOS ]]; then
log "[FATAL] Se ha reiniciado ${#reinicios[@]} veces en los últimos ${VENTANA_TIEMPO}s. Deteniendo monitor."
exit 1
fi
log "[ACCIÓN] Intentando reiniciar: $COMANDO_INICIO"
if eval "$COMANDO_INICIO"; then
reinicios+=("$ahora")
log "[OK] Reinicio iniciado."
else
log "[ERROR] El comando de inicio falló."
fi
fi
sleep "$INTERVALO"
done</comando_inicio></nombre_proceso>
Aspectos clave evaluados: Uso de pgrep para detección de procesos, mecanismo de anti-throttling (evitar reinicios rápidos infinitos), registro de eventos, ejecución segura de comandos con eval.
Metodología de depuración para Shell
Una buena respuesta sobre depuración debería incluir:
- Modo verbose/trazas: Usar
bash -x script.shpara imprimir cada comando antes de ejecutarlo. - Comprobación estática: Usar herramientas como
shellcheckpara detectar errores comunes y malas prácticas sin ejecutar el script. - Mensajes de error detallados: Configurar
trapcon la seudo-señalERRpara reportar la línea exacta donde ocurrió el error. - Cabecera estándar: Usar siempre una cabecera robusta.
#!/bin/bash
set -euo pipefail
IFS=$'\n\t' # Cambiar el separador de campos por defecto para evitar problemas con espacios
trap 'echo "[ERROR] Fallo en la línea $LINENO, código de salida $?" >&2' ERR
Resumen de conceptos clave para entrevistas
- Asignación de variables: Sin espacios alrededor de
=. - Pruebas condicionales: Preferir
[[ ]]sobre[ ]. - Argumentos posicionales: Usar siempre
"$@"para preservarlos. sourcevs ejecución: Diferencia en el alcance de variables.exitvsreturn: Alcance en funciones.- Leer archivos: Usar
while IFS= read -r. - Procesamiento de texto:
awkpara campos,sedpara líneas. - Renombrado masivo: Iterar con globos, no con
ls. - Tuberías y variables: Las tuberías crean subshells.
set -e: Tiene excepciones; usar-o pipefailpara mayor estricticidad.- Concurrencia: Usar procesos en segundo plano y
wait, oxargs -P. - Manejo de señales: Usar
trappara limpieza segura. - Archivos grandes: Procesamiento en flujo, nunca en memoria completa.
rm -rfseguro: Verificar variables, proteger directorios críticos.- Expansiones de parámetros:
${}para valores por defecto y manipulación de cadenas. - Seudo-señales:
DEBUGyERRpara depuración avanzada. - Reintentos: Implementar bucles con límites y esperas.
- Limpieza de logs: Usar
find -mtimecon protecciones. - Monitoreo de procesos:
pgrepy lógica anti-throttling. - Depuración:
bash -x,shellcheck,trap ERR.