
Depurar scripts bash es como buscar una aguja en un pajar, especialmente cuando nuevas adiciones aparecen en una base de código existente sin discutir oportunamente cuestiones de estructura, registro y confiabilidad. En tales situaciones, uno puede encontrar problemas tanto por errores propios como por gestionar complejas acumulaciones de scripts.
Comando he traducido un artículo con recomendaciones que te ayudarán a escribir, depurar y mantener mejor tus scripts. Créelo o no, pero no hay nada como la satisfacción de escribir código bash limpio y listo para usar que funcione cada vez.
En el artículo, el autor comparte lo que ha aprendido en los últimos años, así como algunos errores comunes que lo han sorprendido. Esto es importante porque cada desarrollador de software, en algún momento de su carrera, trabaja con scripts para automatizar tareas de trabajo rutinarias.
Manejadores de señales
La mayoría de los scripts bash con los que me he encontrado nunca utilizan un mecanismo efectivo de limpieza cuando algo inesperado sucede durante la ejecución del script.
Los imprevistos pueden surgir desde el exterior, como la recepción de una señal del núcleo. Manejar tales casos es extremadamente importante para que los scripts sean lo suficientemente confiables como para ejecutarse en sistemas de producción. Suelo usar manejadores de salida para reaccionar a tales escenarios:
function handle_exit() {
// Agrega el código de limpieza aquí
// por ejemplo, rm -f "/tmp/${lock_file}.lock"
// salir con un código de estado apropiado
}
// trampa
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trampa — es un comando incorporado de la shell que te ayuda a registrar una función de limpieza que se llama en caso de recibir alguna señal. Sin embargo, hay que tener especial cuidado con manejadores como SIGINT, que interrumpe el script.
Además, en la mayoría de los casos solo se deben atrapar EXIT, pero la idea es que realmente puedes personalizar el comportamiento del script para cada señal individual.
Las funciones incorporadas set — finalización rápida en caso de error
Es muy importante reaccionar a los errores tan pronto como ocurren y detener la ejecución rápidamente. No hay nada peor que seguir ejecutando un comando como este:
rm -rf ${directory_name}/*
Tenga en cuenta que la variable directory_name no está definida.
Para manejar tales escenarios, es importante utilizar funciones integradas set, como set -o errexit, set -o pipefail o set -o nounset al comienzo del script. Estas funciones garantizan que su script se detenga tan pronto como encuentre cualquier código de salida distinto de cero, uso de variables no definidas, comandos incorrectos pasados por canal, y así sucesivamente:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
function print_var() {
echo "${var_value}"
}
print_var
$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable
Nota: funciones integradas, como set -o errexit, saldrán del script tan pronto como aparezca un código de salida 'no manejado' (excepto cero). Por lo tanto, es mejor introducir un manejo de errores personalizado, como:
#!/bin/bash
error_exit() {
line=$1
shift 1
echo "ERROR: non zero return code from line: $line -- $@"
exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code
Este tipo de escritura de scripts le hace estar más atento al comportamiento de todos los comandos en el script y anticipar la posibilidad de errores antes de que lo sorprendan.
ShellCheck para identificar errores durante el desarrollo
Vale la pena integrar algo como en sus flujos de desarrollo y prueba para verificar su código bash en la aplicación de las mejores prácticas.
Lo utilizo en mis entornos locales de desarrollo para obtener informes sobre sintaxis, semántica y algunos errores en el código que podría haber pasado por alto durante el desarrollo. Es una herramienta de análisis estático para sus scripts bash, y la recomiendo encarecidamente.
Uso de sus códigos de salida
Los códigos de retorno en POSIX no son solo cero o uno, sino cero o un valor distinto de cero. Utilice estas oportunidades para devolver códigos de error personalizados (entre 201-254) para diferentes casos de error.
Esta información puede ser luego utilizada por otros scripts que envuelven el suyo, para entender con precisión qué tipo de error ocurrió y reaccionar de manera adecuada:
#!/usr/bin/env bash
SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241
function read_file() {
if ${file_not_found}; then
return ${FILE_NOT_FOUND}
fi
}
Nota: tenga cuidado especial con los nombres de las variables que define, para evitar la redefinición accidental de variables de entorno.
Funciones logger
Un registro bonito y estructurado es importante para comprender fácilmente los resultados de la ejecución de su script. Al igual que en otros lenguajes de programación de alto nivel, siempre uso en mis scripts de bash funciones de registro propias, como __msg_info, __msg_error y así sucesivamente.
Esto ayuda a asegurar una estructura de registro estandarizada, haciendo cambios solo en un lugar:
#!/usr/bin/env bash
function __msg_error() {
[[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}
function __msg_debug() {
[[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}
function __msg_info() {
[[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}
__msg_error "File could not be found. Cannot proceed"
__msg_debug "Starting script execution with 276MB of available RAM"
Normalmente trato de tener en mis scripts algún mecanismo __init, donde se inicializan o establecen en valores predeterminados tales variables de registro y otras variables del sistema. Estas variables también pueden ser establecidas a partir de parámetros de la línea de comandos al ejecutar el script.
Por ejemplo, algo como:
$ ./run-script.sh --debug
Cuando se ejecuta tal script, se garantiza que la configuración del sistema esté establecida en los valores predeterminados, si son obligatorios, o al menos inicializada con algo apropiado, si es necesario.
Normalmente baso mi elección sobre qué inicializar y qué no en un compromiso entre la interfaz de usuario y los detalles de la configuración que el usuario puede/debe revisar.
Arquitectura para reutilización y estado limpio del sistema
Código modular/reutilizable
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
Mantengo un repositorio separado que se puede utilizar para inicializar un nuevo proyecto/script de bash que deseo desarrollar. Todo lo que se puede reutilizar puede ser guardado en el repositorio y utilizado en otros proyectos que quieran aprovechar esas funcionalidades. Esta organización de proyectos reduce significativamente el tamaño de otros scripts y también garantiza que la base de código sea pequeña y fácil de probar.
Como en el ejemplo anterior, todas las funciones de registro, como __msg_info, __msg_error y otras, como los informes de Slack, se almacenan por separado en common/* y se conectan dinámicamente en otros scripts, como daily_database_operation.sh.
Deje un sistema limpio detrás de usted
Si está descargando algún recurso durante la ejecución del script, se recomienda almacenar todos esos datos en un directorio común con un nombre aleatorio, como /tmp/AlRhYbD97/*Puedes usar generadores de texto aleatorio para elegir el nombre del directorio:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Después de completar el trabajo, la limpieza de tales directorios puede ser asegurada en los manejadores de señales discutidos anteriormente. Si no se ocupa de la eliminación de directorios temporales, se acumulan y, en algún momento, causan problemas inesperados en el host, como un disco lleno.
Uso de archivos lock
A menudo es necesario asegurar que solo una instancia del script se ejecute en el host en cualquier momento. Esto se puede lograr mediante archivos lock.
Normalmente creo archivos lock en /tmp/project_name/*.lock y verifico su existencia al inicio del script. Esto ayuda a finalizar correctamente el script y evita cambios inesperados en el estado del sistema por otro script que se esté ejecutando en paralelo. No se necesitan archivos lock si es necesario que el mismo script se ejecute en paralelo en el host.
Medir y mejorar
A menudo tenemos que trabajar con scripts que se ejecutan durante un largo período, como operaciones diarias con bases de datos. Tales operaciones generalmente incluyen una secuencia de pasos: carga de datos, verificación de anomalías, importación de datos, envío de informes de estado, etc.
En tales casos, siempre trato de dividir el script en pequeños scripts individuales y reportar su estado y tiempo de ejecución con:
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
Más tarde puedo ver el tiempo de ejecución con:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Esto me ayuda a identificar áreas problemáticas/lentas en los scripts que necesitan optimización.
¡Buena suerte!
Qué más leer:
Fuente: habr.com
