Cuando el kernel ejecuta un archivo, lee los primeros dos bytes para identificar el tipo de contenido. Si encuentra #!, toma el resto de esa primera línea como ruta al intérprete y le pasa el archivo como argumento. Eso es el shebang: no es azúcar sintáctico ni una convención del shell, sino un mecanismo del propio kernel implementado en fs/binfmt_script.c. Consecuencia directa: el shebang solo funciona en archivos que el kernel ejecuta directamente, no en scripts que llamas explícitamente con bash script.sh.
La distinción entre #!/bin/bash y #!/bin/sh importa más de lo que parece. En Debian, /bin/sh es un enlace simbólico a dash, no a bash. Dash implementa POSIX sh y nada más: sin arrays, sin [[ ]], sin expansión de llaves, sin sustitución de procesos. Si tu script usa cualquiera de esas características y pones #!/bin/sh, fallará en tiempo de ejecución con errores crípticos. Usa #!/bin/bash siempre que necesites sintaxis específica de bash.
Las variables se asignan como nombre="valor" sin espacios alrededor del =. No es una preferencia de estilo: bash interpreta nombre = "valor" como el comando nombre con los argumentos = y "valor", y falla inmediatamente. Para usar el valor, $nombre funciona, pero ${nombre} es preferible porque delimita explícitamente el nombre de la variable, lo que evita ambigüedades en expansiones como ${nombre}_sufijo (sin llaves, bash buscaría una variable llamada nombre_sufijo).
Las comillas simples congelan el texto literalmente: ningún carácter especial se interpreta dentro de ellas, incluyendo $ y \. Las comillas dobles permiten la expansión de variables y la sustitución de comandos con $(comando), que ejecuta el comando en un subshell y sustituye la expresión por su salida estándar.
Los exit codes son el mecanismo de comunicación entre procesos: 0 significa éxito, cualquier valor entre 1 y 255 significa fallo. La convención es 1 para error general, 2 para uso incorrecto del comando (argumentos inválidos). $? contiene el exit code del último comando ejecutado en primer plano. Si no controlas los exit codes de tu script, el caller no puede saber qué pasó.
El problema con un script sin opciones de seguridad es que falla silenciosamente: un comando falla, bash continúa en la siguiente línea, y el script llega al final con exit 0. Tres opciones cambian ese comportamiento. set -e hace que el script termine en cuanto cualquier comando devuelva un exit code distinto de cero. set -u trata las variables no definidas como error en lugar de expandirlas a cadena vacía, lo que evita desastres como rm -rf /${dir_no_definido}/. set -o pipefail hace que un pipeline falle si cualquier comando dentro de él falla, no solo el último: sin esta opción, comando_que_falla | grep algo devuelve el exit code de grep, enmascarando el fallo del primer comando. La combinación set -euo pipefail al inicio de cualquier script no trivial es la forma más compacta de activar los tres.
#!/bin/bash
# Gestión de backup diario: copia archivos de configuración y registra el resultado
set -euo pipefail
# --- Variables ---
origen="/etc/nginx"
destino="/var/backups/nginx"
fecha=$(date +%Y-%m-%d) # Sustitución de comando: la salida de date se asigna
archivo="${destino}/nginx-${fecha}.tar.gz" # Las llaves son necesarias: separan la variable del literal
# Directorio de destino con permisos solo para root
mkdir -p "${destino}"
chmod 700 "${destino}"
# Comillas dobles en las variables: imprescindible si las rutas contienen espacios
tar -czf "${archivo}" "${origen}"
# $? aquí sería 0 porque set -e ya habría abortado si tar falló.
# Lo útil es capturar el código en ramas donde toleras el fallo explícitamente.
if tar -tzf "${archivo}" > /dev/null 2>&1; then
echo "Backup verificado: ${archivo}"
else
# Salida con código 1: error general. El caller (cron, systemd) puede detectarlo.
echo "Error: el archivo de backup está corrupto o vacío" >&2
exit 1
fi
# Retención: elimina backups con más de 30 días
# La tubería usa pipefail: si find falla (p. ej. el directorio no existe), el script para
find "${destino}" -name "nginx-*.tar.gz" -mtime +30 | xargs -r rm -f
echo "Proceso completado: $(date)"
El shebang #!/bin/bash en la primera línea no es negociable para este script: usa $(), variables con ${}, y opciones que en dash o sh se comportan de forma diferente o no existen. La línea set -euo pipefail en la segunda línea ejecutable define el contrato de fiabilidad de todo lo que viene después.
fecha=$(date +%Y-%m-%d) ilustra la sustitución de comando: bash abre un subshell, ejecuta date +%Y-%m-%d, captura su stdout, y asigna esa cadena a fecha. La asignación no lleva espacios alrededor del =; si los pusieras, bash intentaría ejecutar fecha como un comando.
La variable archivo usa ${destino} y ${fecha} con llaves. Sin ellas, $destino_ o $fecha.tar.gz harían que bash buscara variables llamadas destino_ y fecha.tar.gz, ambas indefinidas; con set -u activo, el script fallaría inmediatamente con un error claro.
Las comillas dobles alrededor de "${archivo}" y "${origen}" en los comandos tar, mkdir, y chmod son defensivas: si alguna variable contiene espacios o caracteres especiales, las comillas evitan que bash los interprete como separadores de argumentos. Con comillas simples, ${archivo} no se expandiría en absoluto.
El bloque if tar -tzf ... ; then muestra el uso correcto de $? implícito: la condición del if captura el exit code del comando directamente. Usar comando; if [ $? -eq 0 ] es redundante y propenso a errores porque otro comando podría sobreescribir $? entre la ejecución y la comprobación.
La línea de find ... | xargs -r rm -f demuestra por qué pipefail importa: si find fallara (directorio inexistente, permisos insuficientes), sin pipefail el exit code del pipeline sería el de xargs, que podría ser 0. Con pipefail activo, el fallo de cualquier etapa del pipeline propaga el error y set -e aborta el script antes de ejecutar una limpieza sobre datos incorrectos.
[Ubuntu]: En Ubuntu, /bin/sh también apunta a dash, por lo que la advertencia sobre #!/bin/sh aplica igual. La diferencia relevante es que algunos scripts del sistema en Ubuntu asumen dash para velocidad; en scripts propios, esto no cambia nada.
N° 73