Este artigo é uma tradução do original de de autoria de Itamar Turner-Trauring intitulado Please stop writing shell scripts escrito no dia 22 de Março de 2022.
Quando você está automatizando alguma tarefa, por exemplo empacotando a sua aplicação pelo Docker, você com frequência se encontrará escrevendo shell scripts. Você pode ter um script Bash para guiar o seu processo de empacotamento , e outro script como um ponto de entrada para o container. A medida que seu empacotamento cresce em complexidade, seu shell script também.
Tudo funciona bem.
E então, uma dia, seu shell script faz algo completamente errado.
É aí que você percebe seu engano: bash, e linguagens shell scripting em geral, são na maioria quebrados por padrão. Ao menos que você seja muito cuidadoso desde o primeiro dia, qualquer shell script acima de um certo nível de complexidade é quase garantido de ser bugado... e adaptar os recursos de correção é bastante difícil.
O problema com shell scripts
Vamos focar no bash como um exemplo específico.
Problema #1: Erros não param a execução
Considere o shell script a seguir:
#!/bin/bashtouch newfilecp newfil newfile2 # Deliberate typoecho "Success"
O que você acha que irá acontecer quando você executá-lo?
$ bash bad1.shcp: cannot stat 'newfil': No such file or directorySuccess
O script continuou executando mesmo que um comando falhou! compare isso com Python, onde uma exceção evita que códigos posteriores sejam executados.
Você podde solucionar isso ao adicionar set -e ao topo do shell script:
#!/bin/bashset -etouch newfilecp newfil newfile2 # Deliberate typo, don't omit!echo "Success"
E agora:
$ bash bad1.shcp: cannot stat 'newfil': No such file or directory
Problema #2: Variáveis desconhecidas não causam erros
Agora vamos considerar o seguinte script, que tenta adicionar um diretório à variável de ambiente PATH. PATH é como o local de executáveis é encontrado.
#!/bin/bashset -eexport PATH="venv/bin:$PTH" # Typo is deliberatels
Quando executamos o script:
$ bash bad2.shbad2.sh: line 4: ls: command not found
Ele não consegue localizar o comando ls porque tinhamos um erro de digitação, escrevendo $PTH ao invés de $PATH—e o bash não reclamou a respeito de uma variaável de ambiente desconhecida. Em Python você iria obter uma NameError exception; em uma linguagem compilada o código nem mesmo compilaria. No bash o script continua executando; o que poderia dar errado?
A solução é utilizar set -u:
#!/bin/bashset -euexport PATH="venv/bin:$PTH" # Typo is deliberatels
E agora bash pega o erro de digitação:
$ bash bad2.shbad2.sh: line 3: PTH: unbound variableProblem #3: Pipes don’t catch errors
Achamos que solucionamos o problema da falha do comando com set -e, mas não solucionamos todos os casos:
#!/bin/bashset -eunonexistentprogram | echoecho "Success!"
E quando executamos:
$ bash bad3.shbad3.sh: line 3: nonexistentprogram: command not foundSuccess!
A solução é utilizar set -o:
#!/bin/bashset -euo pipefailnonexistentprogram | echoecho "Success!"
Agora:
$ bash bad3.shbad3.sh: line 3: nonexistentprogram: command not found
Nesse ponto implementamos (a maioria) do modo não oficial estrito do. Mas ainda não é o suficiente.
Problema #4: Subshells são esquisitos
Nota: Uma versão anterior desse artigo continha informação incorreta sobre subshells. Agradeço a Loris Lucido por me mostrar meu erro.
Utilizando a syntax $(), você pode lançar um subshell:
#!/bin/bashset -euo pipefailexport VAR=$(echo hello | nonexistentprogram)echo "Success!"
Quando você executar-lo:
$ bash bad4.shbad4.sh: line 3: nonexistentprogram: command not foundSuccess!
O que está acontecendo? Erros no subshell não são tratados como um erro se eles forem parte de um argumento de comando. Isso significa que erro de subshell é descartado.
Uma exceção é definir uma variável diretamente, então precisamos escrever nosso código assim:
#!/bin/bashset -euo pipefailVAR=$(echo hello | nonexistentprogram)export VARecho "Success!"
E agora nosso programa opera corretamente:
$ bash good4.shgood4.sh: line 3: nonexistentprogram: command not found
Provavelmente essa seja uma demonstração suficiente de mal comportamento do bash, mas certamente não é uma demonstração completa.
Alguns péssimos motivos para se utilizar shell scripts
Quais são as rezões que você possa querer utilizar shell scripts assim mesmo?
Péssimo motivo #1: Está sempre lá!
Praticamente todo ambiente de computação Unix-y terá um shell básico. Então se você estiver escrevendo algum empacotamento ou script de inicialização, é tentador usar uma ferramenta que você sabe que estará lá.
A coisa é, se você estiver empacotando uma aplicação Python, você pode praticamente garantir que o desenvolvimento, CI, e ambiente de runtime todos terão Python instalado. Então por que não utilizar uma linguagem de programação que trata de verdade de erros por padrão?
Mais amplamente, praticamente todas as linguagens de programação com uma base de usuários de tamanho decente terão algum tipo de biblioteca ou idiomas orientados a scripts. Rust, por exemplo, tem xshell e outras bibliotecas também. Portanto, na maioria dos casos, você pode usar sua linguagem de programação preferida em vez de um shell script.
Péssimo motivo #2: Apenas escreva o código correto!
Em teoria, se você souber o que está fazendo, e manter-se focado e não esquecer de nenhum detalhe, você pode escrever shell scripts corretamente, mesmo mais complexos. Você pode até mesmo escrever testes de unidade.
Na prática:
Você provavelmente não está trabalhando sozinho; é improvável que todos em sua equipe tenham a experiência relevante. Todos se cansam, se distraem e acabam cometendo erros.
Quase todo shell script complexo que eu tenho visto estava sem a invocação set -euo pipefail, e adicioná-lo depois do fato é bastante difícil (geralmente impossível). Não tenho certeza se já vi teste automatizado para u m shell script. Tenho certeza que eles existem, mas eles são bastante raros.
Péssimo motivo #3: Shellcheck capturará todos estes bugs!
Se você estiver escrevendo programas shell, shellcheck é um jeito muito util de capturar bugs. Infelizmente, isso não é o suficiente sozinho.
Considere o seguinte programa:
#!/bin/bashecho "$(nonexistentprogram | grep foo)"export VAR="$(nonexistentprogram | grep bar)"cp x /nosuchdirectory/echo "$VAR $UNKNOWN_VAR"echo "success!"
Se executarmos esse programa, ele exibirá a mensagem “success!”, mesmo que ele tenha 4 problemas separados (ao menos):
$ bash bad6.shbad6.sh: line 2: nonexistentprogram: command not foundbad6.sh: line 3: nonexistentprogram: command not foundcp: cannot stat 'x': No such file or directorysuccess!
Como o shellcheck faz? Ele capturará alguns dos problemas… mas não todos:
- Se você executar shellcheck, ele apontará o problema com o export.
- Se você executar shellcheck -o all, ele executará todos os checks e também apontará o problema com echo "$(nonexistentprogram ...)". Isso é, assumindo que você esteja utilizando v0.8, que foi lançado em Novembro de 2021. Versões mais antigas não possuíam esse check, então qualquer distribuição Linux anterior a isso lhe fornecerá um shellcheck que não captura esse problema.
- Ele não sugere set -euo pipefail.
Se você estiver confiando no shellcheck, recomendo fortemente se atualizar e certificar-se de executar com -o all.
Parece de escrever shell scripts; Shell scripts são legais em algumas situações:
Para scripts one-off que você estiver manualmente supervisionando, você pode escapar com práticas mais relaxadas.
Às vezes, você realmente não tem garantias de que outra linguagem de programação esteja disponível e precisa utilizar o shell para fazer as coisas acontecerem.
Para casos suficientemente simples, apenas executar alguns comandos sequencialmente, com no cubshells, lógica condicional, ou loops, set -euo pipefail é suficiente (e certifiqie-se de utilizar shellcheck -o all).
Assim que você se encontrar fazer qualquer coisa além disso, é muito melhor utilizar uma linguagem de programação menos propensa a erros. E como a maioria dos softwares tendem a crescer com o tempo, sua melhor aposta é começar com algo um pouco menos quebrado.
Nenhum comentário:
Postar um comentário
Viu algum erro e quer compartilhar seu conhecimento? então comente aí.
Observação: somente um membro deste blog pode postar um comentário.