5 pontos por GN⁺ 2023-11-25 | 1 comentários | Compartilhar no WhatsApp
  • ShellCheck é uma ferramenta que permite verificar imediatamente na web erros comuns em scripts shell, mostrando os problemas do código colado na saída abaixo do editor
  • Em ambiente local, pode ser instalada com cabal, apt, dnf, pkg, brew install e outros, permitindo uso baseado em gerenciadores de pacotes
  • Os exemplos mostram pontos frequentemente ignorados em scripts reais, como avisos de portabilidade com shebang sh, problemas semânticos e erros no uso de aspas
  • É software livre sob licença GPLv3 e também está disponível no GitHub, na Wiki e como linter integrado para os principais editores
  • Pode ser usado no CodeClimate, Codacy e CodeFactor para verificação automática de repositórios GitHub, e o próprio ShellCheck é escrito em Haskell

Recursos de análise de scripts shell

  • O ShellCheck é uma ferramenta de análise que encontra bugs em scripts shell
  • Ao colar um script no site, é possível ver imediatamente os resultados da verificação na janela de saída abaixo do editor
  • Os exemplos incluídos mostram que ele consegue detectar vários tipos de problemas
    • diversos problemas escondidos em scripts comuns
    • avisos de portabilidade quando o shebang é sh
    • problemas semânticos de nível mais alto
    • vários problemas no tratamento de aspas

Instalação, licença e integrações

  • A instalação local pode ser feita com cabal, apt, dnf, pkg, brew install
  • É software livre sob licença GPLv3
  • A documentação pode ser consultada na ShellCheck Wiki
  • Está disponível publicamente no GitHub, e o código do site também é aberto
  • Já existem pacotes disponíveis para distribuições e gerenciadores de pacotes
  • Pode ser usado como linter integrado nos principais editores
  • Pode ser utilizado no CodeClimate, Codacy e CodeFactor para verificação automática de repositórios GitHub
  • O ShellCheck é escrito em Haskell

1 comentários

 
GN⁺ 2023-11-25
Comentários do Hacker News
  • A dica que eu uso é a seguinte. Quase sempre coloco -u (nounset) no shebang para transformar variáveis não declaradas em erro; uma exceção comum é quando a sintaxe "${arr[@]}" expande um array vazio e isso é tratado como unbound
    -n (noexec) impede a execução de comandos, então pode ser usado como uma espécie de dry-run dos pobres. -e (errexit) também é útil, mas é preciso tomar cuidado porque, na prática, só um comando “ele mesmo” que falha causa a saída; pessoalmente, prefiro evitar e costumo colocar || fail "..." depois dos comandos

    • O problema de "${arr[@]}" existe apenas no bash 3 ou anterior; a partir do bash 4, mesmo quando a variável realmente não está definida, [@] não gera unbound variable
      Mesmo assim, isso continua sendo um problema porque o macOS ainda instala bash v3 por padrão e não atualiza automaticamente. É realmente absurdo que a última release do bash 3 tenha sido há 20 anos. O unbound na expansão de array vazio pode ser contornado com a expansão ${var+alter}: echo "${arr+${arr[@]}}"
    • Também existe o conselho comum de não colocar opções de Bash/shell no shebang. Se alguém executar o script especificando o interpretador, como bash my_script.sh em vez de ./my_script.sh, as opções não serão aplicadas
      Muita gente faz isso para não precisar definir o bit de execução, e às vezes também por falta de entendimento. Por isso é comum recomendarem usar set, como set -euo pipefail, na primeira linha depois do shebang; isso também ajuda em casos como #!/usr/bin/env bash, em que o shebang lida mal com argumentos adicionais
    • Fico curioso se existe algum motivo especial para colocar -u no shebang. Por que no shebang em vez de set -u?
      No Bash, "${arr[@]}" parece funcionar bem. Como também foi mencionado em outro comentário, isso melhorou no Bash mais recente e parece ser problema só no <= 4.3: https://news.ycombinator.com/item?id=38397241
      Por exemplo, bash -uc 'unset x; echo "=> ${x[@]}"' e bash -uc 'x=(); echo "=> ${x[@]}"' passam com valor vazio, mas bash -uc 'x=(); echo "=> ${x[0]}"' gera bash: x[0]: unbound variable. O Zsh não gosta do primeiro exemplo, mas ambos deveriam suportar expansão com valor padrão, como em bash -uc 'unset x; echo "=> ${x[@]:-null}"'. -e fica muito confuso quando se mistura com funções, e com o tempo passei a gostar cada vez menos dele
    • O problema do -e pode ser tratado bem com -o pipefail, e isso foi incluído no POSIX desde o ano passado
    • Ao usar opções que fazem o script encerrar cedo, eu normalmente também precisava usar trap quando havia arquivos para limpar
      trap é um excelente recurso de scripting, mas dá a sensação de que não se fala o suficiente sobre isso
  • Recentemente encontrei uma vulnerabilidade de elevação de privilégio em shell script por causa de expansão aritmética. É de um tipo parecido com o descrito em https://research.nccgroup.com/2020/05/12/shell-arithmetic-ex...
    Por exemplo, em $((1 + ENV_VAR)), se você conseguir controlar $ENV_VAR, dá para injetar código. Infelizmente, o ShellCheck não detectou isso, pelo menos na configuração padrão. Mas, se você vai implementar qualquer coisa minimamente importante para segurança, em primeiro lugar não deveria usar shell

    • O que deveria ser usado para ter mais segurança?
  • ShellCheck é realmente um salvador. No passado eu fiz um pequeno wrapper https://github.com/jamespwilliams/strictbash que pode ser usado no shebang do script
    Ele roda o ShellCheck antes de executar o script, e se houver qualquer falha o script nem chega a rodar; além disso, ativa todos os flags do “strict mode” do bash. Referência: http://redsymbol.net/articles/unofficial-bash-strict-mode/

    • É legal, mas parece útil só para usar nos seus próprios scripts. Caso contrário, você teria que depurar e corrigir toda vez todos os scripts que precisasse executar
  • Este tópico já apareceu várias vezes: https://news.ycombinator.com/from?site=shellcheck.net
    A última grande discussão foi em 2021, com 301 pontos e 54 comentários: https://news.ycombinator.com/item?id=27030504

  • Há pouco tempo, converti alguns scripts de build e deploy, além de alguns scripts bash para um único servidor de produção, para o Turtle do Haskell
    Foi ótimo porque consegui reduzir bastante a duplicação, e o código resultante também ficou muito menor. https://hackage.haskell.org/package/turtle

    • Usei Turtle recentemente, mas no fim abandonei e escolhi typed-process
      Pelo que sei, programas em Turtle têm apenas um diretório atual, então é difícil executar trabalhos concorrentes que precisam rodar em diretórios específicos. Resolvi parte disso com locks, filas e workers, mas quando o diretório atual do Turtle foi apagado e tudo começou a falhar, ficou inviável de manter
      Já o typed-process funciona melhor para workflows grandes e complexos, porque inicia processos separados e pode executá-los dentro do diretório de trabalho sem precisar dar cd. O suporte a OverloadedStrings também é ótimo, então normalmente dá para copiar e colar o que eu teria digitado no bash e funciona direto
      Também uso o pacote interpolate e QuasiQuotes para deixar strings brutas mais legíveis no código-fonte, mas eles não são compatíveis com hlint, então pretendo procurar outro pacote para lidar com strings
  • É autopromoção descarada, mas tenho ShellCheck e vários linters na configuração do pre-commit com a regra de corrigir todos os avisos antes de fazer commit ou pelo menos antes de fazer merge
    Só que a maior parte do shell nos meus projetos fica dentro de arquivos .gitlab-ci.yml, o que é difícil de verificar. Então criei um wrapper para automatizar isso: https://pypi.org/project/glscpc/
    Com o projeto ShellCheck e um pouco de mágica, ele mostra os apontamentos do ShellCheck com números de linha quase exatos

    • Seria bom se existisse um projeto que fizesse isso de forma mais genérica
      Não uso GitLab CI, mas há vários formatos de arquivo que essencialmente embutem shell script inline, como Dockerfile, GitHub Actions e Justfile
      Normalmente, mesmo que seja só por causa do ShellCheck, qualquer coisa mais complexa do que alguns comandos eu separo em um shell script à parte e faço o script inline no Dockerfile chamá-lo. Esse padrão também ajuda a evitar que o CI fique acoplado demais ao GitHub Actions
    • Toca aqui. Eu também fiz algo parecido bem recentemente: https://gitlab.com/engmark/shellcheck-gitlab-ci-scripts-hook
      Há um exemplo de uso como hook de pre-commit para .gitlab-ci.yml, e um exemplo de configuração está aqui: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...
    • Muito legal. Há algum tempo tentei resolver esse problema por outro ângulo. Queria inserir um pré-processamento que recebesse um shell script “normal” e, no momento do build, o renderizasse na parte script do job correspondente
      A vantagem é que tudo ainda continuaria autocontido dentro do job do gitlab-ci. Mas lidar com todas as esquisitices do shell no ambiente do runner do GitLab CI era doloroso demais, então desisti e agora estou migrando todos os jobs para scripts em Python
  • Também existe o bash language server: https://github.com/bash-lsp/bash-language-server/

  • Bom. Logo na primeira vez que rodei isso em um script de produção /bin/sh, já aprendi algumas coisas, e mexo com esse tipo de script desde os anos 80

  • Se ficou longo demais para escrever em Bash — e na verdade isso já indica que talvez não devesse ser feito assim — também recomendo https://github.com/bach-sh/bach

  • ShellCheck é excelente, mas lidar com source/import é realmente doloroso. Não é culpa do ShellCheck; é porque sh é um pesadelo

    • Dá para fazer assim: # shellcheck source=./deployment/deployment-example.env seguido de . "${1}"
      Mas dá para entender por que isso se torna sofrido quando você tem vários subscripts shell e muitos arquivos para carregar com source