Elimine todos los archivos excepto 12

14

Tengo algunos miles de archivos en el formato nombrearchivo.12345.end. Solo quiero mantener cada 12º archivo, así que file,00012.end, file,00024.end ... file.99996.end y borra todo lo demás.

Los archivos también pueden tener números anteriores en su nombre de archivo, y normalmente tienen la forma: file.00064.name.99999.end

Uso Bash shell y no puedo encontrar la manera de recorrer los archivos y luego obtener el número y verificar si está number%%12=0 borrando el archivo, si no. ¿Alguien puede ayudarme?

Gracias Dorina

Dorina
fuente
¿El número del archivo solo depende del nombre del archivo?
Arronical
Además, ¿los archivos siempre tienen 5 dígitos y el sufijo y el prefijo son siempre los mismos?
Arronico
Sí, siempre tiene 5 dígitos. No estoy seguro si respondo bien su primera pregunta. Los archivos con diferentes nombres de archivo son diferentes, y que necesitan estos archivos específicos tales que tienen los números 00012, 00024, etc.
Dorina
3
@Dorina, edita tu pregunta y deja eso claro. ¡Lo cambia todo!
terdon
2
Y todos están en el mismo directorio, ¿verdad?
Sergiy Kolodyazhnyy

Respuestas:

18

Aquí hay una solución Perl. Esto debería ser mucho más rápido para miles de archivos:

perl -e '@bad=grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV; unlink @bad' *

Que se puede condensar aún más en:

perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

Si tiene demasiados archivos y no puede usar el simple *, puede hacer algo como:

perl -e 'opendir($d,"."); unlink grep{/(\d+)\.end/ && $1 % 12 != 0} readdir($dir)'

En cuanto a la velocidad, aquí hay una comparación de este enfoque y el shell proporcionado en una de las otras respuestas:

$ touch file.{01..64}.name.{00001..01000}.end
$ ls | wc
  64000   64000 1472000
$ time for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

real    2m44.258s
user    0m9.183s
sys     1m7.647s

$ touch file.{01..64}.name.{00001..01000}.end
$ time perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

real    0m0.610s
user    0m0.317s
sys     0m0.290s

Como puede ver, la diferencia es enorme, como se esperaba .

Explicación

  • El -ees simplemente decirle perla ejecutar la secuencia dada en la línea de comandos.
  • @ARGVes una variable especial que contiene todos los argumentos dados al script. Como lo estamos dando *, contendrá todos los archivos (y directorios) en el directorio actual.
  • El grepva a buscar a través de la lista de nombres de archivos y buscar los que coincidan con una cadena de números, un punto y end( /(\d+)\.end/).

  • Como los números ( \d) están en un grupo de captura (paréntesis), se guardan como $1. Entonces grepverificará si ese número es un múltiplo de 12 y, si no lo es, se devolverá el nombre del archivo. En otras palabras, la matriz @badcontiene la lista de archivos que se eliminarán.

  • Luego se pasa la lista a la unlink()que se eliminan los archivos (pero no los directorios).

terdon
fuente
12

Dado que sus nombres de archivo están en el formato file.00064.name.99999.end, primero necesitamos recortar todo excepto nuestro número. Usaremos un forbucle para hacer esto.

También debemos decirle al shell de Bash que use la base 10, porque la aritmética de Bash los tratará con números que comienzan con un 0 como base 8, lo que nos complicará las cosas.

Como secuencia de comandos, que se iniciará cuando esté en el directorio que contiene archivos, use:

#!/bin/bash

for f in ./*
do
  if [[ -f "$f" ]]; then
    file="${f%.*}"
    if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
      rm "$f"
    fi
  else
    echo "$f is not a file, skipping."
  fi
done

O puede usar este comando feo muy largo para hacer lo mismo:

for f in ./* ; do if [[ -f "$f" ]]; then file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; else echo "$f is not a file, skipping."; fi; done

Para explicar todas las partes:

  • for f in ./* significa para todo en el directorio actual, hacer .... Esto establece cada archivo o directorio encontrado como la variable $ f.
  • if [[ -f "$f" ]]comprueba si el elemento encontrado es un archivo; de lo contrario, saltamos a la echo "$f is not...parte, lo que significa que no comenzamos a eliminar directorios accidentalmente.
  • file="${f%.*}"establece la variable $ file como el recorte del nombre de archivo de lo que viene después del último ..
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]]es donde entra en acción la aritmética principal. ${file##*.}Recorta todo antes del último .en nuestro nombre de archivo sin extensión. $(( $num % $num2 ))es la sintaxis para que la aritmética de Bash use la operación de módulo, 10#al principio le dice a Bash que use la base 10, para lidiar con esos molestos ceros iniciales. $((10#${file##*.} % 12))luego nos deja el resto de nuestro número de nombres de archivo dividido por 12. -ne 0comprueba si el resto es "no igual" a cero.
  • Si el resto no es igual a 0, el archivo se elimina con el rmcomando, es posible que desee reemplazar rmcon echocuando se ejecuta por primera vez este, para comprobar que usted obtenga los archivos que se espera que desea eliminar.

Esta solución no es recursiva, lo que significa que solo procesará archivos en el directorio actual, no entrará en ningún subdirectorio.

La ifdeclaración con el echocomando para advertir sobre los directorios no es realmente necesaria ya que rmpor sí sola se quejará de los directorios y no los eliminará, así que:

#!/bin/bash

for f in ./*
do
  file="${f%.*}"
  if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
    rm "$f"
  fi
done

O

for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

Funcionará correctamente también.

Arronico
fuente
55
Llamar rmunos miles de veces puede ser bastante lento. Sugiero que echoel nombre de archivo en lugar y canalizar la salida del bucle de xargs rm(add opciones según sea necesario): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
David Foerster
He editado para incluir su mejora de velocidad sugerida.
Arronico
En realidad, después de probar en un directorio con 55999 archivos, la versión original tomó 2 minutos y 48 segundos, la xargsversión tomó 5 minutos y 1 segundo. ¿Podría esto ser debido a una sobrecarga en echo@DavidFoerster?
Arronico
Impar. Para 60.000 archivos obtengo 0m0.659s / 0m0.545s / 0m0.380s (real / user / sys) con time { for f in *; do echo "$f"; done | xargs rm; }vs. 1m11.450s / 0m10.695s / 0m16.800s con time { for f in *; do rm "$f"; done; }tmpfs. Bash es v4.3.11, Kernel es v4.4.19.
David Foerster
6

Puede usar la expansión de soporte de Bash para generar nombres que contengan cada número 12. Creemos algunos datos de prueba

$ touch file.{0..9}{0..9}{0..9}{0..9}{0..9}.end # create test data
$ mv file.00024.end file.00024.end.name.99999.end # testing this form of filenames

Entonces podemos usar lo siguiente

$ ls 'file.'{00012..100..12}* # print these with numbers less than 100
file.00012.end                 file.00036.end  file.00060.end  file.00084.end
file.00024.end.name.99999.end  file.00048.end  file.00072.end  file.00096.end
$ rm 'file.'{00012..100000..12}* # do the job

Sin embargo, funciona irremediablemente lento para una gran cantidad de archivos: se necesita tiempo y memoria para generar miles de nombres, por lo que es más un truco que una solución eficiente real.

Nykakin
fuente
Me gusta el código de golf en este caso.
David Foerster
1

Un poco largo, pero es lo que me vino a la mente.

 for num in $(seq 1 1 11) ; do
     for sequence in $(seq -f %05g $num 12 99999) ; do
         rm file.$sequence.end.99999;
     done
 done

Explicación: Eliminar cada 12 archivos once veces.

Terrik
fuente
0

Con toda humildad, creo que esta solución es mucho mejor que la otra respuesta:

find . -name '*.end' -depth 1 | awk 'NR%12 != 0 {print}' | xargs -n100 rm

Una pequeña explicación: primero generamos una lista de archivos con find. Obtenemos todos los archivos cuyo nombre termina .endy que tienen una profundidad de 1 (es decir, están directamente en el directorio de trabajo y no en ninguna subcarpeta. Puede omitirlo si no hay subcarpetas). La lista de salida se ordenará alfabéticamente.

Luego canalizamos esa lista awk, donde usamos la variable especial NRque es el número de línea. Dejamos cada 12 archivos imprimiendo los archivos donde NR%12 != 0. El awkcomando se puede acortar a awk 'NR%12', porque el resultado del operador de módulo se interpreta como un valor booleano y de {print}todos modos se hace implícitamente.

Así que ahora tenemos una lista de archivos que deben eliminarse, lo que podemos hacer con xargs y rm. xargsejecuta el comando dado ( rm) con la entrada estándar como argumentos.

Si tiene muchos archivos, recibirá un error que dice algo así como 'lista de argumentos demasiado larga' (en mi máquina ese límite es 256 kB, y el mínimo requerido por POSIX es 4096 bytes). La -n 100bandera puede evitar esto , que divide los argumentos cada 100 palabras (no líneas, algo a tener en cuenta si los nombres de los archivos tienen espacios) y ejecuta un rmcomando separado , cada uno con solo 100 argumentos.

usuario593851
fuente
3
Hay un par de problemas con su enfoque: -depthdebe ser anterior -name; ii) esto fallará si alguno de los nombres de archivo contiene espacios en blanco; iii) está asumiendo que los archivos se enumerarán en orden numérico ascendente (eso es lo que awkestá probando), pero esto seguramente no será el caso. Por lo tanto, esto eliminará un conjunto aleatorio de archivos.
terdon
d'oh! Tienes toda la razón, mi mal (comentario editado). Recibí el error debido a la ubicación incorrecta y no lo recordaba -depth. Aún así, ese fue el menor de los problemas aquí, el más importante es que está eliminando un conjunto aleatorio de archivos y no los que quiere el OP.
terdon
Ah, y no, -depthno tiene un valor y hace lo contrario de lo que crees que hace. Consulte man find: "-profundidad Procese el contenido de cada directorio antes que el directorio mismo". Así que esto realmente descenderá a subdirectorios y causará estragos en todo el lugar.
terdon
I) Ambos -depth ny -maxdepth nexisten. El primero requiere que la profundidad sea exactamente n, y con el segundo puede ser <= n. II) Sí, eso es malo, pero para este ejemplo en particular no es una preocupación. Puede solucionarlo utilizando find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rm, que utiliza el byte nulo como separador de registros (que no está permitido en los nombres de archivo). III) Una vez más, en este caso la suposición es razonable. De lo contrario, puede insertar un sort -nmedio findy awk, o redirigir finda un archivo y ordenarlo como desee.
user593851
3
Ah, entonces probablemente estés usando OSX. Esa es una implementación muy diferente de find. Una vez más, sin embargo, el problema principal es que está asumiendo que finddevuelve una lista ordenada. No lo hace.
terdon
0

Para usar solo bash, mi primer enfoque sería: 1. mover todos los archivos que desea mantener a otro directorio (es decir, todos aquellos cuyo número en el nombre de archivo es un múltiplo de 12) y luego 2. eliminar todos los archivos restantes en el directorio, luego 3. coloque los múltiples archivos de 12 que guardó donde estaban. Entonces algo como esto podría funcionar:

cd dir_containing_files
mkdir keep_these_files
n=0
while [ "${n}" -lt 99999 ]; do
  padded_n="`echo -n "00000${n}" | tail -c 5`"
  mv "filename${padded_n}.end" keep_these_files/
  n=$[n+12]
done
rm filename*.end
mv keep_these_files/* .
rmdir keep_these_files
delt
fuente
Me gusta el enfoque, pero ¿cómo se genera la filenamepieza si no es consistente?
Arronical