Solución de problemas comunes de configuración de scripts Bash
Domina el arte de solucionar problemas de configuración en scripts Bash. Esta guía detalla técnicas esenciales de depuración, centrándose en dependencias del entorno, errores comunes de sintaxis como comillas incorrectas y división de palabras, y fallos críticos de ejecución. Aprende a usar banderas robustas (`set -euo pipefail`), manejar errores de análisis de argumentos y resolver problemas comunes como finales de línea DOS y variables PATH incorrectas, asegurando que tus scripts de automatización se ejecuten de manera confiable en cualquier entorno.
Solución de problemas comunes de configuración de scripts Bash
Los problemas de configuración de Bash suelen manifestarse de forma vaga: un script funciona desde tu terminal pero falla en cron, un script de despliegue no encuentra kubectl, o una ruta de archivo de configuración con un espacio solo falla para un cliente. El error a menudo no está en la lógica principal. Está en las suposiciones sobre el entorno, los argumentos, las comillas, los permisos o el shell que realmente ejecutó el archivo.
Cuando soluciono problemas de un script Bash, primero intento responder cuatro preguntas: ¿Qué shell lo está ejecutando? ¿Qué entorno recibió? ¿Qué entradas analizó? ¿Qué comando falló primero? Ese orden evita que persigas síntomas.
Confirmar el shell y el contexto de ejecución
Un script que comienza con sintaxis de Bash pero se ejecuta bajo sh puede fallar de formas extrañas. Los arrays, [[ ... ]], source, la sustitución de procesos y set -o pipefail son características de Bash. Si el archivo las usa, el shebang debe indicar Bash:
#!/usr/bin/env bash
Luego ejecútalo de la misma manera que tu automatización lo ejecuta. Estos no son equivalentes:
./deploy.sh
bash deploy.sh
sh deploy.sh
./deploy.sh usa el shebang. bash deploy.sh fuerza Bash. sh deploy.sh puede usar dash, BusyBox ash u otro shell dependiendo del sistema. Si la producción llama a sh deploy.sh, un shebang perfecto de Bash no ayudará.
Cron, systemd, runners de CI, comandos forzados de SSH y puntos de entrada de Docker proporcionan entornos diferentes. Un script que funciona de forma interactiva puede fallar porque tu shell de inicio estableció PATH, AWS_PROFILE, NVM_DIR o un gestor de versiones de lenguaje antes de que lo ejecutaras.
Agrega un bloque de diagnóstico temporal cerca de la parte superior:
printf 'shell=%s\n' "$BASH_VERSION" >&2
printf 'user=%s pwd=%s\n' "$(id -un)" "$PWD" >&2
printf 'PATH=%s\n' "$PATH" >&2
Elimínalo o condiciónalo una vez que tengas la respuesta. Los diagnósticos son útiles, pero filtrar valores del entorno en los registros puede exponer secretos.
Usar el modo estricto con cuidado, no a ciegas
set -euo pipefail es un buen valor predeterminado para muchos scripts de automatización, pero tiene casos límite. set -u detecta variables faltantes. pipefail hace visibles los fallos de tuberías. set -e se detiene después de muchos fallos de comandos, aunque se comporta de manera diferente dentro de condicionales, tuberías y comandos compuestos de lo que los nuevos usuarios de Bash esperan.
Un punto de partida práctico es:
set -Eeuo pipefail
trap 'printf "Error en la línea %s: %s\n" "$LINENO" "$BASH_COMMAND" >&2' ERR
Úsalo cuando un comando fallido deba detener el script. No lo uses casualmente en scripts que intencionalmente prueban comandos y continúan. Para fallos esperados, escribe la condición explícitamente:
if ! grep -q '^enabled=true$' "$config_file"; then
printf 'La función está deshabilitada.\n'
fi
Eso es más claro que dejar que grep falle bajo set -e y preguntarse por qué el script salió.
Validar argumentos antes de leer archivos
Un error común de configuración es tratar $1 como presente cuando no lo está. Bajo set -u, hacer referencia a un $1 faltante sale inmediatamente. Sin set -u, se convierte en una cadena vacía.
Usa un pequeño bloque de uso:
usage() {
printf 'Uso: %s <archivo-config> [entorno]\n' "${0##*/}" >&2
}
if (( $# < 1 )); then
usage
exit 2
fi
config_file=$1
environment=${2:-dev}
if [[ ! -r $config_file ]]; then
printf 'El archivo de configuración no es legible: %s\n' "$config_file" >&2
exit 1
fi
Observa el valor predeterminado para environment, pero no para config_file. Los valores predeterminados son útiles para valores opcionales y peligrosos para valores requeridos. Un script no debería recurrir silenciosamente a ./config.yml para un despliegue de producción a menos que ese comportamiento sea muy deliberado.
Poner entre comillas rutas y valores de configuración
La mayoría de los scripts Bash eventualmente leen una ruta de un archivo de configuración o variable de entorno. Si ese valor no está entre comillas, Bash realiza división de palabras y expansión de globos.
backup_dir="/mnt/backups/May reports"
# Incorrecto: se convierte en múltiples argumentos.
cp $backup_dir/latest.tar.gz /restore/
# Correcto.
cp "$backup_dir/latest.tar.gz" /restore/
La misma regla se aplica a las sustituciones de comandos:
release_name=$(git describe --tags --always)
printf 'Desplegando %s\n' "$release_name"
Si intencionalmente necesitas múltiples argumentos, usa un array en lugar de una cadena:
rsync_opts=(-a --delete --exclude '.git')
rsync "${rsync_opts[@]}" "$src/" "$dest/"
Esto evita el patrón frágil de opts="-a --delete" seguido de rsync $opts ....
Verificar PATH y dependencias de comandos externos
command not found suele ser un problema de contexto. Tu terminal puede encontrar aws en /opt/homebrew/bin/aws, mientras que cron solo tiene /usr/bin:/bin.
Al inicio, verifica las herramientas requeridas:
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'Comando requerido no encontrado: %s\n' "$1" >&2
exit 127
}
}
require_cmd docker
require_cmd jq
require_cmd aws
Para utilidades críticas del sistema, las rutas absolutas pueden estar bien. Para herramientas de desarrollador instaladas en diferentes lugares, una verificación de dependencia con un error claro suele ser más fácil de mantener.
Si un script es lanzado por systemd, establece el entorno en la unidad o en un archivo de entorno en lugar de confiar en el .bashrc de un usuario. Los shells no interactivos no necesariamente leen los mismos archivos de inicio que tu terminal.
Analizar variables de entorno explícitamente
La configuración impulsada por el entorno es conveniente, pero vacío y no establecido no siempre son lo mismo. La expansión de parámetros de Bash te permite ser preciso:
: "${APP_ENV:?APP_ENV debe estar establecido}"
log_level=${LOG_LEVEL:-INFO}
${APP_ENV:?mensaje} falla si la variable no está establecida o está vacía. ${LOG_LEVEL:-INFO} usa un valor predeterminado si no está establecida o está vacía. Si una cadena vacía es significativa en tu script, usa las formas sin los dos puntos, como ${VAR-default}.
Evita volcar todo el entorno en los registros mientras solucionas problemas. Es demasiado fácil imprimir tokens, contraseñas de bases de datos o credenciales en la nube.
Cuidado con los finales de línea CRLF y caracteres invisibles
Un script editado en Windows puede contener finales CRLF. El síntoma clásico es un error que contiene ^M, o un fallo de shebang que parece que el intérprete no existe.
Verifica con:
file deploy.sh
sed -n 'l' deploy.sh | head
Arregla con uno de estos:
dos2unix deploy.sh
# o, si dos2unix no está disponible:
sed -i 's/\r$//' deploy.sh
También verifica los valores de configuración copiados en busca de espacios finales. Una variable que parece prod pero en realidad es prod puede fallar en una rama case y hacerte dar vueltas.
Depurar el primer comando que falla
set -x muestra los comandos después de la expansión. Eso es exactamente lo que necesitas para errores de comillas y configuración:
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
# sección que falla aquí
set +x
No habilites xtrace alrededor de secretos. Si tu script maneja contraseñas, tokens, URLs firmadas o claves privadas, rastrea solo la sección estrecha que necesitas.
Para archivos de configuración, imprime el valor resuelto y la prueba que estás a punto de aplicar:
printf 'Usando config_file=%q\n' "$config_file" >&2
[[ -r $config_file ]] || exit 1
%q es útil para depurar porque hace que los espacios en blanco sean visibles de una manera amigable para el shell.
Manejar permisos también como configuración
A veces el script es correcto, pero la cuenta que lo ejecuta no puede leer la configuración, ejecutar el ayudante o escribir el directorio de salida.
Verifica el usuario real:
id
namei -l "$config_file"
namei -l es especialmente útil porque cada directorio en la ruta necesita permiso de ejecución. Un archivo legible dentro de un directorio padre inaccesible sigue siendo inaccesible.
Para scripts ejecutables, establece permisos y finales de línea juntos durante el empaquetado o la construcción de la imagen:
chmod 0755 /usr/local/bin/deploy
Si un script solo funciona con sudo, identifica qué archivo o comando necesita privilegio. No ejecutes todo el script como root solo para cubrir una mala configuración de propiedad.
Una pasada de solución de problemas confiable
Cuando un problema de configuración de Bash no está claro, ejecuta esta pasada en orden:
- Confirma que el script se está ejecutando bajo Bash si usa características de Bash.
- Imprime el directorio de trabajo, el usuario y
PATHpara el contexto que falla. - Valida los argumentos requeridos y los archivos de configuración antes de la lógica principal.
- Pon entre comillas cada expansión a menos que intencionalmente quieras división.
- Verifica los comandos externos requeridos con
command -v. - Usa
set -xsolo alrededor de la sección que falla, con secretos protegidos. - Verifica permisos y finales de línea antes de cambiar la lógica de negocio.
Esa secuencia atrapa la mayoría de los fallos del mundo real sin convertir el script en una novela de misterio. Bash es pequeño, pero su contexto de ejecución es grande; soluciona el contexto primero.
Separar la carga de configuración de la ejecución
Un script es más fácil de solucionar cuando cargar la configuración es su propio paso. No leas un archivo, exportes variables, crees directorios y reinicies servicios todo en un bloque largo. Primero resuelve los valores. Luego valídalos. Luego ejecuta el trabajo.
load_config() {
local file=$1
[[ -r $file ]] || {
printf 'No se puede leer la configuración: %s\n' "$file" >&2
return 1
}
# Ejemplo para un archivo KEY=VALUE deliberadamente simple.
# No hagas source de archivos en los que no confíes completamente.
while IFS='=' read -r key value; do
[[ -z $key || $key == \#* ]] && continue
case $key in
APP_PORT) APP_PORT=$value ;;
APP_ENV) APP_ENV=$value ;;
*) printf 'Ignorando clave de configuración desconocida: %s\n' "$key" >&2 ;;
esac
done < "$file"
}
Hacer source de un archivo de configuración con . config.env es común, pero ejecuta código de shell. Eso es aceptable solo cuando el archivo es confiable y propiedad como código. Para configuración editable por el usuario, analiza solo las claves que soportas.
Hacer que los fallos sean procesables para el próximo operador
Un buen mensaje de error dice qué falló y qué valor lo causó. Compara estos:
printf 'Error\n' >&2
y:
printf 'No se puede escribir el directorio de respaldo: %s\n' "$backup_dir" >&2
El segundo mensaje le da a la próxima persona algo que verificar. Esto importa en scripts de DevOps porque la persona que ve el fallo puede no ser el autor. Puede estar de guardia, medio dormido y mirando registros de CI de un despliegue fallido.
Los códigos de salida también pueden tener significado. Usa 2 para problemas de uso, 1 para fallos generales de tiempo de ejecución y códigos específicos de herramientas cuando tengas una razón documentada. No pases todo el día inventando una taxonomía, pero evita devolver éxito después de una validación fallida solo porque el script imprimió una advertencia.
Probar el contexto que falla, no tu contexto favorito
Si systemd ejecuta el script, pruébalo con systemd. Si cron lo ejecuta, pruébalo con un entorno reducido. Una aproximación rápida es:
env -i HOME="$HOME" PATH=/usr/bin:/bin bash ./script.sh config.env
Eso elimina la manta de seguridad de tu shell interactivo. Las exportaciones faltantes y las suposiciones de PATH aparecen rápidamente.
Para scripts de punto de entrada de Docker, ejecuta la imagen con el mismo entorno y montajes que la producción lo más cerca posible:
docker run --rm --env-file app.env -v "$PWD/config:/config:ro" my-image:tag
Si falla solo en CI, imprime el directorio de trabajo del runner de CI y la línea de comando exacta. Muchos fallos de Bash en CI son solo rutas relativas incorrectas después del checkout, no problemas profundos de shell.
Una pasada de revisión del mundo real antes de enviar
Antes de dar por terminado un script o una configuración de contenedor, léelo una vez como si fueras la próxima persona que tiene que depurarlo a las 2 a.m. Eso cambia lo que notas. Un mensaje que tenía sentido mientras escribías el script puede ser ambiguo cuando aparece en un registro de CI. Un nombre de servicio de Docker que parecía obvio puede no coincidir con el nombre de la variable en la aplicación. Un valor predeterminado de Bash puede ser seguro para desarrollo y peligroso para producción.
Me gusta hacer una prueba en seco corta con valores deliberadamente incómodos. Usa una ruta con espacios. Usa un valor opcional vacío. Prueba un nombre de archivo que comience con un guión. Ejecuta el script desde un directorio de trabajo diferente. Inicia el contenedor sin una variable de entorno esperada. Estas pruebas no son sofisticadas, pero detectan las suposiciones que generalmente fallan primero.
También verifica el mensaje de fallo. Si la única salida es falló, el consejo del artículo no ha llegado a la implementación. Un fallo útil dice qué valor se usó, qué verificación falló y qué puede cambiar el operador. Eso no significa volcar cada variable de entorno o imprimir secretos. Significa ser específico donde la especificidad ayuda: la ruta de configuración, el nombre del comando faltante, el nombre de la red, el nombre de host del servicio o el puerto que el proceso intentó enlazar.
El hábito final es mantener los ejemplos cerca de la forma en que el sistema se ejecuta realmente. Si la producción usa Compose, prueba con Compose. Si un script es lanzado por systemd, pruébalo con systemd o con un entorno igualmente mínimo. Si se supone que un comando es seguro para copiar y pegar, incluye las comillas, los separadores -- y la validación en el ejemplo mismo. Los lectores copian patrones de trabajo más a menudo de lo que copian advertencias.
Esa pasada de revisión no es burocracia. Es cómo la automatización pequeña se mantiene aburrida. Aburrido es lo que quieres de los mensajes del shell, los cargadores de configuración, la expansión de variables, los diagnósticos de contenedores y las redes de Docker. Cuanto menos sorprendente sea el comportamiento, más fácil será para el próximo operador confiar en él.