BASH SCRIPTING PARA SYSADMINS Terminal $ #!/bin/bash $ set -euo pipefail $ echo "automatizar" Variables NAME="value" $1, $2, $@ Arrays, Strings Logica if / elif / else for / while Funciones, Traps Scripts Monitor Disco Limpieza Logs Backup, Auditoria Cron 0 2 * * * Programado Automatizado Del primer script a la administracion de servidores totalmente automatizada

Si administras servidores Linux, Bash scripting no es opcional — es una habilidad fundamental. Cada tarea repetitiva que realizas manualmente es candidata a ser automatizada: verificar espacio en disco, rotar logs, crear respaldos, auditar cuentas de usuario, monitorear servicios. Un script de Bash bien escrito ejecuta en segundos lo que te tomaria minutos (u horas) de escribir y hacer clic. Esta guia te ensena Bash scripting desde cero, con enfoque en tareas practicas de sysadmin que puedes poner a trabajar inmediatamente.

Al final de este articulo, sabras como escribir scripts que usan variables, condicionales, bucles y funciones. Entenderas patrones de manejo de errores que previenen que los scripts causen danos cuando algo sale mal. Y tendras cuatro scripts completos, listos para produccion, que resuelven problemas reales de administracion de sistemas.


Por que Bash Scripting para Sysadmins?

Bash es el shell predeterminado en practicamente todas las distribuciones Linux. Viene preinstalado en Ubuntu, Debian, CentOS, RHEL, Fedora, Arch e incluso macOS (aunque macOS cambio a zsh como shell interactivo predeterminado, Bash sigue disponible). Esto significa que tus scripts se ejecutaran en cualquier servidor sin instalar software adicional.

Esto es lo que Bash scripting te ofrece:

  • Automatizacion: Convierte cualquier secuencia de comandos de terminal en un script repetible
  • Consistencia: Los scripts se ejecutan de la misma manera cada vez, eliminando el error humano
  • Velocidad: Un script puede procesar miles de archivos o usuarios en segundos
  • Programacion: Combinado con cron, los scripts se ejecutan automaticamente a cualquier intervalo
  • Portabilidad: Los scripts de Bash se ejecutan en cualquier sistema Linux sin dependencias
  • Auditabilidad: Los scripts sirven como documentacion de exactamente lo que se hizo y cuando

Cuando usar Bash vs. Python: Bash es ideal para tareas que encadenan herramientas de linea de comandos existentes (operaciones con archivos, procesamiento de texto, gestion de servicios). Si tu tarea requiere estructuras de datos complejas, llamadas a APIs o procesamiento de JSON/YAML, considera Python en su lugar.

Requisitos previos

Antes de comenzar, asegurate de tener:

  • Un sistema Linux (Ubuntu 22.04 o 24.04 recomendado, pero cualquier distribucion funciona)
  • Acceso a terminal con una cuenta de usuario regular que tenga privilegios sudo
  • Un editor de texto (nano para principiantes, vim o VS Code para usuarios mas avanzados)
  • Familiaridad basica con la linea de comandos de Linux (navegar directorios, listar archivos, leer contenido de archivos)

Verifica tu version de Bash:

bash --version

Deberias ver la version 4.0 o mas reciente. Ubuntu 22.04 viene con Bash 5.1 y Ubuntu 24.04 con Bash 5.2.

Tu primer script

Todo script de Bash comienza con una linea shebang que le indica al sistema que interprete usar. Crea un archivo llamado hola.sh:

#!/bin/bash
# Mi primer script de Bash
echo "Hola, $(whoami)! Hoy es $(date +%A), $(date +%B\ %d,\ %Y)."
echo "Estas ejecutando Bash version: $BASH_VERSION"
echo "Tu directorio home es: $HOME"

Hazlo ejecutable y ejecutalo:

chmod +x hola.sh
./hola.sh

Salida:

Hola, jc! Hoy es lunes, enero 27, 2026.
Estas ejecutando Bash version: 5.2.21(1)-release
Tu directorio home es: /home/jc

Conceptos clave:

  • #!/bin/bash — el shebang le dice a Linux que use Bash para interpretar este archivo
  • chmod +x — establece el permiso de ejecucion para que puedas ejecutar el script directamente
  • $(comando) — sustitucion de comando: ejecuta el comando e inserta su salida
  • $VARIABLE — expansion de variable: inserta el valor de la variable

Buena practica: Siempre usa #!/bin/bash (no #!/bin/sh) cuando tu script use caracteristicas especificas de Bash como arrays, pruebas [[ ]] o expansion de llaves {1..10}. Si necesitas maxima portabilidad entre diferentes shells, escribe scripts sh compatibles con POSIX.

Variables y tipos de datos

Las variables de Bash almacenan cadenas de texto por defecto. No hay tipos separados de enteros, decimales o booleanos — todo es una cadena que puede interpretarse como numero en contextos aritmeticos.

Definir variables

#!/bin/bash

# Variables de cadena (sin espacios alrededor del =)
HOSTNAME="webserver01"
BACKUP_DIR="/var/backups"
LOG_FILE="/var/log/myscript.log"

# Aritmetica entera usando $(( ))
MAX_RETRIES=5
CURRENT_RETRY=0
TOTAL=$((MAX_RETRIES - CURRENT_RETRY))

# Sustitucion de comandos
CURRENT_DATE=$(date +%Y-%m-%d)
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}')
UPTIME=$(uptime -p)

echo "Servidor: $HOSTNAME"
echo "Directorio de respaldos: $BACKUP_DIR"
echo "Fecha: $CURRENT_DATE"
echo "Uso de disco: $DISK_USAGE"
echo "Tiempo activo: $UPTIME"
echo "Reintentos restantes: $TOTAL"

Variables especiales

#!/bin/bash

echo "Nombre del script: $0"
echo "Primer argumento: $1"
echo "Segundo argumento: $2"
echo "Todos los argumentos: $@"
echo "Numero de argumentos: $#"
echo "Estado de salida del ultimo comando: $?"
echo "ID de proceso de este script: $$"
echo "ID de proceso del ultimo comando en segundo plano: $!"

Arrays

#!/bin/bash

# Array indexado
SERVERS=("web01" "web02" "db01" "db02" "cache01")

echo "Primer servidor: ${SERVERS[0]}"
echo "Todos los servidores: ${SERVERS[@]}"
echo "Numero de servidores: ${#SERVERS[@]}"

# Recorrer el array
for server in "${SERVERS[@]}"; do
    echo "Verificando $server..."
done

# Array asociativo (Bash 4+)
declare -A SERVICE_PORTS
SERVICE_PORTS[http]=80
SERVICE_PORTS[https]=443
SERVICE_PORTS[ssh]=22
SERVICE_PORTS[mysql]=3306

for service in "${!SERVICE_PORTS[@]}"; do
    echo "$service -> puerto ${SERVICE_PORTS[$service]}"
done

Error comun: Nunca pongas espacios alrededor del signo = al asignar variables. NAME="value" es correcto. NAME = "value" fallara porque Bash interpreta NAME como un comando.

Condicionales: if, elif, else

Los condicionales controlan el flujo de tu script basandose en condiciones de prueba.

Sintaxis basica

#!/bin/bash

FILE="/etc/ssh/sshd_config"

if [[ -f "$FILE" ]]; then
    echo "$FILE existe."
elif [[ -d "$FILE" ]]; then
    echo "$FILE es un directorio, no un archivo."
else
    echo "$FILE no existe."
fi

Operadores de prueba de archivos

#!/bin/bash

TARGET="/var/log/syslog"

# Pruebas de existencia y tipo de archivo
[[ -e "$TARGET" ]] && echo "Existe"
[[ -f "$TARGET" ]] && echo "Es un archivo regular"
[[ -d "$TARGET" ]] && echo "Es un directorio"
[[ -L "$TARGET" ]] && echo "Es un enlace simbolico"
[[ -r "$TARGET" ]] && echo "Es legible"
[[ -w "$TARGET" ]] && echo "Es escribible"
[[ -x "$TARGET" ]] && echo "Es ejecutable"
[[ -s "$TARGET" ]] && echo "Tiene tamano mayor que cero"

Comparaciones de cadenas

#!/bin/bash

ENVIRONMENT="production"

if [[ "$ENVIRONMENT" == "production" ]]; then
    echo "Ejecutando en modo produccion -- precaucion extra!"
    VERBOSE=false
elif [[ "$ENVIRONMENT" == "staging" ]]; then
    echo "Ejecutando en modo staging."
    VERBOSE=true
else
    echo "Entorno desconocido: $ENVIRONMENT"
    exit 1
fi

# Verificaciones de cadenas
[[ -z "$VAR" ]] && echo "VAR esta vacia o no definida"
[[ -n "$VAR" ]] && echo "VAR no esta vacia"

# Coincidencia de patrones con [[ ]]
if [[ "$HOSTNAME" == web* ]]; then
    echo "Este es un servidor web."
fi

Comparaciones numericas

#!/bin/bash

DISK_PERCENT=$(df / | awk 'NR==2 {print $5}' | tr -d '%')

if [[ "$DISK_PERCENT" -gt 90 ]]; then
    echo "CRITICO: Uso de disco al ${DISK_PERCENT}%"
elif [[ "$DISK_PERCENT" -gt 75 ]]; then
    echo "ADVERTENCIA: Uso de disco al ${DISK_PERCENT}%"
else
    echo "OK: Uso de disco al ${DISK_PERCENT}%"
fi

Consejo: Usa [[ ]] (corchetes dobles) en lugar de [ ] (corchetes simples). Los corchetes dobles son una extension de Bash que maneja mejor los espacios en variables, soporta coincidencia de patrones con == y permite &&/|| dentro de la expresion de prueba.

Bucles: for, while, until

Los bucles te permiten repetir operaciones sobre archivos, servidores, usuarios o cualquier lista de elementos.

Bucle for

#!/bin/bash

# Recorrer una lista
for PACKAGE in nginx curl wget htop; do
    echo "Instalando $PACKAGE..."
    sudo apt install -y "$PACKAGE" > /dev/null 2>&1
    echo "$PACKAGE instalado."
done

# Recorrer un rango
for i in {1..10}; do
    echo "Iteracion $i"
done

# Bucle for estilo C
for ((i = 0; i < 5; i++)); do
    echo "Contador: $i"
done

# Recorrer archivos
for FILE in /var/log/*.log; do
    SIZE=$(stat -c%s "$FILE" 2>/dev/null || echo 0)
    echo "$FILE: $SIZE bytes"
done

Bucle while

#!/bin/bash

# Leer un archivo linea por linea
while IFS= read -r line; do
    echo "Procesando: $line"
done < /etc/hosts

# Bucle basado en contador
COUNTER=0
MAX=5
while [[ $COUNTER -lt $MAX ]]; do
    echo "Intento $((COUNTER + 1)) de $MAX"
    COUNTER=$((COUNTER + 1))
done

# Esperar a que un servicio este disponible
RETRIES=0
MAX_RETRIES=30
while ! curl -s http://localhost:8080/health > /dev/null 2>&1; do
    RETRIES=$((RETRIES + 1))
    if [[ $RETRIES -ge $MAX_RETRIES ]]; then
        echo "El servicio no inicio en $MAX_RETRIES intentos."
        exit 1
    fi
    echo "Esperando servicio... (intento $RETRIES/$MAX_RETRIES)"
    sleep 2
done
echo "El servicio esta listo!"

Bucle until

#!/bin/bash

# Bucle until -- se ejecuta hasta que la condicion sea verdadera
COUNT=0
until [[ $COUNT -ge 5 ]]; do
    echo "Conteo es $COUNT"
    COUNT=$((COUNT + 1))
done

Funciones

Las funciones te permiten organizar tus scripts en bloques reutilizables. Esto es critico para scripts mas grandes que realizan multiples tareas relacionadas.

#!/bin/bash

# Definicion de funcion
log_message() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$TIMESTAMP] [$LEVEL] $MESSAGE"
}

# Funcion con valor de retorno
check_service() {
    local SERVICE_NAME="$1"
    if systemctl is-active --quiet "$SERVICE_NAME"; then
        return 0  # exito
    else
        return 1  # fallo
    fi
}

# Funcion que produce un valor (capturar con sustitucion de comando)
get_memory_usage() {
    free -m | awk 'NR==2 {printf "%.1f", $3/$2 * 100}'
}

# Usando las funciones
log_message "INFO" "Iniciando verificacion del sistema..."

SERVICES=("nginx" "ssh" "cron")
for svc in "${SERVICES[@]}"; do
    if check_service "$svc"; then
        log_message "INFO" "$svc esta ejecutandose."
    else
        log_message "WARN" "$svc NO esta ejecutandose!"
    fi
done

MEM_USAGE=$(get_memory_usage)
log_message "INFO" "Uso de memoria: ${MEM_USAGE}%"

Puntos clave sobre las funciones de Bash:

  • Usa local para declarar variables con alcance de funcion (previene contaminar el alcance global)
  • Las funciones usan return para codigos de salida (0-255), no para devolver datos
  • Para devolver datos, usa echo dentro de la funcion y captura con $(nombre_funcion)
  • Los argumentos se acceden con $1, $2, $@ dentro de la funcion

Entrada/Salida y redireccion

Entender la redireccion de E/S es esencial para escribir scripts que registren su salida, procesen archivos y manejen errores correctamente.

#!/bin/bash

# Redirigir stdout a un archivo (sobrescribir)
echo "Entrada de log" > /tmp/output.log

# Redirigir stdout a un archivo (agregar)
echo "Otra entrada" >> /tmp/output.log

# Redirigir stderr a un archivo
command_that_fails 2> /tmp/error.log

# Redirigir tanto stdout como stderr al mismo archivo
some_command > /tmp/all_output.log 2>&1

# Sintaxis moderna (Bash 4+) para redirigir ambos
some_command &> /tmp/all_output.log

# Descartar la salida completamente
noisy_command > /dev/null 2>&1

# Here document (heredoc) para entrada multilinea
cat << 'EOF' > /tmp/config.txt
server {
    listen 80;
    server_name example.com;
    root /var/www/html;
}
EOF

# Canalizar salida a otro comando
ps aux | grep nginx | grep -v grep

# Tee: escribir a archivo Y stdout simultaneamente
echo "Verificacion del sistema aprobada" | tee -a /var/log/checks.log

# Sustitucion de proceso
diff <(ls /dir1) <(ls /dir2)

Leer entrada del usuario

#!/bin/bash

read -p "Ingresa el nombre del servidor: " SERVER_NAME
read -sp "Ingresa la contrasena: " PASSWORD
echo ""  # nueva linea despues de entrada oculta

read -p "Proceder con el despliegue en $SERVER_NAME? (s/n): " CONFIRM
if [[ "$CONFIRM" != "s" ]]; then
    echo "Despliegue cancelado."
    exit 0
fi

Manejo de errores: set -euo pipefail y trap

El manejo adecuado de errores separa los scripts de calidad de produccion de los fragiles. Sin manejo de errores, un script puede fallar silenciosamente a mitad de camino, dejando tu sistema en un estado inconsistente.

El encabezado de modo estricto

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

Lo que hace cada opcion:

  • set -e — Termina inmediatamente si cualquier comando devuelve un estado de salida no cero
  • set -u — Trata las referencias a variables no definidas como errores
  • set -o pipefail — Si cualquier comando en un pipeline falla, todo el pipeline falla (no solo el ultimo comando)
  • IFS=$'\n\t' — Establece el Separador de Campo Interno a nueva linea y tabulacion solamente (previene la division de palabras en espacios en nombres de archivos)

Usar trap para limpieza

#!/bin/bash
set -euo pipefail

TEMP_DIR=""

cleanup() {
    local EXIT_CODE=$?
    if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
        rm -rf "$TEMP_DIR"
        echo "Directorio temporal limpiado: $TEMP_DIR"
    fi
    if [[ $EXIT_CODE -ne 0 ]]; then
        echo "El script fallo con codigo de salida: $EXIT_CODE"
    fi
    exit $EXIT_CODE
}

trap cleanup EXIT ERR

TEMP_DIR=$(mktemp -d)
echo "Trabajando en $TEMP_DIR"

# Tu logica de script aqui...
# Si algo falla, cleanup() se ejecuta automaticamente
cp /some/important/file "$TEMP_DIR/"
process_data "$TEMP_DIR"

Manejar fallos esperados

#!/bin/bash
set -euo pipefail

# Metodo 1: Usar || true para permitir que un comando falle
grep "pattern" /var/log/syslog || true

# Metodo 2: Usar if para verificar el resultado
if grep -q "error" /var/log/syslog; then
    echo "Errores encontrados en syslog"
else
    echo "No se encontraron errores"
fi

# Metodo 3: Capturar y verificar codigo de salida
set +e  # deshabilitar temporalmente salida-ante-error
risky_command
EXIT_CODE=$?
set -e  # re-habilitar

if [[ $EXIT_CODE -ne 0 ]]; then
    echo "El comando fallo con codigo $EXIT_CODE"
fi

Trabajar con archivos y directorios

Los scripts de sysadmin frecuentemente necesitan crear, verificar, mover y procesar archivos. Aqui estan los patrones mas comunes:

#!/bin/bash
set -euo pipefail

# Crear una estructura de directorios
BACKUP_BASE="/var/backups/myapp"
BACKUP_DIR="${BACKUP_BASE}/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

# Verificar si un archivo existe antes de operar con el
CONFIG_FILE="/etc/myapp/config.yml"
if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "ERROR: Archivo de configuracion no encontrado: $CONFIG_FILE"
    exit 1
fi

# Encontrar archivos con mas de 30 dias
find /var/log/myapp -name "*.log" -mtime +30 -type f -print

# Encontrar y eliminar archivos viejos (con confirmacion)
find /tmp -name "*.tmp" -mtime +7 -type f -delete

# Obtener tamano de archivo en bytes
FILE_SIZE=$(stat -c%s "$CONFIG_FILE")
echo "Tamano del archivo de configuracion: $FILE_SIZE bytes"

# Contar lineas en un archivo
LINE_COUNT=$(wc -l < "$CONFIG_FILE")
echo "La configuracion tiene $LINE_COUNT lineas"

# Leer un archivo de configuracion, saltando comentarios y lineas vacias
while IFS='=' read -r key value; do
    # Saltar comentarios y lineas vacias
    [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
    echo "Config: $key = $value"
done < "$CONFIG_FILE"

# Creacion segura de archivos temporales
TEMP_FILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Usando archivo temporal: $TEMP_FILE"
# Siempre limpia archivos temporales (usa trap como se mostro antes)

Scripts practicos para sysadmins

Aqui hay cuatro scripts completos, listos para produccion, que resuelven problemas comunes de administracion de sistemas.

Script 1: Monitor de espacio en disco con alerta por correo

#!/bin/bash
set -euo pipefail

# Configuracion
THRESHOLD=80
ALERT_EMAIL="admin@example.com"
LOG_FILE="/var/log/disk-monitor.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Iniciando verificacion de espacio en disco..."

ALERT_TRIGGERED=false

while IFS= read -r line; do
    # Saltar la linea de encabezado
    [[ "$line" == Filesystem* ]] && continue

    USAGE=$(echo "$line" | awk '{print $5}' | tr -d '%')
    MOUNT=$(echo "$line" | awk '{print $6}')
    FILESYSTEM=$(echo "$line" | awk '{print $1}')

    if [[ "$USAGE" -ge "$THRESHOLD" ]]; then
        ALERT_TRIGGERED=true
        log "ADVERTENCIA: $MOUNT esta al ${USAGE}% ($FILESYSTEM)"
    else
        log "OK: $MOUNT esta al ${USAGE}%"
    fi
done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs)

if [[ "$ALERT_TRIGGERED" == true ]]; then
    SUBJECT="[ALERTA] Advertencia de espacio en disco en $(hostname)"
    BODY="Uno o mas sistemas de archivos excedieron ${THRESHOLD}% de uso en $(hostname) a las $(date).\n\n$(df -h)"
    echo -e "$BODY" | mail -s "$SUBJECT" "$ALERT_EMAIL" 2>/dev/null || \
        log "ADVERTENCIA: No se pudo enviar la alerta por correo"
fi

log "Verificacion de espacio en disco completada."

Script 2: Limpieza y rotacion de logs

#!/bin/bash
set -euo pipefail

# Configuracion
LOG_DIRS=("/var/log/myapp" "/var/log/nginx" "/home/*/logs")
MAX_AGE_DAYS=30
COMPRESS_AGE_DAYS=7
DRY_RUN=false

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Analizar argumentos
while [[ $# -gt 0 ]]; do
    case "$1" in
        --dry-run) DRY_RUN=true; shift ;;
        --max-age) MAX_AGE_DAYS="$2"; shift 2 ;;
        *) echo "Opcion desconocida: $1"; exit 1 ;;
    esac
done

TOTAL_FREED=0

for DIR_PATTERN in "${LOG_DIRS[@]}"; do
    for DIR in $DIR_PATTERN; do
        [[ -d "$DIR" ]] || continue
        log "Procesando $DIR..."

        # Comprimir logs mas antiguos que COMPRESS_AGE_DAYS
        while IFS= read -r -d '' file; do
            if [[ "$DRY_RUN" == true ]]; then
                log "  [SIMULACION] Comprimiria: $file"
            else
                gzip "$file"
                log "  Comprimido: $file"
            fi
        done < <(find "$DIR" -name "*.log" -mtime +"$COMPRESS_AGE_DAYS" -type f -print0)

        # Eliminar logs comprimidos mas antiguos que MAX_AGE_DAYS
        while IFS= read -r -d '' file; do
            SIZE=$(stat -c%s "$file")
            TOTAL_FREED=$((TOTAL_FREED + SIZE))
            if [[ "$DRY_RUN" == true ]]; then
                log "  [SIMULACION] Eliminaria: $file ($SIZE bytes)"
            else
                rm -f "$file"
                log "  Eliminado: $file ($SIZE bytes)"
            fi
        done < <(find "$DIR" -name "*.gz" -mtime +"$MAX_AGE_DAYS" -type f -print0)
    done
done

FREED_MB=$((TOTAL_FREED / 1024 / 1024))
log "Espacio total liberado: ${FREED_MB} MB"

Script 3: Script de respaldo automatizado

#!/bin/bash
set -euo pipefail

# Configuracion
BACKUP_SOURCE="/var/www /etc/nginx /etc/myapp"
BACKUP_DEST="/var/backups"
RETENTION_DAYS=14
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)
BACKUP_FILE="${BACKUP_DEST}/${HOSTNAME}_backup_${TIMESTAMP}.tar.gz"
LOG_FILE="/var/log/backup.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

cleanup() {
    local EXIT_CODE=$?
    if [[ $EXIT_CODE -ne 0 ]]; then
        log "ERROR: El respaldo fallo con codigo de salida $EXIT_CODE"
        rm -f "$BACKUP_FILE"
    fi
}
trap cleanup EXIT

# Asegurar que el directorio de respaldos existe
mkdir -p "$BACKUP_DEST"

log "Iniciando respaldo de: $BACKUP_SOURCE"
log "Destino: $BACKUP_FILE"

# Crear tarball comprimido
# shellcheck disable=SC2086
tar -czf "$BACKUP_FILE" $BACKUP_SOURCE 2>/dev/null

BACKUP_SIZE=$(stat -c%s "$BACKUP_FILE")
BACKUP_SIZE_MB=$((BACKUP_SIZE / 1024 / 1024))
log "Respaldo creado: ${BACKUP_SIZE_MB} MB"

# Verificar el respaldo
if tar -tzf "$BACKUP_FILE" > /dev/null 2>&1; then
    log "Verificacion de integridad del respaldo: APROBADA"
else
    log "ERROR: Verificacion de integridad del respaldo FALLIDA"
    exit 1
fi

# Eliminar respaldos antiguos
DELETED_COUNT=0
while IFS= read -r -d '' old_backup; do
    rm -f "$old_backup"
    DELETED_COUNT=$((DELETED_COUNT + 1))
    log "Respaldo antiguo eliminado: $old_backup"
done < <(find "$BACKUP_DEST" -name "${HOSTNAME}_backup_*.tar.gz" -mtime +"$RETENTION_DAYS" -type f -print0)

log "Respaldo completado. Se eliminaron $DELETED_COUNT respaldo(s) antiguo(s)."

Script 4: Auditoria de cuentas de usuario

#!/bin/bash
set -euo pipefail

# Configuracion
OUTPUT_FILE="/tmp/user_audit_$(date +%Y%m%d).txt"
MIN_UID=1000

log() {
    echo "$1" | tee -a "$OUTPUT_FILE"
}

# Limpiar salida anterior
> "$OUTPUT_FILE"

log "============================================"
log "  REPORTE DE AUDITORIA DE CUENTAS DE USUARIO"
log "  Host: $(hostname)"
log "  Fecha: $(date '+%Y-%m-%d %H:%M:%S')"
log "============================================"
log ""

# Listar todos los usuarios humanos (UID >= 1000)
log "--- Cuentas de usuarios humanos (UID >= $MIN_UID) ---"
while IFS=: read -r username _ uid gid _ home shell; do
    if [[ $uid -ge $MIN_UID ]]; then
        LAST_LOGIN=$(lastlog -u "$username" 2>/dev/null | tail -1 | awk '{print $4, $5, $6, $7, $8, $9}')
        GROUPS=$(groups "$username" 2>/dev/null | cut -d: -f2)
        HAS_SUDO="No"
        if groups "$username" 2>/dev/null | grep -qw "sudo\|wheel"; then
            HAS_SUDO="Si"
        fi
        log "  Usuario: $username (UID: $uid)"
        log "    Home: $home"
        log "    Shell: $shell"
        log "    Sudo: $HAS_SUDO"
        log "    Grupos:$GROUPS"
        log "    Ultimo inicio de sesion: $LAST_LOGIN"
        log ""
    fi
done < /etc/passwd

# Verificar usuarios con contrasenas vacias
log "--- Usuarios con contrasenas vacias ---"
EMPTY_PASS=$(sudo awk -F: '($2 == "") {print $1}' /etc/shadow 2>/dev/null || true)
if [[ -n "$EMPTY_PASS" ]]; then
    log "  ADVERTENCIA: $EMPTY_PASS"
else
    log "  Ninguno encontrado (bien)."
fi
log ""

# Verificar usuarios con UID 0 (equivalentes a root)
log "--- Usuarios con UID 0 (Privilegios de root) ---"
while IFS=: read -r username _ uid _; do
    if [[ $uid -eq 0 ]]; then
        log "  $username (UID: 0)"
    fi
done < /etc/passwd
log ""

# Listar llaves SSH autorizadas
log "--- Llaves SSH autorizadas ---"
for HOME_DIR in /home/*; do
    USERNAME=$(basename "$HOME_DIR")
    AUTH_KEYS="$HOME_DIR/.ssh/authorized_keys"
    if [[ -f "$AUTH_KEYS" ]]; then
        KEY_COUNT=$(wc -l < "$AUTH_KEYS")
        log "  $USERNAME: $KEY_COUNT llave(s) en $AUTH_KEYS"
    fi
done
log ""

log "Reporte guardado en: $OUTPUT_FILE"

Programacion con cron

Cron es el programador de tareas estandar de Linux. Ejecuta scripts a intervalos definidos sin intervencion manual.

Editar el crontab

# Editar crontab para el usuario actual
crontab -e

# Editar crontab para un usuario especifico (requiere root)
sudo crontab -u www-data -e

# Listar entradas actuales del crontab
crontab -l

Sintaxis de cron

# ┌───────────── minuto (0 - 59)
# │ ┌───────────── hora (0 - 23)
# │ │ ┌───────────── dia del mes (1 - 31)
# │ │ │ ┌───────────── mes (1 - 12)
# │ │ │ │ ┌───────────── dia de la semana (0 - 7, 0 y 7 = Domingo)
# │ │ │ │ │
# * * * * * comando

Programaciones comunes de cron

# Ejecutar monitor de disco cada hora
0 * * * * /usr/local/bin/disk-monitor.sh >> /var/log/disk-monitor.log 2>&1

# Ejecutar respaldo diariamente a las 2 AM
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# Ejecutar limpieza de logs cada domingo a las 3 AM
0 3 * * 0 /usr/local/bin/log-cleanup.sh >> /var/log/cleanup.log 2>&1

# Ejecutar auditoria de usuarios el 1ero de cada mes
0 9 1 * * /usr/local/bin/user-audit.sh >> /var/log/user-audit.log 2>&1

# Ejecutar verificacion de salud cada 5 minutos
*/5 * * * * /usr/local/bin/health-check.sh > /dev/null 2>&1

Reglas importantes de cron: Siempre usa rutas absolutas en los trabajos de cron (no rutas relativas). Cron se ejecuta con un entorno minimo, por lo que PATH puede no incluir /usr/local/bin. Siempre redirige la salida a un archivo de log o a /dev/null para prevenir que cron envie correo en cada ejecucion.

Usar variables de entorno en cron

# Establecer variables de entorno al inicio de tu crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com

# Ahora tus trabajos tienen acceso a rutas comunes
0 2 * * * /usr/local/bin/backup.sh

Tabla de referencia de operadores de Bash

CategoriaOperadorDescripcionEjemplo
Pruebas de archivos-fArchivo existe y es regular[[ -f /etc/hosts ]]
-dDirectorio existe[[ -d /var/log ]]
-eArchivo o directorio existe[[ -e /tmp/lock ]]
-rArchivo es legible[[ -r config.yml ]]
-wArchivo es escribible[[ -w /var/log/app.log ]]
-xArchivo es ejecutable[[ -x script.sh ]]
-sTamano mayor que cero[[ -s output.log ]]
-LEs un enlace simbolico[[ -L /usr/bin/python ]]
Cadenas==Cadenas son iguales[[ "$a" == "$b" ]]
!=Cadenas no son iguales[[ "$a" != "$b" ]]
-zCadena esta vacia[[ -z "$var" ]]
-nCadena no esta vacia[[ -n "$var" ]]
=~Coincidencia regex[[ "$s" =~ ^[0-9]+$ ]]
Numerico-eqIgual[[ $a -eq $b ]]
-neNo igual[[ $a -ne $b ]]
-gtMayor que[[ $a -gt $b ]]
-geMayor o igual que[[ $a -ge $b ]]
-ltMenor que[[ $a -lt $b ]]
-leMenor o igual que[[ $a -le $b ]]
Logico&&AND (dentro de [[ ]])[[ $a -gt 0 && $a -lt 100 ]]
||OR (dentro de [[ ]])[[ $a -eq 0 || $a -eq 1 ]]
!NOT[[ ! -f /tmp/lock ]]

Solucion de problemas y depuracion

Cuando un script no funciona como se espera, usa estas tecnicas para encontrar el problema.

Depurar con set -x

#!/bin/bash
set -x  # Imprimir cada comando antes de ejecutarlo

NAME="world"
echo "Hello, $NAME"
# Salida:
# + NAME=world
# + echo 'Hello, world'
# Hello, world

Puedes habilitar la depuracion para secciones especificas:

#!/bin/bash

echo "Salida normal aqui"

set -x  # Iniciar salida de depuracion
RESULT=$(some_complex_command)
process "$RESULT"
set +x  # Detener salida de depuracion

echo "De vuelta a la salida normal"

Ejecutar un script en modo de depuracion

# Depurar el script completo sin modificarlo
bash -x ./myscript.sh

# Modo verboso (imprimir lineas al ser leidas)
bash -v ./myscript.sh

# Verboso + rastreo combinados
bash -xv ./myscript.sh

ShellCheck: Analisis estatico

ShellCheck es una herramienta esencial que detecta errores comunes de Bash, errores de entrecomillado y problemas de portabilidad.

# Instalar ShellCheck
sudo apt install -y shellcheck

# Analizar un script
shellcheck myscript.sh

# Salida de ejemplo:
# In myscript.sh line 5:
# echo $VARIABLE
#      ^--------^ SC2086: Double quote to prevent globbing and word splitting.

Lista de verificacion de depuracion comun

  1. Verificar el shebang: Es #!/bin/bash (no #!/bin/sh)?
  2. Verificar permisos: Ejecutaste chmod +x script.sh?
  3. Verificar finales de linea: Los finales de linea de Windows (\r\n) causan /bin/bash^M: bad interpreter. Corrige con dos2unix script.sh.
  4. Entrecomillar tus variables: Siempre usa "$VARIABLE" no $VARIABLE para manejar espacios y caracteres especiales.
  5. Verificar codigos de salida: Despues de cada comando critico, verifica $? o usa set -e.
  6. Probar con valores fijos: Reemplaza variables con valores conocidos para aislar el problema.
  7. Verificar entorno de cron: Si un script funciona manualmente pero no en cron, el problema usualmente es PATH o variables de entorno faltantes.
# Script rapido para volcar el entorno de cron
* * * * * env > /tmp/cron-env.txt 2>&1
# (eliminar despues de verificar)

Resumen

Bash scripting es una de las habilidades mas valiosas que un administrador de sistemas Linux puede desarrollar. En esta guia, aprendiste como:

  • Escribir scripts con estructura adecuada (shebang, permisos, modo estricto)
  • Usar variables, arrays y sustitucion de comandos
  • Controlar el flujo con condicionales y bucles
  • Organizar codigo con funciones
  • Manejar errores de forma segura con set -euo pipefail y trap
  • Construir scripts practicos para monitoreo de disco, limpieza de logs, respaldos y auditoria de usuarios
  • Programar scripts con cron para ejecucion completamente automatizada
  • Depurar scripts con set -x y ShellCheck

Los cuatro scripts practicos de este articulo son puntos de partida. Personalizalos para tu entorno especifico, agrega notificaciones por correo, integralos con sistemas de monitoreo y construye sobre ellos a medida que crezcan tus necesidades.

Para temas relacionados de administracion de servidores, consulta nuestra Lista de verificacion de seguridad para servidores Linux: 20 pasos esenciales para asegurar los servidores donde se ejecutan tus scripts, y Hardening SSH: 12 pasos para asegurar tu servidor Linux para proteger el acceso SSH que usas para administrarlos.