Exclua todos os arquivos X, exceto os mais recentes, no bash


157

Existe uma maneira simples, em um ambiente UNIX bastante padrão com bash, de executar um comando para excluir todos os arquivos X, exceto os mais recentes, de um diretório?

Para dar um exemplo um pouco mais concreto, imagine algum trabalho do cron escrevendo um arquivo (por exemplo, um arquivo de log ou um backup atrasado) em um diretório a cada hora. Gostaria de ter uma outra tarefa cron em execução que remova os arquivos mais antigos desse diretório até que haja menos de, digamos, 5.

E só para esclarecer, só há um arquivo presente, que nunca deve ser excluído.

Respostas:


117

Os problemas com as respostas existentes:

  • incapacidade de lidar com nomes de arquivos com espaços incorporados ou novas linhas.
    • no caso de soluções que invocam rmdiretamente em uma substituição de comando não citada ( rm `...`), há um risco adicional de globbing não intencional.
  • incapacidade de distinguir entre arquivos e diretórios (ou seja, se os diretórios estiverem entre os 5 itens do sistema de arquivos modificados mais recentemente, você efetivamente reterá menos de 5 arquivos e a aplicação rmnos diretórios falhará).

A resposta do wnoise trata desses problemas, mas a solução é específica para o GNU (e bastante complexa).

Aqui está uma solução pragmática, compatível com POSIX , que vem com apenas uma ressalva : ela não pode lidar com nomes de arquivos com novas linhas incorporadas - mas não considero isso uma preocupação do mundo real para a maioria das pessoas.

Para o registro, eis a explicação de por que geralmente não é uma boa ideia analisar a lssaída: http://mywiki.wooledge.org/ParsingLs

ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}

O acima exposto é ineficiente , porque xargsprecisa chamar rmuma vez para cada nome de arquivo.
Sua plataforma xargspode permitir que você solucione este problema:

Se você possui o GNU xargs , use -d '\n', o que xargsconsidera cada linha de entrada um argumento separado, mas passa tantos argumentos quanto cabem na linha de comando ao mesmo tempo :

ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --

-r( --no-run-if-empty) garante que rmnão seja chamado se não houver entrada.

Se você possui BSD xargs (inclusive no macOS ), pode usar -0para manipular NULentradas separadas, depois de traduzir novas linhas para NUL( 0x0) chars., Que também passa (normalmente) todos os nomes de arquivos de uma só vez (também funcionará com o GNU xargs):

ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --

Explicação:

  • ls -tpimprime os nomes dos itens do sistema de arquivos classificados por quão recentemente foram modificados, em ordem decrescente (itens modificados mais recentemente primeiro) ( -t), com diretórios impressos com um final /para marcá-los como tal ( -p).
  • grep -v '/$'em seguida, elimina os diretórios da lista resultante, omitindo ( -v) as linhas que possuem um final /( /$).
    • Advertência : Como um link simbólico que aponta para um diretório não é tecnicamente um diretório, esses links simbólicos não serão excluídos.
  • tail -n +6ignora os primeiros 5 entradas no perfil, na verdade retornando todos , mas os 5 arquivos modificados mais recentemente, se houver.
    Observe que, para excluir Narquivos, N+1deve ser passado para tail -n +.
  • xargs -I {} rm -- {}(e suas variações) então invoca rmtodos esses arquivos; se não houver correspondências, xargsnão fará nada.
    • xargs -I {} rm -- {}define o espaço reservado {}que representa cada linha de entrada como um todo ; portanto, rmé chamado uma vez para cada linha de entrada, mas com nomes de arquivos com espaços incorporados manipulados corretamente.
    • --em todos os casos garante que quaisquer nomes que acontecem para começar -não são confundidos com opções por rm.

Uma variação no problema original, caso os arquivos correspondentes precisem ser processados individualmente ou coletados em uma matriz de shell :

# One by one, in a shell loop (POSIX-compliant):
ls -tp | grep -v '/$' | tail -n +6 | while IFS= read -r f; do echo "$f"; done

# One by one, but using a Bash process substitution (<(...), 
# so that the variables inside the `while` loop remain in scope:
while IFS= read -r f; do echo "$f"; done < <(ls -tp | grep -v '/$' | tail -n +6)

# Collecting the matches in a Bash *array*:
IFS=$'\n' read -d '' -ra files  < <(ls -tp | grep -v '/$' | tail -n +6)
printf '%s\n' "${files[@]}" # print array elements

2
Certamente melhor do que a maioria das outras respostas aqui, por isso estou feliz em dar meu apoio, mesmo considerando que ignorar o caso da nova linha é algo que deve ser feito apenas com cautela.
Charles Duffy

2
Se você não estiver lsno diretório atual, os caminhos para os arquivos conterão '/', o que significa que grep -v '/'não corresponderá a nada. Eu acredito grep -v '/$'que é o que você deseja excluir apenas diretórios.
waldol1

1
@ waldol1: Obrigado; Atualizei a resposta para incluir sua sugestão, o que também torna o grepcomando conceitualmente mais claro. Observe, no entanto, que o problema que você descreve não teria surgido com um único caminho de diretório; por exemplo, ls -p /private/varainda imprimiria apenas meros nomes de arquivos. Somente se você passasse vários argumentos de arquivo (normalmente via glob) você veria os caminhos reais na saída; por exemplo, ls -p /private/var/*(e você também veria o conteúdo dos subdiretórios correspondentes, a menos que você também incluísse -d).
mklement0

108

Remova todos, exceto 5 (ou qualquer número) dos arquivos mais recentes de um diretório.

rm `ls -t | awk 'NR>5'`

2
Eu precisava disso para considerar apenas meus arquivos compactados. mudar ls -tparals -td *.bz2
James T Snell

3
Eu usei isso para diretórios alterando-o para rm -rf ls -t | awk 'NR>1'(eu só queria o mais recente). Obrigado!
precisa saber é o seguinte

11
ls -t | awk 'NR>5' | xargs rm -f se você prefere pipes e precisa suprimir o erro, se não houver nada a ser excluído.
H2ONaCl

16
Conciso e legível, talvez, mas perigoso de usar; se tentar excluir um arquivo criado com touch 'hello * world', isso excluirá absolutamente tudo no diretório atual .
Charles Duffy

1
Embora isso tenha sido respondido em 2008, ele funciona como um encanto e exatamente o que eu precisava para excluir backups antigos de um diretório específico. Impressionante.
Rens Tillmann

86
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm

Esta versão suporta nomes com espaços:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm

20
Este comando não manipula corretamente arquivos com espaços nos nomes.
precisa saber é o seguinte

5
(ls -t|head -n 5;ls)é um grupo de comandos . Imprime os 5 arquivos mais recentes duas vezes. sortcoloca linhas idênticas juntas. uniq -uremove duplicatas, para que todos, exceto os 5 arquivos mais recentes, permaneçam. xargs rmchama rmcada um deles.
Fabien

15
Isso exclui todos os seus arquivos se você tiver 5 ou menos! Adicione --no-run-if-emptya xargscomo (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rm, atualize a resposta.
Gonfi den Tschal

3
Mesmo aquele que "apóia nomes com espaços" é perigoso. Considere um nome que contenha aspas literais: touch 'foo " bar'descartará todo o restante do comando.
Charles Duffy

2
... é mais seguro usar xargs -d $'\n'do que injetar aspas no seu conteúdo, embora a delimitação NUL do fluxo de entrada (que requer o uso de algo diferente do lsque realmente seja feito corretamente) seja a opção ideal.
Charles Duffy

59

Variante mais simples da resposta de thelsdj:

ls -tr | head -n -5 | xargs --no-run-if-empty rm 

ls -tr exibe todos os arquivos, os mais antigos primeiro (-t os mais novos primeiro, -r reverso).

head -n -5 exibe todas, exceto as 5 últimas linhas (ou seja, os 5 arquivos mais recentes).

xargs rm chama rm para cada arquivo selecionado.


15
É necessário adicionar --no-run-if-empty ao xargs para que não falhe quando houver menos de 5 arquivos.
Tom

ls -1tr | cabeça -n -5 | xargs rm <---------- você precisa adicionar um -1 para os ls ou você não vai obter uma saída lista de cabeça para adequadamente trabalho contra
Al Joslin

3
@AlJoslin, -1é padrão quando a saída é para um pipeline, portanto, não é obrigatório aqui. Isso tem problemas muito maiores, relacionados ao comportamento padrão xargsao analisar nomes com espaços, aspas, etc.
Charles Duffy

parece que o --no-run-if-emptynão é reconhecido no meu shell. Estou usando o Cmder no Windows.
StayFoolish

Pode ser necessário usar a -0opção se os nomes de arquivos puderem conter espaços em branco. Ainda não testei ainda. fonte
Keith

18
find . -maxdepth 1 -type f -printf '%T@ %p\0' | sort -r -z -n | awk 'BEGIN { RS="\0"; ORS="\0"; FS="" } NR > 5 { sub("^[0-9]*(.[0-9]*)? ", ""); print }' | xargs -0 rm -f

Requer localização GNU para -printf, e classificação GNU para -z, e GNU awk para "\ 0" e GNU xargs para -0, mas lida com arquivos com novas linhas ou espaços incorporados.


2
Se você deseja remover os diretórios, altere -f para a -d e adicione -r à rm. encontrar . -maxdepth 1 -type d -printf '% T @% p \ 0' | ordenar -r -z -n | awk 'BEGIN {RS = "\ 0"; ORS = "\ 0"; FS = ""} NR> 5 {sub ("^ [0-9] * (. [0-9] *)?", ""); print} '| xargs -0 rm -rf
alex

1
De relance, estou surpreso com a complexidade (ou, nesse caso, a necessidade) da awklógica. Estou perdendo alguns requisitos da pergunta do OP que o tornam necessário?
Charles Duffy

@ Charles Duffy: O sub () remove o registro de data e hora, que é o que está classificado. O registro de data e hora produzido por "% T @" pode incluir uma parte da fração. A divisão no espaço com o FS quebra os caminhos com espaços incorporados. Suponho que seja removida através do primeiro espaço, mas é quase tão difícil de ler. Os separadores RS e ORS não podem ser configurados na linha de comando, porque são NULs.
Wnoise

1
@ wnoise, minha abordagem usual para isso é canalizar em um while read -r -d ' '; IFS= -r -d ''; do ...loop de shell - a primeira leitura termina no espaço, enquanto a segunda continua no NUL.
Charles Duffy

@ Charles Duffy: Estou sempre desconfiado de casca crua, talvez devido a preocupações de citações bizantinas. Agora acho que o GNU sed -z -e 's/[^ ]* //; 1,5d'é o mais claro. (ou talvez sed -n -z -e 's/[^ ]* //; 6,$p'.
wnoise

14

Todas essas respostas falham quando há diretórios no diretório atual. Aqui está algo que funciona:

find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm

Este:

  1. funciona quando existem diretórios no diretório atual

  2. tenta remover cada arquivo, mesmo que o anterior não possa ser removido (devido a permissões etc.)

  3. falha na segurança quando o número de arquivos no diretório atual é excessivo e xargsnormalmente atrapalhava você (o-x )

  4. não atende a espaços nos nomes de arquivos (talvez você esteja usando o sistema operacional errado?)


5
O que acontece se findretornar mais nomes de arquivo do que pode ser passado em uma única linha de comando ls -t? (Dica: você obtém várias execuções ls -t, cada uma das quais é classificada apenas individualmente, em vez de ter uma ordem de classificação globalmente correta; portanto, essa resposta é muito quebrada ao executar com diretórios suficientemente grandes).
Charles Duffy

12
ls -tQ | tail -n+4 | xargs rm

Listar nomes de arquivos por hora da modificação, citando cada nome de arquivo. Excluir os 3 primeiros (3 mais recentes). Remova o restante.

EDITAR após um comentário útil de mklement0 (obrigado!): Corrigiu o argumento -n + 3 e observe que isso não funcionará conforme o esperado se os nomes de arquivos contiverem novas linhas e / ou o diretório contiver subdiretórios.


A -Qopção parece não existir na minha máquina.
Pierre-Adrien Buisson

4
Hmm, a opção está nos utilitários principais do GNU há ~ 20 anos, mas não é mencionada nas variantes do BSD. Você está em um mac?
Mark

Eu sou de fato. Não achava que havia diferenças para esse tipo de comando realmente básico entre os sistemas atualizados. Obrigado pela sua resposta!
Pierre-Adrien Buisson

3
@ Mark: ++ para -Q. Sim, -Qé uma extensão GNU (aqui está a especificação POSIXls ). Uma pequena ressalva (raramente um problema na prática): -Qcodifica novas linhas incorporadas nos nomes de arquivos como literais \n, que rmnão serão reconhecidas. Para excluir os 3 primeiros , o xargsargumento deve +4. Finalmente, uma ressalva que se aplica à maioria das outras respostas também: seu comando só funcionará como planejado se não houver subdiretórios no diretório atual.
precisa saber é o seguinte

1
Quando não há nada para remover, você tem xargs chamada com --no-run-if-emptyopção:ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Olivier Lécrivain

8

Ignorar novas linhas é ignorar a segurança e a boa codificação. Wnoise teve a única boa resposta. Aqui está uma variação dele que coloca os nomes dos arquivos em uma matriz $ x

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )

2
Eu sugeriria a limpeza IFS- caso contrário, você correria o risco de perder espaço em branco nos nomes de arquivos. Pode escopo isso para o comando de leitura:while IFS= read -rd ''; do
Charles Duffy

1
por que "${REPLY#* }"?
msciwoj

4

Se os nomes de arquivos não tiverem espaços, isso funcionará:

ls -C1 -t| awk 'NR>5'|xargs rm

Se os nomes de arquivos tiverem espaços, algo como

ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh

Lógica básica:

  • obtenha uma lista dos arquivos na ordem do tempo, uma coluna
  • obter todos, exceto os 5 primeiros (n = 5 para este exemplo)
  • primeira versão: envie para a rm
  • segunda versão: gera um script que os removerá corretamente

Não se esqueça do while readtruque para lidar com espaços: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
pinkeen

1
@pinkeen, não é tão seguro quanto dado lá. while IFS= read -r dseria um pouco melhor - -revita que os literais de barra invertida sejam consumidos reade IFS=evita o corte automático do espaço em branco à direita.
Charles Duffy

4
BTW, se alguém estiver preocupado com nomes de arquivos hostis, essa é uma abordagem extremamente perigosa. Considere um arquivo criado com touch $'hello \'$(rm -rf ~)\' world'; as aspas literais dentro do nome do arquivo contrariam as aspas literais adicionadas sed, resultando na execução do código no nome do arquivo.
Charles Duffy

1
(para ficar claro, o "isto" acima se referia ao | shformulário, que é o que possui a vulnerabilidade de injeção do shell).
Charles Duffy

2

Com zsh

Supondo que você não se preocupe com os diretórios atuais e não terá mais de 999 arquivos (escolha um número maior, se desejar, ou crie um loop while).

[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])

Em *(.om[6,999]), os .arquivos de meios, os omeios ordem de classificação se, os mmeios por data de modificação (colocar aem tempo de acesso ou cpara a mudança inode), o [6,999]escolhe uma gama de arquivo, assim não rm a 5 pela primeira vez.


Intrigante, mas por toda a minha vida, eu não consegui fazer o qualificador glob de classificação ( om) funcionar (qualquer classificação que eu tentei não teve efeito - nem no OSX 10.11.2 (tente com o zsh 5.0.8 e 5.1.1) , nem no Ubuntu 14.04 (zsh 5.0.2)) - o que estou perdendo ?. Quanto ao endpoint intervalo: não há necessidade de codificar-lo, basta usar -1para se referir à última entrada e, portanto, incluir todos os arquivos restantes: [6,-1].
precisa saber é o seguinte

2

Sei que esse é um tópico antigo, mas talvez alguém se beneficie disso. Este comando encontrará arquivos no diretório atual:

for F in $(find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n' | sort -r -z -n | tail -n+5 | awk '{ print $2; }'); do rm $F; done

Isso é um pouco mais robusto do que algumas das respostas anteriores, pois permite limitar seu domínio de pesquisa a arquivos correspondentes a expressões. Primeiro, encontre os arquivos que correspondem às condições desejadas. Imprima esses arquivos com os carimbos de data e hora próximos a eles.

find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n'

Em seguida, classifique-os pelos registros de data e hora:

sort -r -z -n

Em seguida, retire os 4 arquivos mais recentes da lista:

tail -n+5

Pegue a segunda coluna (o nome do arquivo, não o carimbo de data e hora):

awk '{ print $2; }'

E então embrulhe tudo isso em uma declaração for:

for F in $(); do rm $F; done

Esse pode ser um comando mais detalhado, mas tive uma sorte muito maior de poder direcionar arquivos condicionais e executar comandos mais complexos contra eles.


1

achei cmd interessante em Sed-Onliners - Exclua as últimas 3 linhas - encontre perfeito para outra maneira de esfolar o gato (tudo bem, não), mas a ideia:

 #!/bin/bash
 # sed cmd chng #2 to value file wish to retain

 cd /opt/depot 

 ls -1 MyMintFiles*.zip > BigList
 sed -n -e :a -e '1,2!{P;N;D;};N;ba' BigList > DeList

 for i in `cat DeList` 
 do 
 echo "Deleted $i" 
 rm -f $i  
 #echo "File(s) gonzo " 
 #read junk 
 done 
 exit 0

1

Remove todos, exceto os 10 arquivos mais recentes (mais recentes)

ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm

Se menos de 10 arquivos, nenhum arquivo for removido e você terá: erro head: contagem ilegal de linhas - 0

Para contar arquivos com bash


1

Eu precisava de uma solução elegante para o busybox (roteador), todas as soluções xargs ou array eram inúteis para mim - esse comando não está disponível lá. find e mtime não é a resposta correta, pois estamos falando de 10 itens e não necessariamente de 10 dias. A resposta de Espo foi a mais curta, limpa e provavelmente a mais não universal.

Erros com espaços e quando nenhum arquivo deve ser excluído são simplesmente resolvidos da maneira padrão:

rm "$(ls -td *.tar | awk 'NR>7')" 2>&-

Versão um pouco mais educacional: podemos fazer tudo se usarmos o awk de maneira diferente. Normalmente, eu uso esse método para passar (retornar) variáveis ​​do awk para o sh. Enquanto lemos o tempo todo que não pode ser feito, imploro para diferir: aqui está o método.

Exemplo para arquivos .tar sem problemas em relação aos espaços no nome do arquivo. Para testar, substitua "rm" pelo "ls".

eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}')

Explicação:

ls -td *.tarlista todos os arquivos .tar classificados por hora. Para aplicar a todos os arquivos na pasta atual, remova a parte "d * .tar"

awk 'NR>7... pula as primeiras 7 linhas

print "rm \"" $0 "\"" constrói uma linha: rm "nome do arquivo"

eval executa

Como estamos usando rm, eu não usaria o comando acima em um script! O uso mais sábio é:

(cd /FolderToDeleteWithin && eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}'))

No caso de usar o ls -tcomando, não haverá nenhum dano em exemplos tolos como: touch 'foo " bar'etouch 'hello * world' . Não que nós sempre criamos arquivos com esses nomes na vida real!

Nota. Se quiséssemos passar uma variável para o sh dessa maneira, simplesmente modificaríamos a impressão (forma simples, sem espaços tolerados):

print "VarName="$1

para definir a variável VarNamepara o valor de $1. Várias variáveis ​​podem ser criadas de uma só vez. Isso VarNamese torna uma variável sh normal e pode ser normalmente usada em um script ou shell posteriormente. Portanto, para criar variáveis ​​com o awk e devolvê-las ao shell:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"

0
leaveCount=5
fileCount=$(ls -1 *.log | wc -l)
tailCount=$((fileCount - leaveCount))

# avoid negative tail argument
[[ $tailCount < 0 ]] && tailCount=0

ls -t *.log | tail -$tailCount | xargs rm -f

2
xargssem -0ou no mínimo -d $'\n'não é confiável; observe como isso se comporta com um arquivo com espaços ou caracteres de aspas em seu nome.
Charles Duffy

0

Eu fiz isso em um script de shell bash. Uso: keep NUM DIRonde NUM é o número de arquivos a serem mantidos e DIR é o diretório a ser limpo.

#!/bin/bash
# Keep last N files by date.
# Usage: keep NUMBER DIRECTORY
echo ""
if [ $# -lt 2 ]; then
    echo "Usage: $0 NUMFILES DIR"
    echo "Keep last N newest files."
    exit 1
fi
if [ ! -e $2 ]; then
    echo "ERROR: directory '$1' does not exist"
    exit 1
fi
if [ ! -d $2 ]; then
    echo "ERROR: '$1' is not a directory"
    exit 1
fi
pushd $2 > /dev/null
ls -tp | grep -v '/' | tail -n +"$1" | xargs -I {} rm -- {}
popd > /dev/null
echo "Done. Kept $1 most recent files in $2."
ls $2|wc -l
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.