20 preguntas avanzadas de Shell que los entrevistadores de grandes empresas adoran hacer

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 N para 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:

  1. awk '{print $1}': Extrae el primer campo (IP).
  2. sort: Ordena las IPs.
  3. uniq -c: Cuenta líneas consecutivas iguales.
  4. sort -rn: Ordena numéricamente en orden inverso.
  5. 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:

  1. Ignora comandos que no son el último en una tubería.
  2. Ignora comandos en la parte condicional de un if o while.
  3. 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-root de rm (aunque no es una solución completa).
  • En producción, considerar usar trash-cli en lugar de rm.

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 echo en 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:

  1. Modo verbose/trazas: Usar bash -x script.sh para imprimir cada comando antes de ejecutarlo.
  2. Comprobación estática: Usar herramientas como shellcheck para detectar errores comunes y malas prácticas sin ejecutar el script.
  3. Mensajes de error detallados: Configurar trap con la seudo-señal ERR para reportar la línea exacta donde ocurrió el error.
  4. 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

  1. Asignación de variables: Sin espacios alrededor de =.
  2. Pruebas condicionales: Preferir [[ ]] sobre [ ].
  3. Argumentos posicionales: Usar siempre "$@" para preservarlos.
  4. source vs ejecución: Diferencia en el alcance de variables.
  5. exit vs return: Alcance en funciones.
  6. Leer archivos: Usar while IFS= read -r.
  7. Procesamiento de texto: awk para campos, sed para líneas.
  8. Renombrado masivo: Iterar con globos, no con ls.
  9. Tuberías y variables: Las tuberías crean subshells.
  10. set -e: Tiene excepciones; usar -o pipefail para mayor estricticidad.
  11. Concurrencia: Usar procesos en segundo plano y wait, o xargs -P.
  12. Manejo de señales: Usar trap para limpieza segura.
  13. Archivos grandes: Procesamiento en flujo, nunca en memoria completa.
  14. rm -rf seguro: Verificar variables, proteger directorios críticos.
  15. Expansiones de parámetros: ${} para valores por defecto y manipulación de cadenas.
  16. Seudo-señales: DEBUG y ERR para depuración avanzada.
  17. Reintentos: Implementar bucles con límites y esperas.
  18. Limpieza de logs: Usar find -mtime con protecciones.
  19. Monitoreo de procesos: pgrep y lógica anti-throttling.
  20. Depuración: bash -x, shellcheck, trap ERR.

Etiquetas: bash shell-scripting linux unix AWK

Publicado el 6-17 03:08