2 puntos por GN⁺ 2024-03-03 | 1 comentarios | Compartir por WhatsApp
  • 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 -x imprime 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 poner set -x al inicio de script.sh
  • Si usas juntos el trap DEBUG y read, 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 -x imprime las líneas que ejecuta el script, y muestra las variables con sus valores ya expandidos
  • Puedes usarlo poniendo set -x al 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 -x al principio de script.sh

Detenerse y revisar línea por línea

  • El trap DEBUG se 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")' DEBUG
    • read -p muestra un mensaje y espera a que se presione Enter
    • $BASH_SOURCE es el nombre del archivo del script
    • $LINENO es el número de línea
    • $BASH_COMMAND es el comando que se ejecutará a continuación

Dejar un mensaje al fallar y salir

  • La función die puede usarse para mostrar un mensaje y terminar el programa cuando un comando falla
    • die() { 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

 
GN⁺ 2024-03-03
Comentarios de Hacker News
  • En ZFSBootMenu usan algunas funciones propias bastante buenas para ayudar con la depuración
    Hay una función de logging zdebug repartida 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
  • Al usar set -x, PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: ' es muy útil
    Así 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
  • También recomiendan shellcheck. Aunque no detecte directamente el problema, sí señala problemas potenciales
    Tambié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
    • De acuerdo, pero también habría que decir eso sobre la ingeniería de CI/CD y las pipelines en YAML
  • Se puede mejorar aún más la depuración usando códigos de salida de esta forma
    die() es una función auxiliar que imprime un mensaje de error en stderr y luego termina con el código de error indicado
    Aquí 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...
    • Es una buena lista, y supongo que los usuarios avanzados todos tienen sus propias funciones auxiliares
      Aun así, no me convence del todo la filosofía de ese die. Idealmente, una función die debería propagar el código de salida del comando que falló y tampoco ocultar la salida de error de ese comando
      Si en un script grande quiero dar yo mismo un significado al fallo de un comando, usaría otro die más específico. Mi die serí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 salida
  • Si usas muchas funciones de Bash, también es posible construir una especie de stack trace
    Aquí hay un ejemplo de implementación: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
    • Aquí hay otra implementación de stack trace: https://github.com/runag/runag/blob/main/lib/fail.sh
      Si lo usas como some-command || fail "message", genera un stack trace y termina el shell cuando some-command devuelve un estado de salida distinto de 0
      Si quieres generar un stack trace dentro de una función y devolverlo, puedes usar algo como some-command || softfail "message" || return $?
  • Me pregunto si Bash sigue siendo el lenguaje de facto para scripting de shell por alguna razón aparte de la herencia legacy y la inercia
    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
    • Es cierto que el uso heredado explica buena parte de su popularidad
      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 scripting del shell Bourne es lo bastante bueno como para que sea casi imposible reemplazarlo
      El rc de 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 cosas
      Para 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
    • Realmente creo que es por legacy e inercia

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 general
La característica central de Bash/sh es 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ásicas
Por 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í

  • No estoy seguro de que eso sea realmente Bash
    La mayoría de los scripts de FreeBSD están escritos para sh, y siento que sh tiene mucho más soporte porque es parte del estándar POSIX. Bash solo me parece popular
  • El hecho de que esté en todas partes es importante
    Pero 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
  • También hay un depurador real estilo gdb bastante potente: https://bashdb.sourceforge.net/
  • Ya que estamos con una promoción vagamente relacionada, hace tiempo hice un depurador de pipelines de Bash que preserva la salida intermedia
    Tiene algunas limitaciones, pero en general puede ser útil: https://github.com/ketancmaheshwari/pd
  • La técnica de die() está bien, pero Bash tiene una característica irritante. Si intentas hacer exit dentro de un subshell, solo termina el subshell y el resto del script sigue ejecutándose
    Por ejemplo, si llamas a die dentro de un pipeline como cat myfile | while read line; do ... die "Found match" ... done, el echo "I don't want this line" de después igual se imprime
    Muchas veces se puede evitar el subshell, y en este ejemplo shellcheck tiene razón al marcar UUOC, y corregir eso también resuelve el problema de die dentro del subshell
    Pero 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
    • Con solo agregar set -e, el script también termina cuando un subshell sale con un código de error distinto de 0
      No se me ocurre por qué habría que omitir set -e en cualquier script de shell
    • Si matas ese PID así, ¿no podría generar procesos zombi?
  • Siempre pongo set -euxo pipefail al principio de los scripts de Bash
    Hace que las pruebas condicionales sean un poco más difíciles, pero especialmente pipefail ya me ha salvado varias veces
    • https://mywiki.wooledge.org/BashPitfalls#set_-euo_pipefail
    • También puedes activarlo por varias líneas y luego desactivarlo con set +x
      Tenerlo prendido todo el tiempo se vuelve bastante tedioso
    • Es una configuración que te salva la vida
      Eso sí, -x me lo guardo hasta que de verdad necesito ver toda la salida de depuración sucia