1. Estructura básica de un script Bash

Todo script Bash comienza con el shebang, una línea especial que le dice al sistema operativo qué intérprete usar para ejecutar el archivo. Sin esta línea, el script podría ejecutarse con el shell equivocado o fallar completamente.

#!/usr/bin/env bash # -------------------------------------------------- # Script: mi_script.sh # Descripción: Ejemplo de estructura básica # Versión: 1.0 | Autor: ClickHalo | 2025-06-01 # -------------------------------------------------- # Opciones de seguridad (siempre incluir) set -euo pipefail # Constantes readonly VERSION="1.0" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Función principal main() { echo "Script ejecutado desde: $SCRIPT_DIR" echo "Versión: $VERSION" } # Punto de entrada main "$@"

La línea set -euo pipefail es crucial y muchos tutoriales la omiten. Veamos qué hace cada opción:

  • -e: El script termina si cualquier comando falla (devuelve código distinto de 0).
  • -u: El script falla si usas una variable no definida. Previene errores silenciosos.
  • -o pipefail: Si un comando en un pipe falla, el pipe completo falla. Sin esto, comando_malo | grep algo podría ignorar el error.

💡 Consejo: Usa #!/usr/bin/env bash en lugar de #!/bin/bash. La versión con env busca Bash en el PATH del sistema, lo que hace el script más portátil entre distribuciones Linux y macOS.

2. Variables, argumentos y entrada del usuario

El manejo correcto de variables es la base de cualquier script robusto. En Bash, las variables no tienen tipos estrictos, lo que puede ser una fuente de bugs si no se tiene cuidado.

#!/usr/bin/env bash set -euo pipefail # Variables locales (siempre entre comillas al usar) NOMBRE="servidor-web" PUERTO=8080 DIRECTORIO="/var/www/html" # Leer argumentos posicionales ARG1="${1:-valor_por_defecto}" # $1 o "valor_por_defecto" si no se pasa ARG2="${2:?'Error: segundo argumento requerido'}" # Número de argumentos echo "Argumentos recibidos: $#" echo "Todos los argumentos: $@" # Leer input interactivo read -rp "Introduce el nombre del entorno: " ENTORNO echo "Configurando entorno: ${ENTORNO}" # Expansión de comandos FECHA="$(date +%Y-%m-%d)" ESPACIO_LIBRE="$(df -h / | awk 'NR==2 {print $4}')" echo "Fecha: ${FECHA} | Espacio libre: ${ESPACIO_LIBRE}"

Observa el uso de ${VAR:-default} para valores por defecto y ${VAR:?mensaje} para requerir una variable. Estas son expansiones de parámetros de Bash que hacen tus scripts mucho más seguros.

3. Estructuras de control: condicionales y bucles

Los condicionales en Bash son más flexibles de lo que parecen. Aquí los patrones más útiles en la práctica:

#!/usr/bin/env bash set -euo pipefail ARCHIVO="/etc/nginx/nginx.conf" SERVICIO="nginx" # Comprobar si un archivo existe if [[ -f "$ARCHIVO" ]]; then echo "✓ Archivo de configuración encontrado" elif [[ -d "$(dirname "$ARCHIVO")" ]]; then echo "⚠ El directorio existe pero no el archivo" else echo "✗ Directorio no encontrado" exit 1 fi # Comprobar si un servicio está activo if systemctl is-active --quiet "$SERVICIO"; then echo "$SERVICIO está corriendo" else echo "Iniciando $SERVICIO..." systemctl start "$SERVICIO" fi # Bucle sobre archivos for LOG_FILE in /var/log/*.log; do TAMANO="$(du -sh "$LOG_FILE" | cut -f1)" echo " $LOG_FILE → $TAMANO" done # Bucle while con contador INTENTOS=0 MAX_INTENTOS=5 while ! ping -c1 google.com &>/dev/null; do INTENTOS=$(( INTENTOS + 1 )) if (( INTENTOS >= MAX_INTENTOS )); then echo "Sin conexión tras $MAX_INTENTOS intentos" exit 1 fi sleep 2 done

4. Manejo robusto de errores

Un script de producción debe gestionar los errores de forma explícita. La combinación de set -e con un trap personalizado te da control total sobre qué ocurre cuando algo falla.

#!/usr/bin/env bash set -euo pipefail # Colores para output (solo si es terminal interactiva) if [[ -t 1 ]]; then RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' else RED=''; GREEN=''; NC='' fi # Función de log log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } # Trap: se ejecuta al salir con error trap 'log_error "Error en línea $LINENO. Código de salida: $?"' ERR # Trap: limpieza al terminar (con o sin error) TEMP_DIR="$(mktemp -d)" trap 'rm -rf "$TEMP_DIR"' EXIT log_info "Directorio temporal: $TEMP_DIR" # Ejecutar comando con manejo de error explícito if ! cp /ruta/que/podria/no/existir "$TEMP_DIR/" 2>/dev/null; then log_error "No se pudo copiar el archivo" exit 1 fi log_info "Script completado con éxito"

⚠️ Importante: El trap ... EXIT se ejecuta SIEMPRE al salir del script, ya sea por éxito, error o señal. Úsalo para limpiar archivos temporales, liberar locks o restaurar configuraciones.

5. Funciones y modularidad

Las funciones en Bash te permiten reutilizar código y hacer tus scripts más legibles. Un principio clave: cada función debería hacer una sola cosa y hacerla bien.

#!/usr/bin/env bash set -euo pipefail # Función con valor de retorno vía stdout get_disk_usage() { local MOUNT_POINT="${1:-/}" df -h "$MOUNT_POINT" | awk 'NR==2 {print $5}' | tr -d '%' } # Función con código de salida is_service_running() { local SERVICE="$1" systemctl is-active --quiet "$SERVICE" } # Función de backup backup_directory() { local SRC="$1" local DEST="$2" local FECHA FECHA="$(date +%Y%m%d_%H%M%S)" [[ -d "$SRC" ]] || { echo "SRC no existe: $SRC"; return 1; } mkdir -p "$DEST" rsync -av --progress \ --exclude='*.log' \ --exclude='.git' \ "$SRC/" "${DEST}/${FECHA}/" echo "Backup guardado en: ${DEST}/${FECHA}" } # Uso de las funciones USO="$(get_disk_usage /)" echo "Disco raíz al ${USO}% de uso" if (( USO > 80 )); then echo "ALERTA: Disco casi lleno" fi is_service_running nginx && echo "nginx OK" || echo "nginx CAÍDO"

6. Automatización con cron jobs

Una vez que tu script funciona correctamente, el siguiente paso es programarlo para que se ejecute automáticamente. El servicio cron de Linux es la herramienta estándar para esto.

Para editar el crontab del usuario actual, ejecuta:

crontab -e

La sintaxis del crontab es minuto hora día-del-mes mes día-de-semana comando:

# m h dom mon dow comando # Backup diario a las 2:00 AM 0 2 * * * /home/usuario/scripts/backup.sh >> /var/log/backup.log 2>&1 # Limpiar logs cada lunes a las 3:30 AM 30 3 * * 1 /home/usuario/scripts/clean_logs.sh # Comprobar espacio en disco cada hora 0 * * * * /home/usuario/scripts/check_disk.sh # Script cada 5 minutos */5 * * * * /home/usuario/scripts/monitor.sh # Primer día de cada mes a las 6:00 AM 0 6 1 * * /home/usuario/scripts/monthly_report.sh

💡 Truco: Siempre redirige la salida del cron a un archivo de log (>> /var/log/mi_script.log 2>&1). Si no lo haces, cron intentará enviarte un correo, que en la mayoría de servidores no está configurado. Usa crontab.guru para generar y verificar expresiones cron visualmente.

7. Ejemplo real: script de backup automatizado

Aquí un script completo y listo para producción que combina todo lo anterior. Este script hace un backup incremental con rsync, mantiene solo los últimos N backups y registra toda la actividad en un log.

#!/usr/bin/env bash # backup.sh — Backup incremental con retención automática # Uso: ./backup.sh [--dry-run] set -euo pipefail ## Configuración readonly SRC_DIR="/var/www/html" readonly BACKUP_ROOT="/backup/web" readonly MAX_BACKUPS=7 readonly LOG_FILE="/var/log/backup_web.log" readonly TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)" DRY_RUN=false ## Parsear argumentos [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true ## Logging log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE"; } trap 'log "ERROR en línea $LINENO"' ERR ## Validar entorno [[ -d "$SRC_DIR" ]] || { log "SRC_DIR no existe: $SRC_DIR"; exit 1; } command -v rsync &>/dev/null || { log "rsync no instalado"; exit 1; } ## Crear destino DEST_DIR="$BACKUP_ROOT/$TIMESTAMP" if [[ "$DRY_RUN" == false ]]; then mkdir -p "$DEST_DIR" fi log "Iniciando backup: $SRC_DIR → $DEST_DIR" $DRY_RUN && log "[MODO DRY-RUN — sin cambios reales]" ## Ejecutar rsync rsync -av \ $( $DRY_RUN && echo "--dry-run") \ --exclude='*.log' \ --exclude='.git' \ --exclude='node_modules' \ "$SRC_DIR/" "$DEST_DIR/" \ | tee -a "$LOG_FILE" ## Eliminar backups antiguos (mantener los últimos MAX_BACKUPS) if [[ "$DRY_RUN" == false ]]; then NUM_BACKUPS="$(ls -d "$BACKUP_ROOT"/*/ 2>/dev/null | wc -l)" if (( NUM_BACKUPS > MAX_BACKUPS )); then ls -dt "$BACKUP_ROOT"/*/ | tail -n +$(( MAX_BACKUPS + 1 )) | xargs rm -rf log "Backups antiguos eliminados. Total: $MAX_BACKUPS" fi fi log "Backup completado: $TIMESTAMP"

8. Buenas prácticas y checklist

Antes de poner un script en producción, verifica que cumple con estos puntos:

  • ✅ Incluye #!/usr/bin/env bash y set -euo pipefail
  • ✅ Todas las variables están entrecomilladas: "$VAR" no $VAR
  • ✅ Usa readonly para constantes que no deben cambiar
  • ✅ Implementa un trap para limpiar archivos temporales al salir
  • ✅ Registra la actividad en un log con timestamps
  • ✅ Valida que los archivos y directorios necesarios existen antes de empezar
  • ✅ Comprueba que las dependencias externas (rsync, jq, etc.) están instaladas
  • ✅ Prueba con --dry-run o bash -n script.sh antes de ejecutar en producción
  • ✅ Usa shellcheck para detectar errores comunes automáticamente
  • ✅ El script tiene permisos correctos: chmod 750 script.sh

🔧 Herramienta recomendada: Instala ShellCheck (apt install shellcheck) y úsalo sobre todos tus scripts antes de ponerlos en producción. Detecta automáticamente errores comunes de Bash que los ojos pasan por alto.

Conclusión

La automatización con Bash es una de las habilidades más valiosas que puede tener un administrador de sistemas o desarrollador que trabaja en entornos Linux. Un buen script no solo hace la tarea, sino que la hace de forma segura, registra lo que ocurre y falla de forma controlada cuando algo sale mal.

El siguiente paso natural es explorar herramientas como Ansible para automatización a mayor escala, o Python para scripts que requieren estructuras de datos más complejas. Pero para la mayoría de tareas del día a día en un servidor, Bash sigue siendo la herramienta perfecta.