Scripting en Bash: guía práctica para tu homelab

Scripting en Bash: guía práctica para tu homelab

Scripting en Bash: guía práctica para tu homelab

Bash es el pegamento del homelab: conecta comandos, automatiza tareas y deja correr scripts mientras duermes. Aquí va lo que necesitas para empezar, con ejemplos reales y sin atajos.

Por Javier · Actualizado: 2026-05-15

Un script Bash es un archivo de texto con comandos shell que el sistema ejecuta en secuencia. Con el shebang #!/bin/bash, permisos de ejecución (chmod +x) y cuatro conceptos básicos —variables, bucles, condicionales y cron— puedes automatizar tareas repetitivas en tu homelab sin necesidad de un lenguaje de propósito general.

Para ti, que ya sabes moverse en la terminal

Si llevas un tiempo gestionando tu servidor en casa, probablemente ya tienes soltura con la terminal. Sabes hacer SSH, sabes dónde están los logs, quizá tienes un par de alias guardados en .bashrc. Pero cada vez que hay que hacer una tarea repetitiva —limpiar una carpeta, comprobar que un servicio sigue vivo, hacer una copia de seguridad rápida— la haces a mano, paso a paso, como la primera vez.

La barrera para escribir tu primer script suele ser más psicológica que técnica. No es que no sepas los comandos: es que nunca has dado el paso de meterlos en un archivo y ejecutarlo. Quizá has abierto algún script ajeno, has visto un if raro o unas comillas que no cuadraban, y has cerrado el archivo pensando que eso no era para ti.

Este post va exactamente de ese paso. No asume que sabes programar, pero sí que sabes qué quieres automatizar. Vas a escribir scripts reales —útiles en tu homelab— entendiendo lo que hace cada línea. Sin magia, sin copiar y pegar a ciegas.

Por qué importa

Shebang y permisos

Todo script empieza con #!/usr/bin/env bash y necesita chmod +x. Sin eso, no se ejecuta.

Variables sin tipo

Asigna sin espacios: nombre=»valor». Úsalas con $nombre. Bash no distingue strings de números.

Automatiza con cron

Cron usa 5 campos (minuto hora día mes día-semana) para lanzar tu script sin que estés delante.

Bash no lo hace todo

Para lógica compleja o manejo de datos estructurados, Python es más adecuado. Bash brilla en tareas de sistema y glue code.

El shebang, los permisos y tu primer script

Un script Bash es, en esencia, un archivo de texto con comandos que el shell ejecutaría uno a uno si los teclearas a mano. La ventaja es que los ejecutas todos de golpe, en orden, sin estar delante del terminal.

Antes de escribir la primera línea útil, hay dos cosas que no puedes saltarte: el shebang y los permisos de ejecución.

El shebang: decirle al sistema qué intérprete usar

La primera línea del script indica qué intérprete debe usar el sistema para ejecutarlo:

#!/usr/bin/env bash

La forma #!/usr/bin/env bash es más portable que #!/bin/bash porque busca Bash en el PATH en lugar de asumir una ruta fija. En macOS con Homebrew, por ejemplo, Bash puede estar en /opt/homebrew/bin/bash, no en /bin/bash. Sin shebang, el sistema puede intentar ejecutar el archivo con sh o dash, y algunas características de Bash puro dejarán de funcionar sin avisar bien.

Dar permisos de ejecución

El archivo existe, pero el sistema no lo ejecutará hasta que le des permiso explícito:

chmod +x mi_script.sh

Después puedes lanzarlo con ./mi_script.sh. Si lo colocas en un directorio que esté en tu PATH —como ~/.local/bin/— puedes llamarlo por nombre desde cualquier carpeta.

Un primer script real que uso para avisar si el disco del NAS supera un umbral de uso:

#!/usr/bin/env bash
THRESHOLD=85
USAGE=$(df /mnt/nas | awk 'NR==2 {print $5}' | tr -d '%')

if [ "$USAGE" -gt "$THRESHOLD" ]; then
  echo "Aviso: disco al ${USAGE}%"
fi

No hace nada del otro mundo, pero tiene todos los ingredientes básicos: shebang, variable, sustitución de comando y condicional.

Variables, condiciones y bucles

En Bash las variables no tienen tipo declarado. Almacenan texto por defecto, aunque puedes operar aritméticamente con ellas cuando los valores son enteros.

Variables

La asignación va sin espacios alrededor del =. Con espacio, Bash interpreta el nombre como un comando y falla con un error confuso:

# Correcto
DIRECTORIO="/home/javi/backups"

# Esto falla: Bash cree que DIRECTORIO es un comando
DIRECTORIO = "/home/javi/backups"

Para usar el valor de una variable la precedas de $ o la envuelves en ${}. La forma con llaves es más segura cuando concatenas la variable con texto, porque delimita dónde termina el nombre:

NOMBRE="proxmox"
echo "El host es ${NOMBRE}-01"   # proxmox-01

Condiciones con if

Los corchetes [ ] en los condicionales son el comando test bajo otro nombre, así que los espacios interiores son obligatorios. Sin ellos obtienes un error críptico en lugar de un resultado útil:

if [ "$VARIABLE" = "valor" ]; then
  echo "coincide"
elif [ "$VARIABLE" = "otro" ]; then
  echo "otro valor"
else
  echo "no coincide"
fi

Para comparaciones numéricas usa los operadores específicos: -eq, -gt, -lt, -ge, -le, -ne. Comparar números con = a veces funciona y a veces no, porque compara lexicográficamente, lo que da resultados incorrectos con números de distinta longitud.

Bucles for y while

El bucle for es el más habitual en scripts de homelab: iterar sobre archivos o sobre listas de hosts:

# Iterar sobre archivos
for ARCHIVO in /mnt/backups/*.tar.gz; do
  echo "Procesando: $ARCHIVO"
done

# Comprobar disponibilidad de varios hosts
for HOST in proxmox-01 proxmox-02 nuc-01; do
  ping -c 1 "$HOST" >/dev/null 2>&1 && echo "$HOST: OK" || echo "$HOST: sin respuesta"
done

El bucle while encaja mejor cuando lees líneas de un archivo o cuando no sabes cuántas iteraciones necesitas de antemano:

while IFS= read -r LINEA; do
  echo "Host: $LINEA"
done < lista_hosts.txt

Argumentos y sustitución de comandos

Un script que solo funciona con valores fijos tiene vida corta. Los argumentos posicionales le permiten recibir datos en el momento de ejecutarlo, haciendo el mismo archivo reutilizable para distintos escenarios.

Argumentos posicionales

$1 es el primer argumento, $2 el segundo. $@ los contiene todos y $# dice cuántos recibió el script. Es buena práctica validar que llegan antes de hacer nada:

#!/usr/bin/env bash
# uso: ./backup.sh /ruta/origen /ruta/destino

if [ "$#" -ne 2 ]; then
  echo "Uso: $0 <origen> <destino>" >&2
  exit 1
fi

ORIGEN="$1"
DESTINO="$2"

rsync -av "$ORIGEN" "$DESTINO"

El exit 1 cuando faltan argumentos es importante: los programas que llamen a tu script, incluido cron, necesitan saber si algo salió mal. Código de salida 0 indica éxito; cualquier otro valor indica error.

Sustitución de comandos

La sustitución de comandos con $() captura la salida de un comando y la usa como valor en variables o directamente en otras expresiones:

FECHA=$(date +%Y-%m-%d)
HOSTNAME=$(hostname -s)
NOMBRE_BACKUP="backup-${HOSTNAME}-${FECHA}.tar.gz"

En código antiguo verás la forma con backticks (`comando`). Funciona igual pero no se puede anidar fácilmente y resulta menos legible. La recomendación actual es $().

Automatizar con cron: que el script se ejecute solo

Tener un script que funciona en manual es el primer paso. El segundo es que se ejecute solo, en los momentos que definas, sin que tengas que acordarte.

El formato de crontab

Cada línea de un crontab tiene cinco campos de tiempo seguidos del comando a ejecutar:

# minuto  hora  día-mes  mes  día-semana  comando
  0       3     *        *    *           /home/javi/scripts/backup_nas.sh

Esa entrada ejecuta el script todos los días a las 3:00 AM. Para editar tu crontab personal: crontab -e. Algunos patrones habituales:

  • */15 * * * * — cada 15 minutos
  • 0 */6 * * * — cada 6 horas (0h, 6h, 12h, 18h)
  • 30 2 * * 1 — los lunes a las 2:30
  • 0 0 1 * * — el primer día de cada mes a medianoche

Gotchas de cron que queman tiempo

El entorno de cron es más escueto que el de tu terminal interactiva: no carga tu .bashrc, el PATH es mínimo y las variables de entorno que das por supuestas simplemente no están. Tres cosas que hacen los scripts mucho más robustos bajo cron:

  1. Rutas absolutas para binarios, archivos de entrada y de salida.
  2. Capturar la salida en un log para poder depurar si algo falla en mitad de la noche.
  3. Definir PATH explícitamente al principio del script si usas binarios que no están en /usr/bin o /bin.
#!/usr/bin/env bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

LOGFILE="/var/log/backup_nas.log"
exec >> "$LOGFILE" 2>&1

echo "=== $(date) ==="
rsync -av /datos/origen/ /mnt/nas/destino/

Manejo de errores y salida limpia

Un script que falla en silencio es peor que uno que no existe. Si el backup de las 3 AM falla sin dejar rastro, te enteras cuando necesitas restaurar algo.

set -e y set -u

Dos opciones que activo al principio de casi cualquier script de uso habitual:

#!/usr/bin/env bash
set -e   # sale si un comando devuelve error
set -u   # trata variables no definidas como error

set -e evita que el script continúe alegremente después de un fallo: sin él, si un comando a mitad del script falla, los siguientes se ejecutan igualmente. set -u detecta typos en nombres de variables antes de que causen problemas más adelante.

Un matiz importante: set -e no interrumpe el script si el comando fallido forma parte de una condición if, del lado derecho de ||, o de la condición de un while. Vale la pena entender el comportamiento real, no asumir que el script es infalible solo por activar esta opción.

Redirigir errores correctamente

Los mensajes de error deben ir al stderr (>&2), no al stdout. Eso permite usar el script en pipelines sin mezclar la salida útil con los mensajes de diagnóstico:

if ! rsync -av "$ORIGEN" "$DESTINO"; then
  echo "ERROR: rsync falló. Revisa $LOGFILE" >&2
  exit 1
fi

Cuándo Bash no es la herramienta adecuada

Bash es ideal para encadenar comandos del sistema, mover archivos, lanzar procesos y automatizar secuencias de operaciones de shell. Es el pegamento natural del homelab. Pero tiene límites claros, y conviene reconocerlos antes de meterle 500 líneas a un .sh.

Cuando el script empieza a necesitar estructuras de datos complejas, parseo de JSON o XML, llamadas a APIs web, o lógica condicional muy profunda, el código Bash se vuelve frágil y difícil de mantener. En ese punto Python suele ser la opción más sensata:

  • Manejo nativo de JSON con la librería estándar
  • Tipos de datos reales y manejo de errores más explícito
  • Mucho más fácil de testear unitariamente
  • Librerías para casi cualquier API o protocolo

Una regla práctica que uso: si el script necesita parsear la salida de un comando más de dos veces con awk o sed, probablemente Python lo haría en menos líneas y con menos sorpresas. Bash no es un lenguaje de propósito general; tiene su dominio, y respetarlo hace el código más mantenible a largo plazo.

Por otro lado, zsh, fish y otros shells tienen sus propias virtudes y casos de uso. Bash es el estándar de facto en scripts de sistema por su presencia universal en Linux, pero eso no lo convierte en la única opción válida para todo.

Preguntas frecuentes

Q: ¿Vale Bash para automatizar mi homelab de Proxmox?

A: Para tareas repetitivas como snapshots, backups o arrancar/parar VMs funciona muy bien. Donde Bash muestra sus límites es en lógica compleja o manejo de datos estructurados: ahí Python escala mejor. Para la mayoría de automatizaciones domésticas de homelab, Bash es más que suficiente.

Q: ¿Cómo ejecuto un script automáticamente cada noche con cron?

A: Añade una línea en crontab con el formato de 5 campos: '0 2 * * * /ruta/a/tu/script.sh' lo ejecuta cada día a las 2:00. Usa 'crontab -e' para editarlo y asegúrate de que el script tenga permisos de ejecución con 'chmod +x'.

Q: ¿Qué pasa si corro un script Bash con sudo sin revisarlo?

A: Riesgo real: un script con sudo puede borrar archivos del sistema, modificar configuraciones críticas o abrir puertas traseras si contiene código malicioso. Antes de ejecutar cualquier script con privilegios de root, léelo entero y entiende qué hace cada línea.

Q: ¿Por qué mi script funciona en Ubuntu pero no en Alpine o Debian?

A: Bash extiende el estándar POSIX con características propias que no están disponibles en shells más ligeros como dash, que usan algunas distros por defecto. Si necesitas portabilidad, usa '#!/bin/sh' y cíñete a POSIX puro, o especifica siempre '#!/bin/bash' y verifica que Bash está instalado en el sistema destino.

Q: ¿Cuándo tiene más sentido pasarme a Python en vez de Bash?

A: Cuando tu script supera las 50-100 líneas, necesitas manipular JSON, hacer peticiones HTTP, o la lógica condicional se vuelve difícil de seguir. Bash brilla en encadenar comandos del sistema y automatizaciones simples; Python es más adecuado en cuanto aparece lógica de negocio real o estructuras de datos complejas.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *