2 pontos por GN⁺ 2024-03-03 | 1 comentários | Compartilhar no WhatsApp
  • Quando um script Bash se comporta de forma diferente do esperado, apenas ver exatamente quais comandos estão sendo executados já facilita rastrear a causa
  • set -x imprime cada linha após a expansão das variáveis, permitindo verificar quais comandos o script realmente executa
  • Executar pela linha de comando com bash -x script.sh produz o mesmo efeito de colocar set -x no topo de script.sh
  • Usando o trap DEBUG junto com read, é possível pausar antes da execução de cada linha para verificar nome do arquivo, número da linha e o próximo comando
  • A função die() { echo $1 >&2; exit 1; } simplifica o fluxo de imprimir uma mensagem de erro padrão e encerrar após um comando que falhou

Verificando visualmente o fluxo de execução

  • set -x imprime as linhas executadas pelo script, mostrando as variáveis já expandidas
  • Você pode usá-lo colocando set -x no topo do script
  • O mesmo comportamento também pode ser usado pela linha de comando
    • $ bash -x script.sh
    • Isso é o mesmo que colocar set -x no início de script.sh

Pausando para verificar linha por linha

  • O trap DEBUG é acionado antes da execução de cada linha de código
  • Se você adicionar o código abaixo no início do script, ele aguardará que Enter seja pressionado antes de executar o próximo comando
    • trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUG
    • read -p exibe uma mensagem e aguarda que Enter seja pressionado
    • $BASH_SOURCE é o nome do arquivo do script
    • $LINENO é o número da linha
    • $BASH_COMMAND é o próximo comando a ser executado

Exibindo uma mensagem e encerrando em caso de falha

  • A função die pode ser usada para imprimir uma mensagem e encerrar o programa quando um comando falhar
    • die() { echo $1 >&2; exit 1; }
    • Basta adicioná-la após um comando que pode falhar, como em some_command || die "oh no!"
  • Essa função envia a mensagem para a saída de erro padrão e encerra com exit 1

1 comentários

 
GN⁺ 2024-03-03
Comentários do Hacker News
  • No ZFSBootMenu, usamos algumas funções próprias bem decentes para ajudar na depuração
    Há uma função de logging zdebug espalhada pelo código: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...
    Com o logging de debug ativado, ao pressionar Ctrl-T no menu principal aparece uma tela assim: https://i.imgur.com/Ge75zkP.png
    Também há profiling com flamegraph, que pode ser ativado por https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme..., e, remontando os dados despejados pela porta serial, dá para gerar gráficos como https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master...
    Bash é surpreendentemente flexível
  • Ao usar set -x, PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: ' é muito útil
    Assim são exibidos nome do arquivo, nome da função e número da linha, o que ajuda bastante ao depurar scripts Bash grandes
  • Também recomendo shellcheck. Mesmo que ele não encontre o problema diretamente, aponta problemas potenciais
    Também recomendo reescrever o script em outra linguagem. Na empresa, estamos convertendo scripts Bash para Rust; o custo de entrada é alto, mas o código resultante é muito mais fácil de manter e mais confiável
    Bash ainda é ótimo para scripts rápidos, mas, quando passa de umas 100 linhas, vale a pena usar uma linguagem que ofereça garantias mais fortes
    • Concordo, mas isso também precisa ser dito para a engenharia de CI/CD e pipelines YAML
  • Dá para melhorar ainda mais a depuração usando códigos de saída deste jeito
    die() é uma função auxiliar que imprime uma mensagem de erro no erro padrão e depois encerra com o código de erro especificado
    Há mais códigos de saída e funções auxiliares para scripts de shell aqui: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
    • É uma boa lista, e imagino que todo usuário experiente tenha suas próprias funções auxiliares
      Mas tenho uma pequena ressalva quanto à filosofia desse die. Uma função die deveria, por padrão, propagar o código de saída do comando que falhou e não ocultar a saída de erro desse comando
      Se, em um script grande, eu quiser atribuir meu próprio significado a uma falha de comando, usaria um die mais específico. Meu die é mais ou menos na forma __errex "$?" "${LINENO}" "$0", imprimindo o erro fatal, o número da linha, o nome do script e a mensagem, e terminando com aquele código de saída
  • Se você usa muitas funções Bash, também é possível criar uma espécie de stack trace
    Um exemplo de implementação está aqui: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
    • Há outra implementação de stack trace: https://github.com/runag/runag/blob/main/lib/fail.sh
      Usando algo como some-command || fail "message", quando some-command retorna um status de saída diferente de 0, ela cria um stack trace e encerra o shell
      Se quiser criar e retornar um stack trace dentro de uma função, dá para usar some-command || softfail "message" || return $?
  • Fico curioso se ainda há algum motivo, além de legado e inércia, para Bash continuar sendo a linguagem de script de shell de facto
    Ele faz o que precisa, mas é tosco e a sintaxe é horrível. Quando um script atinge certo tamanho ou complexidade, ele praticamente força a migração para uma linguagem de verdade; talvez isso até seja um design intencional
    • É verdade que o uso legado responde por boa parte da popularidade
      Em uma distribuição moderna, geralmente há uma versão bem recente do Bash e, a menos que você queira usar coisas como arrays, nem precisa se preocupar muito com a versão
      O apelo do Bash está na posição que ele ocupa entre outras linguagens e ferramentas. Ele é ideal para costurar outras ferramentas, fica perto o bastante do sistema operacional para ser conveniente e não exige instalação de bibliotecas como Python
      É comum ouvir que scripts mais complexos deveriam ser migrados para uma linguagem como Python, mas isso acrescenta uma camada de complexidade que talvez não ajude no longo prazo. Um script Bash escrito há 20 anos provavelmente ainda roda bem, enquanto um programa Python de 20 anos atrás provavelmente terá problemas de versão
    • Scripting em shell Bourne é bom o suficiente, então é quase impossível substituí-lo
      O rc do Plan 9 é um pouco mais limpo, mas ninguém troca só por “algo parecido, porém mais limpo”. Mesmo que hoje seja possível instalar algo parecido, porém melhor, em https://pkgsrc.se/shells, as pessoas não usam, e isso não muda a forma como outras pessoas executam seus scripts
      Para substituir uma tecnologia estabelecida, é preciso ser várias vezes melhor em aspectos essenciais. O Plan 9 também é melhor que a família UNIX, mas não foi bom o bastante para substituí-la
      É difícil criar algo bom o suficiente para substituir o nicho do scripting em shell Bourne. Antes de ficar tão bom assim, você já acaba entrando no nicho ecológico ou no domínio de problemas de linguagens de script de verdade como Perl, Python e Ruby
      Em um domínio de problemas estreito, um ótimo local suga todo o ar, dificultando o surgimento de um concorrente próximo do ótimo global teórico
    • Acho que é realmente por causa de legado e inércia

Os recursos adicionados recentemente até se encaixaram bem sobre sh/Bash, mas, no fim das contas, scripting de shell é um meio para um fim e precisa evoluir muito mais devagar do que linguagens de programação comuns
A característica central do Bash/sh é ser anti-entrópica. Como quase não há desenvolvimento nem evolução, é menor a chance de dores de cabeça por dependências ou novos recursos, e coisas que funcionavam há 20 anos continuam sendo ferramentas básicas
Por seu desenho, acaba sendo um sistema resistente a mudanças e, quando as pessoas esbarram em seus limites, isso cria um incentivo para sair dele

  • Não tenho certeza de que isso seja realmente Bash
    A maioria dos scripts do FreeBSD é escrita para sh, e, como sh faz parte do padrão POSIX, sinto que tem suporte muito mais amplo. Vejo o Bash apenas como bastante popular
  • O fato de estar em todo lugar pesa muito
    Mas o Bash era tão ruim que criei um monte de utilitários com namespace reduzido para usar scripts em Groovy. Dava para desenvolver em uma IDE, o sistema de bibliotecas era seguro, e o Groovy aparava quase todos os incômodos do Java, então era muito melhor
  • Também há um depurador real no estilo gdb bem poderoso: https://bashdb.sourceforge.net/
  • Fazendo uma divulgação um pouco relacionada: há algum tempo criei um depurador de pipelines Bash que preserva as saídas intermediárias
    Tem algumas limitações, mas em geral pode ser útil: https://github.com/ketancmaheshwari/pd
  • A técnica die() é boa, mas o Bash tem uma característica irritante. Se você tenta fazer exit dentro de uma subshell, só a subshell termina e o restante do script continua executando
    Por exemplo, se você chamar die dentro de um pipeline como cat myfile | while read line; do ... die "Found match" ... done, o echo "I don't want this line" depois ainda será impresso
    Muitas vezes dá para evitar subshells, e neste exemplo o shellcheck está certo ao apontar UUOC; ao corrigir isso, o problema de die dentro da subshell também é resolvido
    Mas às vezes não dá para evitar uma subshell, ou evitá-la deixa o script complicado demais. Nesses casos, dá para capturar o PID no início do script com MYPID=$$ e matá-lo com algo como die() { echo "$1" >&2; kill -9 $MYPID; exit 1; }
    Claro que isso também é uma concessão. Esse modo de matar é bem bruto e, não sei por quê, também não era totalmente confiável
    • Só adicionar set -e já faz o script terminar junto quando uma subshell encerra com um código de erro diferente de 0
      Não consigo pensar em um bom motivo para deixar set -e de fora de qualquer shell script
    • Se você matar esse PID assim, não poderia acabar criando processos zumbi?
  • No topo dos scripts Bash, sempre coloco set -euxo pipefail
    Isso torna os testes condicionais um pouco mais difíceis, mas só o pipefail já se pagou várias vezes
    • https://mywiki.wooledge.org/BashPitfalls#set_-euo_pipefail
    • Também dá para ativar por várias linhas e depois desativar com set +x
      Se ficar sempre ligado, torna-se bem cansativo
    • É uma configuração salvadora
      Mas guardo o -x para quando realmente preciso ver toda a saída de depuração bagunçada