Depuração de Bash
(wizardzines.com)- 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 -ximprime 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.shproduz o mesmo efeito de colocarset -xno topo descript.sh - Usando o trap
DEBUGjunto comread, é 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 -ximprime as linhas executadas pelo script, mostrando as variáveis já expandidas- Você pode usá-lo colocando
set -xno 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 -xno início descript.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")' DEBUGread -pexibe 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
diepode ser usada para imprimir uma mensagem e encerrar o programa quando um comando falhardie() { 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
Comentários do Hacker News
Há uma função de logging
zdebugespalhada 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
set -x,PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'é muito útilAssim são exibidos nome do arquivo, nome da função e número da linha, o que ajuda bastante ao depurar scripts Bash grandes
shellcheck. Mesmo que ele não encontre o problema diretamente, aponta problemas potenciaisTambé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
die()é uma função auxiliar que imprime uma mensagem de erro no erro padrão e depois encerra com o código de erro especificadoHá 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...
Mas tenho uma pequena ressalva quanto à filosofia desse
die. Uma funçãodiedeveria, por padrão, propagar o código de saída do comando que falhou e não ocultar a saída de erro desse comandoSe, em um script grande, eu quiser atribuir meu próprio significado a uma falha de comando, usaria um
diemais específico. Meudieé 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ídaUm exemplo de implementação está aqui: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
Usando algo como
some-command || fail "message", quandosome-commandretorna um status de saída diferente de 0, ela cria um stack trace e encerra o shellSe quiser criar e retornar um stack trace dentro de uma função, dá para usar
some-command || softfail "message" || return $?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
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
O
rcdo 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 scriptsPara 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
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 comunsA 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ásicasPor 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
A maioria dos scripts do FreeBSD é escrita para
sh, e, comoshfaz parte do padrão POSIX, sinto que tem suporte muito mais amplo. Vejo o Bash apenas como bastante popularMas 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
Tem algumas limitações, mas em geral pode ser útil: https://github.com/ketancmaheshwari/pd
die()é boa, mas o Bash tem uma característica irritante. Se você tenta fazerexitdentro de uma subshell, só a subshell termina e o restante do script continua executandoPor exemplo, se você chamar
diedentro de um pipeline comocat myfile | while read line; do ... die "Found match" ... done, oecho "I don't want this line"depois ainda será impressoMuitas vezes dá para evitar subshells, e neste exemplo o
shellcheckestá certo ao apontar UUOC; ao corrigir isso, o problema dediedentro da subshell também é resolvidoMas à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 comodie() { 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
set -ejá faz o script terminar junto quando uma subshell encerra com um código de erro diferente de 0Não consigo pensar em um bom motivo para deixar
set -ede fora de qualquer shell scriptset -euxo pipefailIsso torna os testes condicionais um pouco mais difíceis, mas só o
pipefailjá se pagou várias vezesset +xSe ficar sempre ligado, torna-se bem cansativo
Mas guardo o
-xpara quando realmente preciso ver toda a saída de depuração bagunçada