Pergunta

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 mais concreto, imagine algum cron job gravando um arquivo (digamos, um arquivo de log ou um backup com tar) em um diretório a cada hora.Eu gostaria de ter outro cron job em execução que removesse os arquivos mais antigos desse diretório até que haja menos de, digamos, 5.

E só para ficar claro, há apenas um arquivo presente, ele nunca deve ser excluído.

Foi útil?

Solução

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 rm diretamente em uma substituição de comando sem aspas (rm `...`), há um risco adicional de globbing não intencional.
  • incapacidade de distinguir entre arquivos e diretórios (ou seja, se diretórios estivesse entre os 5 itens do sistema de arquivos modificados mais recentemente, você efetivamente manteria menos de 5 arquivos e aplicando rm para diretórios falhará).

resposta do wnoise aborda essas questões, mas a solução é GNU-específico (e bastante complexo).

Aqui está um pragmático, Solução compatível com POSIX que vem apenas com uma ressalva:ele não pode lidar com nomes de arquivos incorporados novas linhas - mas não considero isso uma preocupação real para a maioria das pessoas.

Para que conste, aqui está a explicação de por que geralmente não é uma boa ideia analisar ls saída: http://mywiki.wooledge.org/ParsingLs

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

O acima é ineficiente, porque xargs tem que invocar rm uma vez por cada nome do arquivo.
A sua plataforma xargs pode permitir que você resolva este problema:

Se você tem GNU xargs, usar -d '\n', que faz xargs considera cada linha de entrada um argumento separado, mas passa tantos argumentos quanto couberem em uma linha de comando de uma vez só:

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

-r (--no-run-if-empty) Assegura que rm não é invocado se não houver entrada.

Se você tem BSD xargs (incluindo em OS X), você pode usar -0 lidar NULentrada separada, depois de primeiro traduzir as novas linhas para NUL (0x0) chars., que também passa (normalmente) todos os nomes de arquivos de uma vez só (também funcionará com GNU xargs):

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

Explicação:

  • ls -tp imprime os nomes dos itens do sistema de arquivos classificados pela data de modificação recente, em ordem decrescente (os 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 listagem resultante, omitindo (-v) linhas que têm um final / (/$).
    • Embargo:Desde um link simbólico que aponta para um diretório tecnicamente não é um diretório, esses links simbólicos serão não ser excluído.
  • tail -n +6 pula o primeiro 5 entradas na listagem, na verdade retornando todos mas os 5 arquivos modificados mais recentemente, se houver.
    Observe que para excluir N arquivos, N+1 deve ser passado para tail -n +.
  • xargs -I {} rm -- {} (e suas variações) então invoca em rm em todos esses arquivos;se não houver nenhuma correspondência, xargs não fará nada.
    • xargs -I {} rm -- {} define espaço reservado {} que representa cada linha de entrada como um todo, então rm é então invocado uma vez para cada linha de entrada, mas com nomes de arquivos com espaços incorporados tratados corretamente.
    • -- em todos os casos garante que quaisquer nomes de arquivos que comecem com - não são confundidos com opções por rm.

A variação sobre o problema original, caso os arquivos correspondentes precisem ser processados individualmente ou coletados em uma matriz 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

Outras dicas

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

rm `ls -t | awk 'NR>5'`
(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

Variante mais simples da resposta do thelsdj:

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

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

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

xargs rm chama rm para cada arquivo selecionado.

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 GNU find para -printf e GNU sort para -z e GNU awk para "\0" e GNU xargs para -0, mas lida com arquivos com novas linhas ou espaços incorporados.

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

Esse:

  1. funciona quando há 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 xargs normalmente iria ferrar você (o -x)

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

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

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

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

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

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

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

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

Se os nomes dos 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 em ordem temporal, uma coluna
  • obtenha todos, exceto os 5 primeiros (n = 5 para este exemplo)
  • primeira versão:mande isso para rm
  • segunda versão:gere um script que irá removê-los corretamente

Com zsh

Supondo que você não se importe com os diretórios atuais e não terá mais do que 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]), o . significa arquivos, o o significa ordem de classificação, o m significa por data de modificação (colocar a para tempo de acesso ou c para mudança de inode), o [6,999] escolhe um intervalo de arquivos, portanto não executa os 5 primeiro.

Sei que este é um tópico antigo, mas talvez alguém se beneficie com isso.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 que correspondam a expressões.Primeiro, encontre os arquivos que correspondam à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 carimbos de data/hora:

sort -r -z -n

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

tail -n+5

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

awk '{ print $2; }'

E então resumir tudo em uma declaração for:

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

Este pode ser um comando mais detalhado, mas tive muito mais sorte em conseguir direcionar arquivos condicionais e executar comandos mais complexos contra eles.

encontrei cmd interessante em Sed-Onliners - Exclua as últimas 3 linhas - ache-o perfeito para outra maneira de esfolar o gato (ok, não), mas 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

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

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

Se houver menos de 10 arquivos, nenhum arquivo será removido e você terá:cabeça de erro:contagem de linhas ilegais - 0

Para contar arquivos com bash

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

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

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

Versão um pouco mais educacional:Podemos fazer tudo isso se usarmos o awk de forma diferente.Normalmente, eu uso esse método para passar (retornar) variáveis ​​do awk para o sh.Como lemos o tempo todo que não pode ser feito, discordo: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” por “ls”.

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

Explicação:

ls -td *.tar lista todos os arquivos .tar classificados por hora.Para aplicar a todos os arquivos da 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

Já que 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 ls -t comando não causará nenhum dano em exemplos bobos como: touch 'foo " bar' e touch 'hello * world'.Não que alguma vez criemos arquivos com esses nomes na vida real!

Nota.Se quiséssemos passar uma variável para o sh desta forma, simplesmente modificaríamos o print (formato simples, sem tolerância de espaços):

print "VarName="$1

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

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

Eu transformei isso em um script de shell bash.Uso: keep NUM DIR onde 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

Rodando no Debian (suponha que seja o mesmo em outras distros que recebo:rm:não é possível remover o diretório `..'

o que é bastante chato..

De qualquer forma, ajustei o acima e também adicionei grep ao comando.No meu caso, tenho 6 arquivos de backup em um diretório, por exemplo.arquivo1.tar arquivo2.tar arquivo3.tar etc e quero excluir apenas o arquivo mais antigo (remover o primeiro arquivo no meu caso)

O script que executei para excluir o arquivo mais antigo foi:

LS -C1 -T | Arquivo Grep | awk 'nr> 5' | xargs rm

Isso (como acima) exclui o primeiro dos meus arquivos, por ex.arquivo1.tar também fica com arquivo2 arquivo3 arquivo4 arquivo5 e arquivo6

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top