¿ESLint o ShellCheck para tus scripts de Bash? Probamos @eslint/shell ✅ yoDEV Tested

ESLint ya analiza scripts de Bash, POSIX sh y mksh con @eslint/shell, un plugin oficial publicado el 6 de noviembre de 2026. Lo probamos frente a ShellCheck con el mismo script: coinciden en casi todo, cada uno detecta algo que el otro no, y encontramos un comportamiento no documentado que conviene conocer antes de llevarlo a CI.

:white_check_mark: yoDEV Tested · 11 de noviembre de 2026
@eslint/shell 0.1.2 con ESLint 10.14.0 frente a ShellCheck 0.11.0 en Linux (Node 22), sobre un script de deploy de 16 líneas. Los resultados que reportamos como nuestros salen de esa prueba.

¿Qué es ESLint y qué añade @eslint/shell?

ESLint es el linter estándar del ecosistema JavaScript, y desde julio de 2024 su equipo trabaja para convertirlo en un linter de propósito general para cualquier lenguaje. Ya tenía soporte oficial para JSON, Markdown y CSS; @eslint/shell añade los scripts de shell.

El plugin tiene tres piezas:

  • Un parser construido sobre mvdan/sh, el mismo que usa shfmt. Convierte el script en un árbol sintáctico estilo ESTree, así que si alguna vez escribiste una regla personalizada de ESLint, ya sabes escribir una para shell. Puedes explorar ese árbol en el ESLint Code Explorer (explorer.eslint.org).
  • Diez reglas que el propio proyecto describe como inspiradas en ShellCheck. El README asocia cada regla con el código de ShellCheck que replica.
  • Una configuración shell/recommended que activa las diez.

La licencia es Apache-2.0 y el código está en github.com/eslint/shell. Lo firma Nicholas C. Zakas, creador de ESLint y miembro de su Technical Steering Committee. Al momento de publicar esta nota va por la versión 0.1.2, anterior a la 1.0, así que los nombres de las reglas y su comportamiento pueden cambiar.

¿Cómo analizar un script de Bash con ESLint?

Necesitas Node.js 20.19 o superior, 22.13 o superior, o 24 en adelante. El README indica que el plugin está probado con ESLint v10.

npm install -D eslint @eslint/shell

Después añade la configuración recomendada a eslint.config.js:

import { defineConfig } from "eslint/config";
import shell from "@eslint/shell";

export default defineConfig([
    {
        plugins: { shell },
        extends: ["shell/recommended"],
    },
]);

shell/recommended se aplica a los archivos **/*.sh y **/*.bash y los interpreta como Bash. Ejecuta npx eslint . y tus scripts se analizan junto con tu JavaScript, en el mismo paso de CI y con la misma integración de editor que ya mantienes. Si buscabas un bash linter o un shell script linter que no sea una herramienta más que instalar, es esto.

Si prefieres elegir las reglas una por una, el anuncio muestra la forma manual:

import { defineConfig } from "eslint/config";
import shell from "@eslint/shell";

export default defineConfig([
    {
        files: ["**/*.sh"],
        plugins: { shell },
        language: "shell/posix",
        rules: {
            "shell/no-backticks": "error",
            "shell/no-unquoted-expansions": "error",
        },
    },
]);

¿Bash vs sh? Cómo analizar scripts POSIX en lugar de Bash

Define language junto a la configuración recomendada:

export default defineConfig([
    {
        plugins: { shell },
        extends: ["shell/recommended"],
        language: "shell/posix",
    },
]);

Los tres dialectos son shell/bash (el predeterminado), shell/posix y shell/mksh.

Dos cosas que vimos en la prueba:

  • El shebang no elige el dialecto; lo elige la configuración. Un archivo que empieza con #!/bin/sh y usa arrays y [[ ]] de Bash pasó sin errores con la configuración predeterminada. Si tu repositorio mezcla scripts de Bash y de sh, sepáralos con patrones files.
  • En modo POSIX, un bashismo es un error de parseo, no una regla. El ejemplo con arrays se detuvo en Parsing error: … arrays are a bash/mksh feature y el resto del archivo no se analizó. Los bashismos se corrigen de uno en uno.

¿Qué reglas de ShellCheck incluye @eslint/shell?

Estas son las diez, con el código de ShellCheck que cada una replica según el README:

Regla ShellCheck Qué detecta Corrección automática Recommended
no-backticks SC2006 Backticks antiguos en lugar de $(...) Sí error
no-expansions-in-single-quotes SC2016 Variables entre comillas simples que no se expanden No warn
no-ls-iteration SC2045 Recorrer la salida de ls No error
no-unquoted-expansions SC2086 / SC2046 Expansiones sin comillas sujetas a word splitting y globbing Sí error
no-unused-vars SC2034 Variables asignadas que nunca se usan No error
no-useless-cat SC2002 cat archivo | comando No error
no-useless-echo SC2116 $(echo ...) No error
no-variables-in-printf-format SC2059 Variables en el formato de printf No error
require-cd-guard SC2164 cd sin manejar el fallo No (sugerencias) error
require-read-r SC2162 read sin -r Sí error

ShellCheck tiene cientos de comprobaciones; esto son diez. Están bien elegidas: las comillas, los cd que fallan y el read -r explican buena parte de los errores reales en scripts. Pero es un subconjunto, no una reimplementación, y el proyecto no dice lo contrario.

¿ESLint o ShellCheck? Qué detectó cada uno en el mismo script bash

Escribimos un script de “deploy” de 16 líneas con un ejemplo de cada problema y algunos extra: un [ -z $TARGET ] sin comillas, un eval "$TARGET" y un glob pasado directamente a ls. Después ejecutamos las dos herramientas.

Problema en el script @eslint/shell 0.1.2 ShellCheck 0.11.0
for f in $(ls *.log): recorrer ls :white_check_mark: :white_check_mark: SC2045
…misma línea, $(ls ...) sin comillas :white_check_mark: —
…misma línea, glob que puede expandirse a -opciones — :white_check_mark: SC2035
cd /opt/app sin || exit :white_check_mark: :white_check_mark: SC2164
Backticks :white_check_mark: :white_check_mark: SC2006
rm $files sin comillas :white_check_mark: :white_check_mark: SC2086
cat config.txt | grep PORT :white_check_mark: — (no lo reportó en nuestra prueba)
printf "$TARGET\n" :white_check_mark: :white_check_mark: SC2059
read line sin -r :white_check_mark: :white_check_mark: SC2162
'$HOME' entre comillas simples :white_check_mark: (warn) :white_check_mark: SC2016
x=$(echo hola) :white_check_mark: :white_check_mark: SC2116
[ -z $TARGET ] sin comillas :white_check_mark: :white_check_mark: SC2086
Tres variables sin usar :cross_mark: 0 de 3 :white_check_mark: SC2034 × 3
eval "$TARGET" — —

Ninguna de las dos marcó el eval, y ninguna dice hacerlo.

La fila importante es la de las variables sin usar.

¿Por qué un eval desactiva no-unused-vars?

shell/no-unused-vars funciona: en un archivo de tres líneas con foo=1 reporta foo. Pero en nuestro script de prueba no reportó nada, mientras ShellCheck marcó las tres variables sin usar.

Lo aislamos. Un solo eval en cualquier parte del archivo silencia no-unused-vars en todo el archivo. Un script de tres líneas, foo=1 seguido de eval "$1", termina con código 0. Si quitas el eval del script original, vuelven las tres advertencias.

La lógica se puede defender: eval puede referirse a cualquier variable con un nombre construido en tiempo de ejecución, así que el linter no puede demostrar que una variable no se usa. ShellCheck adopta la posición contraria y reporta igual. Pero al momento de publicar esta nota, con la versión 0.1.2, el README no documenta este comportamiento y la salida no avisa de que la regla se desactivó. Si tus scripts usan eval, algo habitual en scripts de deploy y de bootstrap, ESLint no está revisando variables sin usar en esos archivos.

¿Puede corregir el script automáticamente?

Tres reglas se corrigen solas: no-backticks, no-unquoted-expansions y require-read-r. Al ejecutar npx eslint --fix sobre nuestro script:

  • convirtió files=`find . -name "*.tmp"` en files=$(find . -name "*.tmp")
  • añadió comillas en rm "$files" y en [ -z "$TARGET" ]
  • convirtió read line en read -r line

No tocó nada que requiera una decisión humana, como el bucle sobre ls, la protección del cd o el formato de printf. Es lo correcto.

¿Cómo desactivar una regla en un script bash?

Los comentarios estándar de ESLint funcionan, escritos como comentarios de shell:

# eslint-disable-next-line shell/no-unused-vars
foo=1
bar=2 # eslint-disable-line shell/no-unused-vars

Para una variable que se define en un archivo y se lee en otro, como un valor de configuración que carga un script con source, no-unused-vars acepta una lista allowed:

rules: {
    "shell/no-unused-vars": ["error", { allowed: ["CONFIG"] }],
},

Probamos las dos opciones.

¿Conviene reemplazar ShellCheck por ESLint?

Si tu equipo ya ejecuta ESLint en CI y tus scripts son el pegamento habitual (deploy, setup, utilidades de CI), @eslint/shell te da las comprobaciones que detectan la mayoría de los errores reales sin una segunda herramienta que instalar, fijar y configurar. La corrección automática de comillas es una ventaja real.

Si el shell es una parte importante de tu código, o tus scripts usan eval, mantén ShellCheck. Cubre muchas más comprobaciones y en nuestra prueba detectó cosas que ESLint no. No entran en conflicto: usar los dos es una configuración razonable mientras el plugin no llegue a la 1.0.

El equipo de ESLint recibe ideas de reglas en github.com/eslint/shell/issues, y al ser una versión 0.1, la lista de reglas es lo que más probablemente crezca.


¿Y tú? ¿Qué error en un script de shell te ha costado más caro en producción?

¿Con qué revisas hoy tus scripts de Bash?
  • ShellCheck
  • ESLint con @eslint/shell
  • No uso ningún linter para shell
  • Prefiero otra opción (cuéntanos cuál)
0 votantes

Comenta abajo o, si este artículo te llegó por correo, responde directamente al email: tu respuesta se publica aquí.

Relacionado: @shadcn/lint: el linter que obliga a tu agente de IA a respetar tu design system en Tailwind CSS