ESLint ou ShellCheck para seus scripts Bash? Testamos @eslint/shell ✅ yoDEV Tested

O ESLint já analisa scripts de Bash, POSIX sh e mksh com o @eslint/shell, um plugin oficial publicado em 6 de novembro de 2026. Nós o testamos contra o ShellCheck com o mesmo script: eles concordam em quase tudo, cada um detecta algo que o outro não detecta, e encontramos um comportamento não documentado que vale conhecer antes de levá-lo para o CI.

:white_check_mark: yoDEV Tested · 11 de novembro de 2026
@eslint/shell 0.1.2 com ESLint 10.14.0 contra ShellCheck 0.11.0 no Linux (Node 22), sobre um script de deploy de 16 linhas. Os resultados que reportamos como nossos saem desse teste.

O que é o ESLint e o que o @eslint/shell acrescenta?

O ESLint é o linter padrão do ecossistema JavaScript, e desde julho de 2024 sua equipe trabalha para transformá-lo em um linter de propósito geral para qualquer linguagem. Ele já tinha suporte oficial para JSON, Markdown e CSS; o @eslint/shell acrescenta os scripts de shell.

O plugin tem três peças:

  • Um parser construído sobre o mvdan/sh, o mesmo usado pelo shfmt. Ele converte o script em uma árvore sintática no estilo ESTree, então, se você já escreveu uma regra personalizada do ESLint, já sabe escrever uma para shell. Você pode explorar essa árvore no ESLint Code Explorer (explorer.eslint.org).
  • Dez regras que o próprio projeto descreve como inspiradas no ShellCheck. O README associa cada regra ao código do ShellCheck que ela replica.
  • Uma configuração shell/recommended que ativa as dez.

A licença é Apache-2.0 e o código está em github.com/eslint/shell. Quem assina é Nicholas C. Zakas, criador do ESLint e membro do seu Technical Steering Committee. No momento da publicação desta nota, ele está na versão 0.1.2, anterior à 1.0, então os nomes das regras e seu comportamento podem mudar.

Como analisar um script de Bash com o ESLint?

Você precisa do Node.js 20.19 ou superior, 22.13 ou superior, ou 24 em diante. O README indica que o plugin é testado com o ESLint v10.

npm install -D eslint @eslint/shell

Depois, adicione a configuração recomendada ao eslint.config.js:

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

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

O shell/recommended se aplica aos arquivos **/*.sh e **/*.bash e os interpreta como Bash. Execute npx eslint . e seus scripts são analisados junto com o seu JavaScript, na mesma etapa de CI e com a mesma integração de editor que você já mantém. Se você procurava um bash linter ou um shell script linter que não seja mais uma ferramenta para instalar, é isto.

Se você prefere escolher as regras uma a uma, o anúncio mostra a 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? Como analisar scripts POSIX em vez de Bash

Defina language junto com a configuração recomendada:

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

Os três dialetos são shell/bash (o padrão), shell/posix e shell/mksh.

Duas coisas que vimos no teste:

  • O shebang não escolhe o dialeto; quem escolhe é a configuração. Um arquivo que começa com #!/bin/sh e usa arrays e [[ ]] do Bash passou sem erros com a configuração padrão. Se o seu repositório mistura scripts de Bash e de sh, separe-os com padrões files.
  • No modo POSIX, um bashismo é um erro de parsing, não uma regra. O exemplo com arrays parou em Parsing error: … arrays are a bash/mksh feature e o restante do arquivo não foi analisado. Os bashismos são corrigidos um de cada vez.

Quais regras do ShellCheck o @eslint/shell inclui?

Estas são as dez, com o código do ShellCheck que cada uma replica segundo o README:

Regra ShellCheck O que detecta Correção automática Recommended
no-backticks SC2006 Backticks antigos em vez de $(...) Sim error
no-expansions-in-single-quotes SC2016 Variáveis entre aspas simples que não se expandem Não warn
no-ls-iteration SC2045 Percorrer a saída do ls Não error
no-unquoted-expansions SC2086 / SC2046 Expansões sem aspas sujeitas a word splitting e globbing Sim error
no-unused-vars SC2034 Variáveis atribuídas que nunca são usadas Não error
no-useless-cat SC2002 cat arquivo | comando Não error
no-useless-echo SC2116 $(echo ...) Não error
no-variables-in-printf-format SC2059 Variáveis no formato do printf Não error
require-cd-guard SC2164 cd sem tratar a falha Não (sugestões) error
require-read-r SC2162 read sem -r Sim error

O ShellCheck tem centenas de verificações; aqui são dez. Elas foram bem escolhidas: as aspas, os cd que falham e o read -r explicam boa parte dos erros reais em scripts. Mas é um subconjunto, não uma reimplementação, e o projeto não diz o contrário.

ESLint ou ShellCheck? O que cada um detectou no mesmo script bash

Escrevemos um script de “deploy” de 16 linhas com um exemplo de cada problema e alguns extras: um [ -z $TARGET ] sem aspas, um eval "$TARGET" e um glob passado diretamente ao ls. Depois executamos as duas ferramentas.

Problema no script @eslint/shell 0.1.2 ShellCheck 0.11.0
for f in $(ls *.log): percorrer ls :white_check_mark: :white_check_mark: SC2045
…mesma linha, $(ls ...) sem aspas :white_check_mark: —
…mesma linha, glob que pode se expandir para -opções — :white_check_mark: SC2035
cd /opt/app sem || exit :white_check_mark: :white_check_mark: SC2164
Backticks :white_check_mark: :white_check_mark: SC2006
rm $files sem aspas :white_check_mark: :white_check_mark: SC2086
cat config.txt | grep PORT :white_check_mark: — (não reportou no nosso teste)
printf "$TARGET\n" :white_check_mark: :white_check_mark: SC2059
read line sem -r :white_check_mark: :white_check_mark: SC2162
'$HOME' entre aspas simples :white_check_mark: (warn) :white_check_mark: SC2016
x=$(echo hola) :white_check_mark: :white_check_mark: SC2116
[ -z $TARGET ] sem aspas :white_check_mark: :white_check_mark: SC2086
Três variáveis não usadas :cross_mark: 0 de 3 :white_check_mark: SC2034 × 3
eval "$TARGET" — —

Nenhuma das duas marcou o eval, e nenhuma diz fazer isso.

A linha importante é a das variáveis não usadas.

Por que um eval desativa o no-unused-vars?

O shell/no-unused-vars funciona: em um arquivo de três linhas com foo=1, ele reporta foo. Mas no nosso script de teste não reportou nada, enquanto o ShellCheck marcou as três variáveis não usadas.

Isolamos o problema. Um único eval em qualquer parte do arquivo silencia o no-unused-vars no arquivo inteiro. Um script de três linhas, foo=1 seguido de eval "$1", termina com código 0. Se você tirar o eval do script original, as três advertências voltam.

A lógica é defensável: o eval pode se referir a qualquer variável com um nome construído em tempo de execução, então o linter não consegue demonstrar que uma variável não é usada. O ShellCheck adota a posição contrária e reporta mesmo assim. Mas, no momento da publicação desta nota, com a versão 0.1.2, o README não documenta esse comportamento e a saída não avisa que a regra foi desativada. Se os seus scripts usam eval, algo comum em scripts de deploy e de bootstrap, o ESLint não está verificando variáveis não usadas nesses arquivos.

Ele consegue corrigir o script automaticamente?

Três regras se corrigem sozinhas: no-backticks, no-unquoted-expansions e require-read-r. Ao executar npx eslint --fix sobre o nosso script, ele:

  • converteu files=`find . -name "*.tmp"` em files=$(find . -name "*.tmp")
  • adicionou aspas em rm "$files" e em [ -z "$TARGET" ]
  • converteu read line em read -r line

Não mexeu em nada que exija uma decisão humana, como o loop sobre o ls, a proteção do cd ou o formato do printf. É o correto.

Como desativar uma regra em um script bash?

Os comentários padrão do ESLint funcionam, escritos como comentários de shell:

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

Para uma variável que é definida em um arquivo e lida em outro, como um valor de configuração carregado por um script com source, o no-unused-vars aceita uma lista allowed:

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

Testamos as duas opções.

Vale a pena substituir o ShellCheck pelo ESLint?

Se a sua equipe já executa o ESLint no CI e os seus scripts são a cola de sempre (deploy, setup, utilitários de CI), o @eslint/shell oferece as verificações que detectam a maioria dos erros reais sem uma segunda ferramenta para instalar, fixar e configurar. A correção automática de aspas é uma vantagem real.

Se o shell é uma parte importante do seu código, ou se os seus scripts usam eval, mantenha o ShellCheck. Ele cobre muito mais verificações e, no nosso teste, detectou coisas que o ESLint não detectou. Eles não entram em conflito: usar os dois é uma configuração razoável enquanto o plugin não chegar à 1.0.

A equipe do ESLint recebe ideias de regras em github.com/eslint/shell/issues e, por ser uma versão 0.1, a lista de regras é o que tem mais chance de crescer.


E você? Qual erro em um script de shell já saiu mais caro para você em produção?

Com o que você revisa hoje os seus scripts de Bash?
  • ShellCheck
  • ESLint com @eslint/shell
  • Não uso nenhum linter para shell
  • Prefiro outra opção (conte qual)
0 votantes

Comente abaixo ou, se este artigo chegou até você por e-mail, responda diretamente ao e-mail: sua resposta é publicada aqui.

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