Depuración de Bash
(wizardzines.com)- Cuando un script de Bash funciona de forma distinta a la esperada, ver exactamente los comandos que se ejecutan ya facilita rastrear la causa
set -ximprime cada línea después de expandir las variables, para que puedas comprobar qué comandos está ejecutando realmente el script- Si lo ejecutas desde la línea de comandos con
bash -x script.sh, produce el mismo efecto que ponerset -xal inicio descript.sh - Si usas juntos el trap
DEBUGyread, puedes detenerte antes de ejecutar cada línea y revisar el nombre del archivo, el número de línea y el siguiente comando - La función
die() { echo $1 >&2; exit 1; }simplifica el flujo de mostrar un mensaje de error estándar y salir después de un comando fallido
Confirmar visualmente el flujo de ejecución
set -ximprime las líneas que ejecuta el script, y muestra las variables con sus valores ya expandidos- Puedes usarlo poniendo
set -xal inicio del script - El mismo comportamiento también se puede ejecutar desde la línea de comandos
$ bash -x script.sh- Esto equivale a poner
set -xal principio descript.sh
Detenerse y revisar línea por línea
- El trap
DEBUGse ejecuta antes de que se ejecute cada línea de código - Si agregas el siguiente código al inicio del script, esperará a que presiones Enter antes de ejecutar el siguiente comando
trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUGread -pmuestra un mensaje y espera a que se presione Enter$BASH_SOURCEes el nombre del archivo del script$LINENOes el número de línea$BASH_COMMANDes el comando que se ejecutará a continuación
Dejar un mensaje al fallar y salir
- La función
diepuede usarse para mostrar un mensaje y terminar el programa cuando un comando falladie() { echo $1 >&2; exit 1; }- Basta con agregarla después de un comando que pueda fallar, por ejemplo
some_command || die "¡oh no!"
- Esta función envía el mensaje a stderr y termina con
exit 1
1 comentarios
Comentarios de Hacker News
Hay una función de logging
zdebugrepartida por el código: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...Si activas el logging de depuración y presionas Ctrl-T en el menú principal, aparece una pantalla como esta: https://i.imgur.com/Ge75zkP.png
También tienen perfilado con flamegraphs que se puede activar con https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme..., y si vuelves a ensamblar los datos volcados por el puerto serial puedes generar un gráfico como https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master...
Bash es sorprendentemente flexible
set -x,PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'es muy útilAsí se muestran el nombre del archivo, la función y el número de línea, lo que ayuda bastante al depurar scripts grandes de Bash
shellcheck. Aunque no detecte directamente el problema, sí señala problemas potencialesTambién recomiendan reescribir el script en otro lenguaje. En su empresa están reemplazando scripts de Bash por Rust; el costo de entrada es alto, pero el código resultante es mucho más fácil de mantener y más confiable
Bash sigue siendo bueno para scripts rápidos, pero si pasas de unas 100 líneas, ya vale la pena usar un lenguaje con garantías más fuertes
die()es una función auxiliar que imprime un mensaje de error en stderr y luego termina con el código de error indicadoAquí hay más códigos de salida y funciones auxiliares para scripts de shell: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
Aun así, no me convence del todo la filosofía de ese
die. Idealmente, una funcióndiedebería propagar el código de salida del comando que falló y tampoco ocultar la salida de error de ese comandoSi en un script grande quiero dar yo mismo un significado al fallo de un comando, usaría otro
diemás específico. Midiesería más o menos__errex "$?" "${LINENO}" "$0", para imprimir un error fatal, el número de línea, el nombre del script y el mensaje, y terminar con ese código de salidaAquí hay un ejemplo de implementación: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
Si lo usas como
some-command || fail "message", genera un stack trace y termina el shell cuandosome-commanddevuelve un estado de salida distinto de 0Si quieres generar un stack trace dentro de una función y devolverlo, puedes usar algo como
some-command || softfail "message" || return $?Puede hacer lo necesario, pero es tosco y su sintaxis es terrible. Cuando un script alcanza cierto tamaño o complejidad, prácticamente te obliga a moverlo a un lenguaje de verdad, así que hasta parece un diseño intencional
En una distro moderna normalmente tienes una versión bastante reciente de Bash, y salvo que quieras usar cosas como arreglos, tampoco hace falta preocuparse demasiado por la versión
El atractivo de Bash está en el lugar que ocupa entre otros lenguajes y herramientas. Es ideal para conectar otras herramientas y está lo bastante cerca del sistema operativo como para ser cómodo, sin requerir instalación de librerías como Python
A menudo se oye que el scripting más complejo debería pasarse a lenguajes como Python, pero eso añade una capa de complejidad que a la larga puede no ayudar. Un script de Bash escrito hace 20 años probablemente sigue funcionando bien, mientras que un programa de Python de hace 20 años seguramente tendrá problemas de versión
El
rcde Plan 9 es más limpio, pero “algo parecido, solo que más limpio” no hace que nadie cambie. Ahora mismo puedes instalar algo parecido pero mejor desde https://pkgsrc.se/shells, y aun así no lo usas, ni tampoco cambia la manera en que otras personas ejecutan cosasPara reemplazar una tecnología establecida, tiene que ser varias veces mejor en aspectos clave. Plan 9 también era mejor que los sistemas tipo UNIX, pero no lo bastante como para reemplazarlos
Es difícil crear algo lo bastante bueno como para reemplazar el nicho del scripting del shell Bourne. Antes de llegar a ese punto, ya te pasaste al terreno ecológico o al espacio de problemas de lenguajes de scripting de verdad como Perl, Python o Ruby
En dominios estrechos, un óptimo local absorbe todo el aire, haciendo difícil que aparezca un competidor más cercano al óptimo global teórico
Aunque las funciones agregadas recientemente se han acoplado bien sobre
sh/Bash, al final el scripting de shell es un medio para un fin y debería evolucionar mucho más lento que un lenguaje de programación generalLa característica central de Bash/
shes que es anti-entrópico. Casi no hay desarrollo ni evolución, así que es menos probable tener dolores de cabeza por dependencias o funciones nuevas, y las cosas que funcionaban hace 20 años siguen siendo herramientas básicasPor diseño se vuelve un sistema reacio al cambio, y cuando la gente se topa con sus límites, surge el incentivo de salirse de ahí
La mayoría de los scripts de FreeBSD están escritos para
sh, y siento queshtiene mucho más soporte porque es parte del estándar POSIX. Bash solo me parece popularPero Bash era tan malo que terminé haciendo un montón de utilidades con menos espacio de nombres para poder usar scripts de Groovy. Se podían desarrollar con un IDE, el sistema de bibliotecas era seguro, y Groovy pulía casi todas las incomodidades de Java, así que era mucho mejor
gdbbastante potente: https://bashdb.sourceforge.net/Tiene algunas limitaciones, pero en general puede ser útil: https://github.com/ketancmaheshwari/pd
die()está bien, pero Bash tiene una característica irritante. Si intentas hacerexitdentro de un subshell, solo termina el subshell y el resto del script sigue ejecutándosePor ejemplo, si llamas a
diedentro de un pipeline comocat myfile | while read line; do ... die "Found match" ... done, elecho "I don't want this line"de después igual se imprimeMuchas veces se puede evitar el subshell, y en este ejemplo
shellchecktiene razón al marcar UUOC, y corregir eso también resuelve el problema dediedentro del subshellPero a veces no se puede evitar el subshell, o evitarlo haría el script demasiado complejo. En esos casos, puedes guardar el PID al inicio del script con
MYPID=$$y matar así:die() { echo "$1" >&2; kill -9 $MYPID; exit 1; }Claro, eso también tiene sus compensaciones. Esta forma de matar el proceso es bastante brusca y, por alguna razón, tampoco me resultó completamente confiable
set -e, el script también termina cuando un subshell sale con un código de error distinto de 0No se me ocurre por qué habría que omitir
set -een cualquier script de shellset -euxo pipefailal principio de los scripts de BashHace que las pruebas condicionales sean un poco más difíciles, pero especialmente
pipefailya me ha salvado varias vecesset +xTenerlo prendido todo el tiempo se vuelve bastante tedioso
Eso sí,
-xme lo guardo hasta que de verdad necesito ver toda la salida de depuración sucia