Comportamento bizantino no shell script

3

Eu escrevi o seguinte script para remover de forma interativa e recursiva os arquivos de backup órfãos, ou seja, remover cada file.txt~ que não tem um file.txt correspondente.

#!/bin/sh -x

set -o errexit
unalias -a

backups=$(find . -name "*~")

orphans=""
while read -r file
do
    [ ! -e "${file%~}" ] && orphans=$(echo "$file\n$orphans");
done << EOF
$backups
EOF

if [ -z "$orphans" ]; then
    echo "No orphans."
else
    echo "orphans:\n$orphans"
    echo "$orphans" | xargs --interactive -d '\n' rm
fi

Este script faz coisas muito estranhas de maneira aleatória. Às vezes, ele se comporta corretamente, às vezes ignora as opções -x passadas para sh, às vezes executa código comentado, às vezes os testes dão resultados errados.

O problema parece estar relacionado ao script here, já que redirecionando a saída do find para um arquivo temporário, todos os problemas parecem desaparecer. Mas por que, onde está o erro?

Solução (graças a Dennis Williamson ): Escape o caractere ~ . O ~ sem escape na expansão do parâmetro ${file%~} estava criando de alguma forma todo o comportamento imprevisível.

Uma solução mais legível e determinista, com algum corte de gordura, poderia ser (graças às sugestões de Mikel ):

#!/bin/sh
IFS='
'
for backup in $(find . -type f -name "*~"); do
    if [ ! -e "${backup%\~}" ]; then
        rm -i "$backup"
    fi
done

Se você é um fã de while read loop, as coisas ficam menos elegantes porque o comando interativo rm -i não pode ser usado (ele entrará em conflito com o comando read ). De qualquer forma, uma solução poderia ser:

#!/bin/sh
orphans=""
while read -r backup; do
    if [ ! -e "${backup%\~}" ]; then
        orphans=$(echo "$backup\n$orphans");
fi
done << EOF
$(find . -type f -name "*~")
EOF

if [ ! -z "$orphans" ]; then
    echo "$orphans" | xargs --interactive -d '\n' rm
fi

Ou, uma maneira mais complexa também é sugerida por Dennis Williamson .

    
por mrucci 18.01.2011 / 22:35

2 respostas

2

Você provavelmente precisará escapar do til na expansão da chave, caso contrário, ele será expandido para o seu diretório inicial.

Por que você não canaliza o find para seu loop while em vez de criar variáveis para mantê-los? Dentro do seu loop, basta fazer rm -i "$file" .

#!/bin/sh -x

set -o errexit
unalias -a

exec 3<&0    # open a duplicate of stdin
flag=false
find . -name "*~" | while IFS=$'\n' read -r file
do
    if [ ! -e "${file%\~}" ]
    then
        orphans="$file"$'\n'"$orphans"

        # use an alternate file descriptor so read and rm -i get along
        rm -i "$file" <&3
        flag=true
    fi
done
exec 3<&-    # close the file descriptor

if ! $flag
then
    echo "No orphans."
else
    echo "orphans:\n$orphans"
fi

Se você quiser usar o Bash, você precisa mover o find para o final do loop para que não seja criado um subshell.

#!/bin/bash

...

# use an alternate file descriptor so read and rm -i get along
while read -u 3 -r ...

    rm -i ...
    ...

done 3< <(find ...)

...
    
por 18.01.2011 / 23:04
1

O que você quer dizer com "às vezes"? Às vezes na mesma caixa? Ou comportamento diferente em sistemas diferentes?

O que você quer dizer com "executa o código comentado"? Seu exemplo não tem comentários.

Algumas ideias:

  • experimente set -x em vez de /bin/sh -x
  • é melhor usar set -e do que set -o errexit
  • se você estiver usando o bash, chame /bin/bash , não /bin/sh , que poderia ser outra coisa
  • echo é desnecessário, use apenas = com uma nova linha literal
  • se você precisar usar echo , use echo -e ou assegure que xpg_echo esteja definido
  • sua linha de órfãos está colocando-os na ordem inversa, isso é deliberado?
  • seu loop de leitura falhará se os nomes de arquivos contiverem espaços, você deve definir IFS first

Uma versão mais simples:

#!/bin/bash

set -x
set -e

IFS=$'\n'
orphans=false
for backup in $(find . -type f -name "*~"); do
    original=${backup%\~}
    if [ ! -e "$original" ]; then
        orphans=true
        rm -i "$backup"
    fi  
done

if ! $orphans; then
    echo "No orphans."
fi

Ou se você quiser que ele funcione usando /bin/sh :

#!/bin/sh

set -x
set -e

IFS='
'
orphans=false
for backup in $(find . -type f -name "*~"); do
    original=${backup%\~}
    if [ ! -e "$original" ]; then
        orphans=true
        rm -i "$backup"
    fi  
done

if ! $orphans; then
    echo "No orphans."
fi
    
por 18.01.2011 / 23:06