Então, apaguei minha pasta pessoal (ou, mais precisamente, todos os arquivos aos quais eu tinha acesso de gravação). O que aconteceu é que eu tinha
build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>
em um script bash e, depois de não precisar mais $build
, removendo a declaração e todos os seus usos - mas o rm
. Bash felizmente se expande para rm -rf /*
. Sim.
Eu me senti estúpido, instalei o backup, refiz o trabalho que perdi. Tentando superar a vergonha.
Agora, eu me pergunto: quais são as técnicas para escrever scripts bash para que tais erros não aconteçam, ou pelo menos sejam menos prováveis? Por exemplo, se eu tivesse escrito
FileUtils.rm_rf("#{build}/*")
em um script Ruby, o intérprete teria reclamado por build
não ter sido declarado, então aí a linguagem me protege.
O que considerei no bash, além de encurralar rm
(o que, como muitas respostas em perguntas relacionadas mencionam, não é isento de problemas):
rm -rf "./${build}/"*
Isso teria matado meu trabalho atual (um repositório Git), mas nada mais.- Uma variante/parametrização
rm
disso requer interação ao atuar fora do diretório atual. (Não foi possível encontrar nenhum.) Efeito semelhante.
É isso ou existem outras maneiras de escrever scripts bash que são "robustos" nesse sentido?
ou
Isso faria com que o shell atual tratasse as expansões de variáveis não definidas como um erro:
set -u
eset -o nounset
são opções de shell POSIX .No entanto, um valor vazio não acionaria um erro.
Para isso, utilize
A expansão de
${variable:?word}
expandiria para o valor devariable
, a menos que estivesse vazio ou não definido. Se estiver vazio ou não definido, oword
será exibido em erro padrão e o shell tratará a expansão como um erro (o comando não será executado e, se estiver sendo executado em um shell não interativo, isso será encerrado). Deixar de:
fora acionaria o erro apenas para um valor não definido, assim como emset -u
.${variable:?word}
é uma expansão de parâmetro POSIX .Nenhum deles faria com que um shell interativo terminasse, a menos que
set -e
(ouset -o errexit
) também estivesse em vigor.${variable:?word}
faz com que os scripts saiam se a variável estiver vazia ou não definida.set -u
faria com que um script fosse encerrado se usado junto comset -e
.Quanto à sua segunda pergunta. Não há como limitar
rm
o trabalho fora do diretório atual.A implementação GNU de
rm
tem uma--one-file-system
opção que a impede de excluir recursivamente os sistemas de arquivos montados, mas isso é o mais próximo que acredito que podemos obter sem envolver arm
chamada em uma função que realmente verifica os argumentos.Como uma nota lateral:
${build}
é exatamente equivalente a , a$build
menos que a expansão ocorra como parte de uma string em que o caractere imediatamente seguinte seja um caractere válido em um nome de variável, como em"${build}x"
.Vou sugerir verificações de validação normais usando
test
/[ ]
Você estaria seguro se tivesse escrito seu script como tal:
As
[ -n "${build}" ]
verificações que"${build}"
é uma string de comprimento diferente de zero .O
||
é o operador OR lógico no bash. Faz com que outro comando seja executado se o primeiro falhar.Desta forma,
${build}
estava vazio/indefinido/etc. o script teria saído (com um código de retorno de 1, que é um erro genérico).Isso também o protegeria caso você removesse todos os usos ,
${build}
porque[ -n "" ]
sempre será falso.A vantagem de usar
test
/[ ]
é que existem muitas outras verificações mais significativas que ele também pode usar.Por exemplo:
No seu caso específico, eu reformulei 'exclusão' no passado para mover arquivos/diretórios (assumindo que /tmp está na mesma partição que seu diretório):
Nos bastidores, isso move as referências de arquivo/diretório de nível superior da origem para as
$trashdir
estruturas de diretório de destino, todas na mesma partição, e não gasta tempo percorrendo a estrutura de diretório e liberando os blocos de disco por arquivo ali mesmo. Isso produz uma limpeza muito mais rápida enquanto o sistema está em uso ativo, em troca de uma reinicialização um pouco mais lenta (/tmp é limpo nas reinicializações).Alternativamente, uma entrada cron para limpar periodicamente /tmp/.trash-$USER impedirá que /tmp seja preenchido, para processos (por exemplo, compilações) que consomem muito espaço em disco. Se seu diretório estiver em uma partição diferente como /tmp, você pode criar um diretório semelhante a /tmp em sua partição e fazer com que o cron limpe isso.
Mais importante, porém, se você estragar as variáveis de alguma forma, você pode recuperar o conteúdo antes que a limpeza aconteça.
Use a substituição de parâmetro bash para atribuir padrões quando a variável não for inicializada, por exemplo:
rm -rf ${variável:-"/inexistente"}
Eu sempre tento iniciar meus scripts Bash com uma linha
#!/bin/bash -ue
.-e
significa "falha no primeiro erro não detectado ";-u
significa "falha no primeiro uso da variável não declarada".Encontre mais detalhes no ótimo artigo Use o Unofficial Bash Strict Mode (a menos que você adore a depuração) . Também o autor recomenda o uso
set -o pipefail; IFS=$'\n\t'
, mas para meus propósitos isso é um exagero.O conselho geral para verificar se sua variável está definida é uma ferramenta útil para evitar esse tipo de problema. Mas neste caso havia uma solução mais simples.
Provavelmente, não há necessidade de glob o conteúdo do
$build
diretório para excluí-los, mas não o próprio$build
diretório real. Portanto, se você pulou o estranho*
, um valor não definido se transformaria norm -rf /
qual, por padrão, a maioria das implementações de rm na última década se recusará a executar (a menos que você desative essa proteção com a--no-preserve-root
opção do GNU rm).Ignorar o trailing
/
também resultaria narm ''
mensagem de erro:Isso funciona mesmo que seu comando rm não implemente proteção para arquivos
/
.Você está pensando em termos de linguagem de programação, mas bash é uma linguagem de script :-) Então, use um paradigma de instrução totalmente diferente para um paradigma de linguagem totalmente diferente .
Nesse caso:
Como
rmdir
se recusará a remover um diretório não vazio, você precisa remover os membros primeiro. Você sabe quais são esses membros, certo? Eles provavelmente são parâmetros para seu script ou derivados de um parâmetro, então:Agora, se você colocar alguns outros arquivos ou diretórios lá, como um arquivo temporário ou algo que você não deveria,
rmdir
ele gerará um erro. Trate-o corretamente, então:Essa maneira de pensar me serviu bem porque... sim, estive lá, fiz isso.