Como posso endurecer os scripts de bash contra causar danos quando alterados no futuro?

44

Então, eu deletei minha pasta pessoal (ou, mais precisamente, todos os arquivos que eu tinha acesso de gravação). O que aconteceu é que eu tive

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

em um script bash e, depois de não precisar mais de $build , remover a declaração e todos os seus usos - mas o rm . Bash se expande felizmente para rm -rf /* . Sim.

Eu me senti idiota, 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 de bash para que tais erros não aconteçam, ou pelo menos sejam menos prováveis? Por exemplo, eu escrevi

FileUtils.rm_rf("#{build}/*")

em um script Ruby, o intérprete teria reclamado sobre o build não ser declarado, então a linguagem me protege.

O que eu considerei na bash, além de encurtar rm (que, como muitas respostas em questões relacionadas mencionam, não é problemático):

  1. rm -rf "./${build}/"*
    Isso teria matado meu trabalho atual (um repositório do Git), mas nada mais.
  2. Uma variante / parametrização de rm que requer interação ao agir 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?

    
por Raphael 11.07.2018 / 14:57

7 respostas

73
set -u

ou

set -o nounset

Isso faria com que o shell atual tratasse as expansões de variáveis não definidas como um erro:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -u e set -o nounset são opções de shell POSIX .

Um valor vazio não acionará um erro.

Para isso, use

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

A expansão de ${variable:?word} se expandiria para o valor de variable , a menos que esteja vazio ou não definido. Se estiver vazio ou não configurado, o word seria exibido no erro padrão e o shell trataria a expansão como um erro (o comando não seria executado e, se fosse executado em um shell não interativo, isso terminaria). Deixar o : out acionaria o erro apenas para um valor não definido, como em set -u .

${variable:?word} é uma expansão do parâmetro POSIX .

Nenhum desses causaria o encerramento de um shell interativo, a menos que set -e (ou set -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 com set -e .

Quanto à sua segunda pergunta. Não há como limitar o rm para não funcionar fora do diretório atual.

A implementação GNU de rm tem uma opção --one-file-system que a impede de remover recursivamente sistemas de arquivos montados, mas isso é o mais próximo que acredito ser possível sem envolver a chamada rm em uma função que realmente verifica os argumentos .

Observação: ${build} é exatamente equivalente a $build , a menos que a expansão ocorra como parte de uma sequência em que o caractere imediatamente a seguir é um caractere válido em um nome de variável, como em "${build}x" .

    
por 11.07.2018 / 15:00
11

Vou sugerir verificações de validação normais usando test / [ ]

Você estaria seguro se tivesse escrito seu roteiro como tal:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

O [ -n "${build}" ] verifica se "${build}" é uma string de comprimento diferente de zero .

O || é o operador OR lógico no bash. Isso faz com que outro comando seja executado se o primeiro falhar.

Dessa 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 protegeria você caso você tenha removido todos os usos ${build} porque [ -n "" ] sempre será falso.

A vantagem de usar test / [ ] é que existem muitas outras verificações mais significativas que também podem ser usadas.

Por exemplo:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.
    
por 11.07.2018 / 20:36
3

No seu caso específico, eu tenho retrabalhado "deleção" no passado para mover arquivos / diretórios (supondo que / tmp está na mesma partição que seu diretório):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-'date'
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

Nos bastidores, isso move as referências de arquivo / dir de nível superior da origem para as estruturas de diretório de destino $trashdir na mesma partição e não gasta tempo percorrendo a estrutura de diretórios 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 ligeiramente mais lenta (/ tmp é limpo nas reinicializações).

Como alternativa, uma entrada do cron para limpar periodicamente /tmp/.trash-$USER impedirá que o / tmp seja preenchido, para processos (por exemplo, compilações) que consomem muito espaço em disco. Se o seu diretório estiver em uma partição diferente como / tmp, você poderá criar um diretório semelhante a / tmp em sua partição e ter o cron limpo em seu lugar.

O 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.

    
por 12.07.2018 / 10:07
2

Use a substituição de parâmetro bash para atribuir padrões quando a variável não for inicializada, por exemplo:

rm -rf $ {variable: - "/ inexistente"}

    
por 12.07.2018 / 07:56
1

O conselho geral para verificar se sua variável está definida é uma ferramenta útil para evitar esse tipo de problema. Mas, neste caso, houve uma solução mais simples.

Provavelmente não há necessidade de globar o conteúdo do diretório $build para excluí-los, mas não o diretório $build propriamente dito. Portanto, se você ignorou o * externo, um valor não definido se transformaria em rm -rf / , que por padrão a maioria das implementações de rm na última década se recusaria a executar (a menos que você desative essa proteção com a opção --no-preserve-root do GNU rm). p>

Ignorar o / à direita também resultaria em rm '' , o que resultaria na mensagem de erro:

rm: can't remove '': No such file or directory

Isso funciona mesmo se o comando rm não implementar proteção para / .

    
por 24.07.2018 / 21:57
0

Eu sempre tento iniciar meus scripts Bash com uma linha #!/bin/bash -ue .

-e significa "falha no primeiro e rror";

-u significa "falha no primeiro uso de u ndeclarada variável".

Encontre mais detalhes em um ótimo artigo Use o Modo Estrito Unofficial Bash (A menos que você elimine Depuração) . Também autor recomenda usar set -o pipefail; IFS=$'\n\t' , mas para os meus propósitos isso é um exagero.

    
por 24.07.2018 / 20:25
-2

Você está pensando em termos de linguagem de programação, mas bash é uma linguagem de script :-) Então, use um paradigma de instrução completamente diferente para um completamente diferente paradigma da linguagem .

Neste caso:

rmdir ${build}

Como rmdir se recusará a remover um diretório não vazio, você precisará remover os membros primeiro. Você sabe o que esses membros são, certo? Eles são provavelmente parâmetros para seu script ou derivados de um parâmetro, então:

rm -rf ${build}/${parameter}
rmdir ${build}

Agora, se você colocar alguns outros arquivos ou diretórios lá como um arquivo temporário ou algo que você não deveria ter, o rmdir lançará um erro. Manuseie-o corretamente e, em seguida:

rmdir ${build} || build_dir_not_empty "${build}"

Esse modo de pensar me serviu bem porque ... sim, estive lá, fiz isso.

    
por 12.07.2018 / 00:03