rm -r: excluye ciertos subdirectorios

2

El problema

Tengo una estructura de directorio, por ejemplo, algo como esto (aquí las barras inclinadas representan carpetas):

./A
./B/A/A
./B/A/B
./B/B/
./B/C
./C/

Y necesito eliminar recursivamente todo excepto algunos archivos y directorios:

./A
./B/A

Después de ejecutar el comando / script que estoy buscando, quiero que quede la siguiente jerarquía:

./A
./B/A/A
./B/A/B

El intento de solución

Traté de usar find( -printes un marcador de posición):

find \( -path ./A -or -path ./B/A \) -prune -or -print

Esto no funciona porque elimina los directorios principales de las entradas en la lista "no tocar":

$ find \( -path ./A -or -path ./B/A \) -prune -or -print
.
./B
./B/B
./B/C
./C

Especialmente, esto elimina ./Bmientras necesito mantener ./B/A. Diablos, elimina el directorio actual, después de todo.

Quiero evitar las invocaciones recursivas (es decir find -exec something-that-calls-find.sh), ya que las listas de directorios que manejaré son bastante grandes ...

intelfx
fuente

Respuestas:

1

Creo que es más fácil usar una expresión regular para que coincida con las rutas

  • ./B/A
  • ./B/A/A
  • ./B/A/B
  • ./B/A/B/C
  • etcétera

Entonces, lo siguiente coincidirá ./Ao cualquier cosa debajo de la ./B/Acarpeta, incluida. Agregué un \para hacer que el comando sea más legible. También tenga en cuenta que esto solo funciona con GNU find, es decir, no en BSD find.

find -depth -regextype posix-extended -mindepth 1 \
! \( -path "./A" -or -regex "\./B(/A(/.*)?)?" \)

Para explicar la expresión regular: /.*coincide con cualquier cosa del Adirectorio. Necesita la barra oblicua aquí porque, de lo contrario, un directorio llamado ABtambién habría coincidido. Este patrón anterior puede aparecer cero veces (para el directorio A) o una vez (para cualquier cosa a continuación A), por eso necesitamos el ?. Como no queremos eliminar B, la parte posterior puede ocurrir cero o una vez ( ?).

Como hay una negación ( !), el findcomando coincidiría:

./B/B
./B/C
./C

Luego puede agregar la -exec rm -rf {} opción para eliminar esos archivos y carpetas. Sin -depthembargo, necesitamos la opción para comenzar con la más profunda, para no intentar eliminar carpetas que ya no existen.

slhck
fuente
He aquí por qué no funcionará: intentará eliminarlo ./By, por lo tanto, lo eliminará a ./B/Apesar de que esté en la lista "ignorar".
intelfx
Es cierto, lo extrañé por completo. Actualicé mi expresión regular.
slhck
Entonces sí, parece funcionar y parece programable. Sin embargo, 1) es mejor usar -exec rm -rf {} \; -pruney eliminar .*parte de la expresión regular, al menos en términos de rendimiento; 2) He encontrado una solución más fácil de escribir (sin ruta-> transformación de expresiones regulares); Lo publicaré en breve.
intelfx
¡Genial, sería bueno ver lo que se te ocurrió! No tuve mucho tiempo para mejorar e iterar sobre esto.
slhck
hecho, y mi solución es dos funciones frente a una línea :)
intelfx
1

Aquí está mi propia solución para eso.
NOTA: No soy un gran amante de la portabilidad cuando se trata de shell y utilidades, por lo que posiblemente depende en gran medida de Bash 4 y GNU find.

Código

#!/bin/bash

## given "a/b/c/d", prints "a/b/c", "a/b" and "a".
# $1...: pathes to process
function get_parent_directories() {
    local CURRENT_CHUNK

    for arg; do
        CURRENT_CHUNK="$arg"

        while true; do
            CURRENT_CHUNK="$(dirname "$arg")"
            [[ "$CURRENT_CHUNK" == "." ]] && break
            echo "$CURRENT_CHUNK"
        done
    done
}

## recursively removes all files in given directory, except given names.
# $1: target directory
# $2...: exceptions
function remove_recursive() {
    local DIR="$1"
    shift
    local EXCEPTIONS=( "$@" )

    # find all files in given directory...
    local FIND_ARGS=( find "$DIR" -mindepth 1 )

    # ...skipping all exceptions and below...
    for file in "${EXCEPTIONS[@]}"; do
        FIND_ARGS+=( -path "$file" -prune -or )
    done

    # ...and ignoring all parent directories of exceptions (to avoid removing "./B" when "./B/A" is an exception)...
    while read file; do
        FIND_ARGS+=( -path "$file" -or )
    done < <(get_parent_directories "${EXCEPTIONS[@]}" | sort -u)

    # ...and printing all remaining names, without their descendants (we're going to recursively remove these anyway).
    FIND_ARGS+=( -print0 -prune )

    "${FIND_ARGS[@]}" | xargs -r0 rm -r
}

Explicación

La findlínea de comando resultante se construye como una cadena de -predicates -actions -orsecuencias.

Esto significa lo siguiente: para cada ruta, si tiene -predicateséxito, haga -actions, de lo contrario continúe con la siguiente secuencia. El último elemento de la cadena es justo -actions, que es el caso predeterminado.

Aquí, estoy haciendo -prunetodos los parches encontrados directamente en $EXCEPTIONS. Esto deja findde descender más allá de estos nombres.

A continuación, no estoy haciendo nada por todos los padres de padres $EXCEPTIONS. No queremos eliminar los directorios principales de excepciones, ya que la eliminación es recursiva.

Finalmente, estoy alimentando todos los parches restantes (el caso predeterminado) a xargs rm -r. Esto es más rápido que -exec rm -r {} \;porque solo se rmgenerará uno .

También lo hago -prunepor ellos porque no tiene sentido eliminarlos explícitamente ./A/B/Csi vamos a eliminarlos ./A/B.

PD: esto terminó en mi biblioteca de fragmentos :)

intelfx
fuente