Pregunta Comportamiento bizantino en script de shell


Escribí el siguiente script para eliminar de forma interactiva y recursiva archivos de copia de seguridad huérfanos, es decir, eliminar cada uno file.txt~ que no tiene un correspondiente file.txt.

#!/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

Esta secuencia de comandos hace cosas muy extrañas de forma aleatoria. A veces se comporta correctamente, a veces ignora las opciones -x pasadas a sh, a veces ejecuta código comentado, a veces las pruebas dan resultados incorrectos.

El problema parece estar relacionado con el script aquí, ya que al redirigir la salida de find a un archivo temporal, todos los problemas parecen desaparecer. Pero, ¿por qué? ¿Dónde está el error?


Solución (gracias a Dennis Williamson): Escapar del ~ personaje. El no protegido ~ en la expansión de parámetros ${file%~} estaba creando de alguna manera todo el comportamiento impredecible.


Una solución más legible y determinista, con algo de recorte de grasa, podría ser (gracias a las sugerencias de Mikel)

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

Si eres un while read ventilador de bucle, las cosas se vuelven menos elegantes porque el comando interactivo rm -i no se puede usar (entrará en conflicto con el read mando). De todos modos, una solución podría 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

O bien, una forma más compleja también es sugerida por Dennis Williamson.


3


origen




Respuestas:


Probablemente necesites escapar de la tilde en la expansión de llave, de lo contrario se expandirá a tu directorio de inicio.

¿Por qué no pones la find en tu while bucle en lugar de crear variables para mantenerlos? Dentro de tu ciclo, solo hazlo 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

Si quieres usar Bash, necesitas mover el find hasta el final del ciclo para que no se cree una 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 ...)

...

2



+1. Tilde definitivamente necesita escapar. También estoy de acuerdo en que habría formas más simples de escribir esto. - Mikel
¡Gracias! El problema estaba en escapar de ~. Ahora todo funciona de una manera cómoda y determinista. Por cierto, tu script no funciona con rm interactivo, supongo porque el comando de lectura entrará en conflicto con él. - mrucci
Estoy usando bash 4.1.5 (1) -release y el sub-shell todavía es creado por el tubo. Entonces la redirección de find salida en el while comando debe hacerse. También debe eliminar los restos $ en el orphans línea de concatenación - mrucci
@mrucci: Ese signo de dólar es para crear la nueva línea: $'\n' - Dennis Williamson
Sí, supongo rm -i lee de stdin, que es proporcionado por el heredoc, en lugar de /dev/tty. - Mikel


¿Qué quieres decir "a veces"? A veces en la misma caja? O diferente comportamiento en diferentes sistemas?

¿Qué quieres decir con "ejecuta el código comentado"? Tu ejemplo no tiene ningún comentario.

Algunos pensamientos:

  • tratar set -x en lugar de /bin/sh -x
  • mejor usar set -e que set -o errexit
  • si usas bash, entonces llama /bin/bashno /bin/sh, que podría ser otra cosa
  • echo es innecesario, solo use = con una línea nueva literal
  • si tienes que usar echo, Deberías usar echo -e o asegurar xpg_echo Está establecido
  • su línea de huérfanos los está poniendo en orden inverso, ¿es eso deliberado?
  • su ciclo de lectura fallará si los nombres de archivo contienen espacios, debe establecer IFS primero

Una versión más simple:

#!/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

O si quieres que 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

1



¿Por qué estás set comandos mejor? Es más legible de usar $'\n' (or echo) than a literal newline. In some shells echo` no entiende -e (lo hacen eco literalmente) y realizan el proceso de escape por defecto. Es por eso que a menudo es mejor usar printf ya que es consistente. - Dennis Williamson
Él dijo que estaba usando bash. Esto me permite decir uso echo -e. De lo contrario, realmente necesitas printf para un comportamiento consistente en todas las plataformas, pero eso es un comando externo en bash, entonces echo -e es correcto y más rápido en este caso. - Mikel
Dije uso set -x porque él estaba usando el -x opción. Por consistencia y porque creo que el set -<flag> la forma es más común que set -o <optname>. - Mikel
¡Gracias! No sabía que podría usar Buscar dentro del bucle for tan fácilmente. Buen punto sobre el eco innecesario, estaba totalmente en un estado de "frenesí de eco". El ciclo no fallará cuando los archivos tengan espacios en ellos. Tu solución no funciona con / bin / sh, pero solo con bash pero no puedo ver por qué. - mrucci
Si no estás usando bash, debes reemplazar IFS=$'\n' con IFS='<ENTER>'. - Mikel