Remova todos os arquivos, exceto todos os 12º


14

Eu tenho alguns milhares de arquivos no formato filename.12345.end. Eu só quero manter cada 12º arquivo, então file.00012.end, file.00024.end ... file.99996.end e exclua todo o resto.

Os arquivos também podem ter números anteriores no nome do arquivo e normalmente têm o formato: file.00064.name.99999.end

Eu uso o shell Bash e não consigo descobrir como fazer um loop sobre os arquivos e, em seguida, obter o número e verificar se ele está number%%12=0 excluindo o arquivo, se não estiver. Alguém pode me ajudar?

Obrigado Dorina


O número do arquivo depende apenas do nome do arquivo?
Arronical

Além disso, os arquivos sempre têm 5 dígitos e o sufixo e o prefixo sempre são os mesmos?
Arronical

Sim, são sempre 5 dígitos. Não tenho certeza se acertar sua primeira pergunta. Os arquivos com nomes de arquivos diferentes são diferentes, e eu preciso desses arquivos específicos que acontecem ter os números 00012, 00024, etc.
Dorina

3
@Dorina please edite sua pergunta e deixe isso claro. Isso muda tudo!
terdon 12/09/16

2
E eles estão todos no mesmo diretório, certo?
Sergiy Kolodyazhnyy 12/09

Respostas:


18

Aqui está uma solução Perl. Isso deve ser muito mais rápido para milhares de arquivos:

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

Que pode ser ainda mais condensado em:

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

Se você tiver muitos arquivos e não puder usar o simples *, poderá fazer algo como:

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

Quanto à velocidade, aqui está uma comparação dessa abordagem e a shell fornecida em uma das outras respostas:

$ 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 você pode ver, a diferença é enorme, como esperado .

Explicação

  • O -esimplesmente está dizendo perlpara executar o script fornecido na linha de comando.
  • @ARGVé uma variável especial que contém todos os argumentos fornecidos ao script. Como estamos fornecendo *, ele conterá todos os arquivos (e diretórios) no diretório atual.
  • O greppesquisará a lista de nomes de arquivos e procurará por qualquer que corresponda a uma sequência de números, um ponto e end(/(\d+)\.end/) .

  • Como os números ( \d) estão em um grupo de captura (parênteses), eles são salvos como $1. Portanto grep, verificará se esse número é múltiplo de 12 e, se não for, o nome do arquivo será retornado. Em outras palavras, a matriz @badcontém a lista de arquivos a serem excluídos.

  • A lista é então passada para a unlink()qual remove os arquivos (mas não os diretórios).


12

Como seus nomes de arquivo estão no formato file.00064.name.99999.end, primeiro precisamos aparar tudo, exceto nosso número. Usaremos um forloop para fazer isso.

Também precisamos dizer ao shell do Bash para usar a base 10, porque a aritmética do Bash tratará os números começando com um 0 como base 8, o que irá atrapalhar as coisas para nós.

Como um script, para ser iniciado quando no diretório que contém arquivos, 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

Ou você pode usar este comando muito longo e feio para fazer a mesma coisa:

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 as partes:

  • for f in ./* significa para tudo no diretório atual, faça .... Isso define cada arquivo ou diretório encontrado como a variável $ f.
  • if [[ -f "$f" ]]verifica se o item encontrado é um arquivo; caso contrário, saltamos para a echo "$f is not...parte, o que significa que não começamos a excluir diretórios acidentalmente.
  • file="${f%.*}" define a variável $ file como o nome do arquivo aparando o que vier depois da última . .
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]]é onde a aritmética principal entra em ação. Ela ${file##*.}apara tudo antes da última .em nosso nome de arquivo sem extensão. $(( $num % $num2 ))é a sintaxe da aritmética do Bash para usar a operação de módulo, 10#no início diz ao Bash para usar a base 10, para lidar com os 0s iniciais traquinas. $((10#${file##*.} % 12))então nos deixa o restante do número de nossos nomes de arquivos dividido por 12. -ne 0verifica se o restante é "diferente de zero".
  • Se o restante não for igual a 0, o arquivo será excluído com o rmcomando, você poderá substituí-lo rmpela echoprimeira vez, para verificar se os arquivos esperados serão excluídos.

Essa solução não é recursiva, o que significa que processará apenas os arquivos no diretório atual e não entrará em nenhum subdiretório.

A ifdeclaração com o echocomando para avisar sobre diretórios não é realmente necessária, pois rm, por si só, reclamará de diretórios e não os excluirá, portanto:

#!/bin/bash

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

Ou

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

Também funcionará corretamente.


5
Ligar rmalguns milhares de vezes pode ser bem lento. Sugiro echoo nome do arquivo em vez disso e canalizar a saída do loop para xargs rm(opções de adicionar como necessário): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
David Foerster

Editei para incluir sua melhoria de velocidade sugerida.
Arronical 13/09/16

Na verdade, depois de testar em um diretório com 55999 arquivos, a versão original xargslevou 2 minutos e 48 segundos e a versão levou 5 minutos e 1 segundo. Isso pode ser echocausado por sobrecarga no @DavidFoerster?
Arronical 13/09/16

Ímpar. Para 60.000 arquivos, recebo 0m0.659s / 0m0.545s / 0m0.380s (real / usuário / sys) com time { for f in *; do echo "$f"; done | xargs rm; }vs. 1m11.450s / 0m10.695s / 0m16.800s com time { for f in *; do rm "$f"; done; }um tmpfs. O Bash é a v4.3.11, o kernel é a v4.4.19.
David Foerster

6

Você pode usar a expansão de colchete Bash para gerar nomes contendo cada 12º número. Vamos criar alguns dados de teste

$ 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

Então podemos usar o seguinte

$ 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

No entanto, funciona desesperadamente lento para uma grande quantidade de arquivos - leva tempo e memória para gerar milhares de nomes - por isso é mais um truque que a solução eficiente.


Eu gosto do código de golfe neste.
David Foerster

1

Um pouco longo, mas é o que me veio à 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

Explicação: Exclua cada 12º arquivo onze vezes.


0

Com toda humildade, acho que essa solução é muito melhor do que a outra resposta:

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

Uma pequena explicação: Primeiro, geramos uma lista de arquivos com find. Obtemos todos os arquivos cujo nome termina com.end e que estão na profundidade de 1 (ou seja, eles estão diretamente no diretório de trabalho e não em nenhuma subpasta. Você pode deixar isso de fora se não houver subpastas). A lista de saída será classificada em ordem alfabética.

Em seguida, canalizamos essa lista para awkonde usamos a variável especial NRque é o número da linha. Deixamos de fora todos os arquivos 12, imprimindo os arquivos onde NR%12 != 0. O awkcomando pode ser abreviado para awk 'NR%12', porque o resultado do operador módulo é interpretado como um valor booleano e, de {print}qualquer forma, é implicitamente feito.

Portanto, agora temos uma lista de arquivos que precisam ser excluídos, o que podemos fazer com xargs e rm. xargsexecuta o comando fornecido ( rm) com a entrada padrão como argumentos.

Se você tiver muitos arquivos, receberá um erro ao dizer algo como 'lista de argumentos muito longa' (na minha máquina esse limite é de 256 kB e o mínimo exigido pelo POSIX é de 4096 bytes). Isso pode ser evitado pela -n 100bandeira, que divide os argumentos a cada 100 palavras (não linhas, algo a ser observado se os nomes dos arquivos tiverem espaços) e executa um rmcomando separado , cada um com apenas 100 argumentos.


3
Existem alguns problemas com sua abordagem: -depthprecisa ser anterior -name; ii) isso falhará se algum dos nomes de arquivo contiver espaço em branco; iii) você está assumindo que os arquivos serão listados em ordem numérica crescente (é para isso que você awkestá testando), mas isso quase certamente não será o caso. Portanto, isso excluirá um conjunto aleatório de arquivos.
terdon 12/09/16

d'oh! Você está certo, meu mal (comentário editado). Recebi o erro por causa do posicionamento errado e não me lembrava -depth. Ainda assim, esse foi o menor dos problemas aqui, o mais importante é que você está excluindo um conjunto aleatório de arquivos e não os que o OP deseja.
terdon 12/09/16

Ah, e não, -depthnão tem um valor e faz o oposto do que você pensa que faz. Veja man find: "-thp Processa o conteúdo de cada diretório antes do próprio diretório.". Portanto, isso realmente desce em subdiretórios e causa estragos por todo o lugar.
terdon 12/09/16

I) Ambos -depth ne -maxdepth nexistem. O primeiro requer que a profundidade seja exatamente n e, com o segundo, pode ser <= n. II) Sim, isso é ruim, mas para este exemplo em particular não é motivo de preocupação. Você pode corrigi-lo usando find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rm, que usa o byte nulo como separador de registros (o que não é permitido em nomes de arquivos). III) Mais uma vez, neste caso, a suposição é razoável. Caso contrário, você poderá inserir um sort -nentre finde awk, ou redirecionar findpara um arquivo e classificá-lo como quiser.
user593851

3
Ah, você provavelmente está usando o OSX então. Essa é uma implementação muito diferente do find. Novamente, no entanto, o principal problema é que você está assumindo que findretorna uma lista classificada. Não faz.
terdon 12/09/16

0

Para usar apenas o bash, minha primeira abordagem seria: 1. mover todos os arquivos que você deseja manter em outro diretório (ou seja, todos aqueles cujo número no nome do arquivo é múltiplo de 12) e depois 2. excluir todos os arquivos restantes no diretório, 3. coloque os múltiplos de 12 arquivos que você guardou onde estavam. Então, algo assim pode 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

Eu gosto da abordagem, mas como você gera a filenamepeça se ela não é consistente?
Arronical 14/09/16
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.