5 puntos por GN⁺ 2023-11-25 | 1 comentarios | Compartir por WhatsApp
  • ShellCheck es una herramienta para revisar al instante errores comunes en scripts de shell desde la web, y permite ver los problemas del código pegado en la salida debajo del editor
  • En entornos locales puede instalarse con cabal, apt, dnf, pkg, brew install, entre otros, lo que permite usarlo mediante gestores de paquetes
  • Los ejemplos muestran puntos que suelen pasarse por alto en scripts reales, como advertencias de portabilidad con shebang sh, problemas semánticos y errores en el manejo de comillas
  • Es software libre con licencia GPLv3 y también está disponible en GitHub, la Wiki y como linter integrado para los principales editores
  • Puede usarse en CodeClimate, Codacy y CodeFactor para revisar automáticamente repositorios de GitHub, y ShellCheck en sí está escrito en Haskell

Funciones de análisis de scripts de shell

  • ShellCheck es una herramienta de análisis para encontrar errores en scripts de shell
  • Si pegas un script en el sitio web, puedes ver de inmediato los resultados del análisis en la ventana de salida debajo del editor
  • Los ejemplos incluidos muestran que puede detectar varios tipos de problemas
    • diversos problemas ocultos en scripts comunes
    • advertencias de portabilidad que aparecen cuando el shebang es sh
    • problemas semánticos de nivel más alto
    • varios problemas con el manejo de comillas

Instalación, licencia e integración

  • La instalación local puede hacerse con cabal, apt, dnf, pkg, brew install
  • Es software libre con licencia GPLv3
  • La documentación puede consultarse en la Wiki de ShellCheck
  • Está disponible públicamente en GitHub, y también se publica el código del sitio web
  • Ya existen paquetes disponibles para distribuciones o gestores de paquetes
  • Puede usarse como linter integrado en los principales editores
  • Puede utilizarse en CodeClimate, Codacy, CodeFactor para la revisión automática de repositorios de GitHub
  • ShellCheck está escrito en Haskell

1 comentarios

 
GN⁺ 2023-11-25
Opiniones de Hacker News
  • El consejo que uso es este. Casi siempre conviene poner -u (nounset) en el shebang para que las variables no declaradas sean un error, y la excepción que se encuentra con frecuencia es cuando la sintaxis "${arr[@]}" para expandir un arreglo vacío se trata como unbound.
    -n (noexec) impide ejecutar comandos, así que puede usarse como una especie de dry-run de pobres. -e (errexit) también es útil, pero hay que tener cuidado porque en la práctica solo provoca la salida ante un comando que falló “por sí mismo”; personalmente prefiero evitarlo y suelo agregar || fail "..." después de los comandos.

    • El problema con "${arr[@]}" existe solo en bash 3 o anterior; desde bash 4, [@] no lanza unbound variable incluso cuando la variable realmente no está definida.
      Aun así, sigue siendo un problema porque macOS todavía instala bash v3 por defecto y tampoco lo actualiza automáticamente. Que la última versión de bash 3 haya salido hace 20 años es una locura. El unbound al expandir arreglos vacíos puede esquivarse con la expansión ${var+alter}: echo "${arr+${arr[@]}}"
    • También existe el consejo común de no poner opciones de Bash/shell en el shebang. Si alguien ejecuta el script especificando explícitamente el intérprete, como bash my_script.sh en lugar de ./my_script.sh, esas opciones no se aplican.
      Mucha gente lo hace para no tener que configurar el bit de ejecución y, a veces, también por falta de comprensión. Por eso es común recomendar usar set, como set -euo pipefail, en la primera línea después del shebang; además ayuda en casos donde el shebang maneja mal argumentos adicionales, como #!/usr/bin/env bash.
    • Me pregunto si hay alguna razón especial para poner -u en el shebang. ¿Por qué el shebang en lugar de set -u?
      En Bash, "${arr[@]}" parece funcionar bien. Como se mencionó en otros comentarios, en Bash moderno mejoró aún más y parece ser un problema solo en <= 4.3: https://news.ycombinator.com/item?id=38397241
      Por ejemplo, bash -uc 'unset x; echo "=> ${x[@]}"' y bash -uc 'x=(); echo "=> ${x[@]}"' pasan con un valor vacío, pero bash -uc 'x=(); echo "=> ${x[0]}"' deja bash: x[0]: unbound variable. A Zsh no le gusta el primer ejemplo, pero ambos deberían soportar expansión con valor por defecto, como bash -uc 'unset x; echo "=> ${x[@]:-null}"'. Con los años, -e me gusta cada vez menos porque se vuelve muy confuso cuando se combina con funciones.
    • Los problemas de -e se pueden manejar bien con -o pipefail, y eso forma parte de POSIX desde el año pasado.
    • Cuando usaba opciones que hacen que un script termine temprano, normalmente también tenía que usar trap si había archivos que limpiar.
      trap es una gran función de scripting, pero siento que no se habla lo suficiente de ella.
  • Hace poco encontré una vulnerabilidad de escalada de privilegios en un shell script por culpa de la expansión aritmética. Es un tipo similar al descrito en https://research.nccgroup.com/2020/05/12/shell-arithmetic-ex....
    Por ejemplo, $((1 + ENV_VAR)) permite inyección de código si se puede controlar $ENV_VAR. Lamentablemente, ShellCheck no lo detectó, al menos con la configuración por defecto. Pero si estás implementando algo aunque sea un poco importante para la seguridad, para empezar no deberías usar shell.

    • ¿Qué habría que usar si se quiere más seguridad?
  • ShellCheck es realmente un salvavidas. Hace un tiempo hice un pequeño wrapper, https://github.com/jamespwilliams/strictbash, que puede usarse como shebang de un script.
    Ejecuta ShellCheck antes de correr el script y, si hay fallas, directamente no lo ejecuta; además configura todas las flags de “strict mode” de bash. Referencia: http://redsymbol.net/articles/unofficial-bash-strict-mode/.

    • Está bueno, pero parece útil solo para tus propios scripts. Si no, tendrías que depurar y corregir cada vez todos los scripts que necesites ejecutar.
  • Este tema ya apareció varias veces: https://news.ycombinator.com/from?site=shellcheck.net
    La última discusión grande fue en 2021, con 301 puntos y 54 comentarios: https://news.ycombinator.com/item?id=27030504

  • Hace poco reemplacé los scripts de build y despliegue, y algunos scripts bash para un único servidor de producción, por Turtle de Haskell
    Me gustó porque pude reducir mucho la duplicación, y el código resultante quedó bastante más corto. https://hackage.haskell.org/package/turtle

    • Hace poco probé Turtle, pero al final lo dejé y elegí typed-process
      Hasta donde sé, los programas de Turtle tienen un solo directorio actual, así que se complica cuando hay que ejecutar tareas concurrentes que deben correr en directorios específicos. Lo resolví en parte con locks, colas y workers, pero cuando el directorio actual de Turtle se borró y empezó a fallar, se volvió inmanejable
      En cambio, typed-process lanza procesos separados y puede ejecutarlos dentro del directorio de trabajo sin necesidad de hacer cd, así que encaja bien en flujos de trabajo grandes y complejos. También tiene buen soporte para OverloadedStrings, así que normalmente basta con copiar y pegar lo que habrías escrito en bash y funciona tal cual
      También uso el paquete interpolate y QuasiQuotes para que los strings crudos se vean mejor en el código fuente, pero no es compatible con hlint, así que pienso buscar otro paquete para manejar strings
  • Es autopromoción descarada, pero tengo ShellCheck y varios linters configurados en pre-commit con el principio de arreglar todas las advertencias antes de commitear o, al menos, antes de mergear
    Pero la mayoría del shell de mis proyectos está dentro de archivos .gitlab-ci.yml, así que es difícil revisarlo. Por eso hice un wrapper que lo procesa automáticamente: https://pypi.org/project/glscpc/
    Usando el proyecto ShellCheck y un poco de magia, muestra los hallazgos de ShellCheck con números de línea casi exactos

    • Ojalá hubiera un proyecto que hiciera esto de forma más general
      No uso GitLab CI, pero uso bastantes formatos de archivo que esencialmente incrustan shell scripts en línea, como Dockerfile, GitHub Actions y Justfile
      Por lo general, aunque sea solo por ShellCheck, saco cualquier cosa más compleja que unos pocos comandos a un shell script separado y hago que lo llamen desde el script en línea del Dockerfile. Este patrón también ayuda a que el CI no quede demasiado atado a GitHub Actions
    • Choca esos cinco. Yo también hice algo parecido muy recientemente: https://gitlab.com/engmark/shellcheck-gitlab-ci-scripts-hook
      El ejemplo de uso es un hook de pre-commit para .gitlab-ci.yml, y el ejemplo de configuración está aquí: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...
    • Realmente genial. Hace tiempo intenté resolver este problema desde otro ángulo. Quería meter un preprocesamiento que tomara un shell script “normal” y, en tiempo de build, lo renderizara como la parte script de ese job
      La ventaja es que todo seguiría quedando autocontenido dentro del job de gitlab-ci. Pero lidiar con todas las rarezas del shell en el entorno del runner de GitLab CI era demasiado doloroso, así que lo dejé, y ahora estoy moviendo todos los jobs a scripts de Python
  • También existe bash language server: https://github.com/bash-lsp/bash-language-server/

  • Muy bueno. En el primer script de producción en /bin/sh que ejecuté, aprendí varias cosas de inmediato, y eso que vengo tocando este tipo de scripts desde los 80

  • Si se volvió demasiado largo para escribirlo en Bash, al punto de que en realidad no deberías estar haciéndolo así, también recomiendo https://github.com/bach-sh/bach

  • ShellCheck es excelente, pero el manejo de source/import es realmente doloroso. No es culpa de ShellCheck, sino de que sh es una pesadilla

    • Se puede hacer algo así: # shellcheck source=./deployment/deployment-example.env y luego usar . "${1}"
      Pero si tienes muchos sub-scripts de shell y muchos archivos para hacer source, se entiende por qué se vuelve doloroso