Exclua todos os arquivos X mais recentes no bash

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

Para dar um pouco mais de um exemplo concreto, imagine algum cron job gravando um arquivo (digamos, um arquivo de log ou um backup tar-up) em um diretório a cada hora. Gostaria de ter uma outra tarefa do cron em execução, removendo os arquivos mais antigos nesse diretório até que haja menos do que, digamos, 5.

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

Os problemas com as respostas existentes:

  • incapacidade de lidar com nomes de arquivos com espaços embutidos 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 globalização não intencional.
  • incapacidade de distinguir entre arquivos e diretórios (ou seja, se os diretórios estivessem entre os 5 itens de sistema de arquivos modificados mais recentemente, você efetivamente reteria menos de 5 arquivos e a aplicação de rm nos diretórios falhará).

A resposta de wnoise aborda esses problemas, mas a solução é específica do 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 manipular nomes de arquivos com novas linhas embutidas – mas eu não considero isso uma preocupação do mundo real para a maioria das pessoas.

Para que fique registrado, aqui está a explicação de por que geralmente não é uma boa ideia analisar a saída ls : 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 para cada nome de arquivo.
Os xargs da sua plataforma podem permitir que você resolva este problema:

Se você tiver o GNU xargs , use -d '\n' , o que faz com que os xargs considerem cada linha de input como um argumento separado, mas passem tantos argumentos quanto couberem em uma linha de comando de uma só vez :

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

-r ( --no-run-if-empty ) garante que rm não seja invocado se não houver input.

Se você tiver BSD xargs (incluindo no OS X ), você pode usar -0 para manipular a input separada por NUL , depois de traduzir novas linhas para NUL ( 0x0 ), que também passa (normalmente) todos os nomes de arquivos de uma só vez 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 ordenados pelo quão recentemente foram modificados, em ordem decrescente (os itens modificados mais recentemente primeiro) ( -t ), com diretórios impressos com um trailing / para marcá-los como tal ( -p ).
  • grep -v '/$' então elimina os diretórios da lista resultante, omitindo ( -v ) as linhas que possuem um / ( /$ ).
    • Advertência : Como um link simbólico que aponta para um diretório não é tecnicamente um diretório, tais links simbólicos não serão excluídos.
  • tail -n +6 pula as 5 primeiras inputs da lista, retornando todos, exceto 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), em seguida, invoca em rm em todos esses arquivos; se não houver correspondências, os xargs não farão nada.
    • xargs -I {} rm -- {} define o espaço reservado {} que representa cada linha de input como um todo , então rm é então chamado uma vez para cada linha de input, mas com nomes de arquivos com espaços incorporados manipulados corretamente.
    • -- em todos os casos, garante que qualquer nome de arquivo que comece com - não seja confundido com opções por rm .

Uma variação do 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 -rf; 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 -rf; 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 
 (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 

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

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

Variante mais simples da resposta de thelsdj:

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

ls -tr exibe todos os arquivos, o mais antigo primeiro (-t mais novo primeiro, -r reverso).

head -n -5 exibe todas menos as 5 últimas linhas (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 manipula arquivos com novas linhas ou espaços embutidos.

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 permissions, etc.)

  3. falha segura quando o número de arquivos no diretório atual é excessivo e xargs normalmente o -x (o -x )

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

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

Listar nomes de arquivos por hora de modificação, citando cada nome de arquivo. Exclui primeiro 3 (3 mais recentes). Remova o restante.

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

Ignorar novas linhas está ignorando a segurança e 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 ) 

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:

  • obter uma listview dos arquivos em ordem de tempo, uma coluna
  • obter todos, mas os primeiros 5 (n = 5 para este exemplo)
  • primeira versão: envie para rm
  • segunda versão: gen um script que irá removê-los corretamente

Com zsh

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

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

Em *(.om[6,999]) , o . significa arquivos, o significa ordenar acima, o m significa por data de modificação (colocar a para o tempo de access ou c para mudança de inode), o [6,999] escolhe um intervalo de arquivo, então não rm o 5 primeiro.

Eu percebo que este é um tópico antigo, mas talvez alguém se beneficie disso. Este comando irá 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 o domínio de pesquisa aos arquivos correspondentes às expressões. Primeiro, encontre arquivos correspondentes às condições desejadas. Imprima esses arquivos com os timestamps ao lado deles.

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

Em seguida, ordene-os pelos timestamps:

 sort -r -z -n 

Em seguida, copie os quatro arquivos mais recentes da lista:

 tail -n+5 

Pegue a segunda coluna (o nome do arquivo, não o timestamp):

 awk '{ print $2; }' 

E depois embrulhe tudo isso em uma declaração:

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

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

Encontrei cmd interessante em Sed-Onliners – Apague as últimas 3 linhas – e é perfeito para outra maneira de esfolar o gato (ok, não), mas idéia:

  #!/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 
 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 fiz 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 removido.

 #!/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 

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 não forem removidos, você terá: cabeça de erro: contagem de linha ilegal – 0

Para contar arquivos com bash

Rodando no Debian (suponha que seja igual em outras distribuições: rm: não pode remover o diretório `.. ‘

o que é muito chato ..

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

O script que eu executei para excluir o arquivo mais antigo era:

ls -C1 -t | arquivo grep | awk ‘NR> 5’ | xargs rm

Isso (como acima) exclui o primeiro dos meus arquivos, por exemplo, file1.tar isso também deixa com arquivo2 arquivo3 arquivo4 arquivo5 e arquivo6