Elimina tutti i file X tranne quelli più recenti in bash


157

Esiste un modo semplice, in un ambiente UNIX piuttosto standard con bash, di eseguire un comando per eliminare tutti i file X tranne quelli più recenti da una directory?

Per fare un esempio un po 'più concreto, immagina un cron job che scriva un file (per esempio un file di registro o un backup tarato) in una directory ogni ora. Vorrei un modo per avere un altro cron job in esecuzione che rimuovesse i file più vecchi in quella directory fino a quando non ci sono meno, diciamo, 5.

E per essere chiari, c'è solo un file presente, non dovrebbe mai essere cancellato.

Risposte:


117

I problemi con le risposte esistenti:

  • incapacità di gestire nomi di file con spazi incorporati o nuove righe.
    • nel caso di soluzioni che invocano rmdirettamente una sostituzione di comando non quotata ( rm `...`), c'è un ulteriore rischio di globbing involontario.
  • incapacità di distinguere tra file e directory (ad esempio, se le directory fossero tra i 5 elementi del filesystem modificati più di recente, si conserverebbero effettivamente meno di 5 file e l'applicazione rmalle directory fallirà).

La risposta di wnoise affronta questi problemi, ma la soluzione è specifica per GNU (e piuttosto complessa).

Ecco una soluzione pragmatica, conforme a POSIX che viene fornita con un solo avvertimento : non è in grado di gestire i nomi di file con le nuove linee incorporate , ma non considero una preoccupazione del mondo reale per la maggior parte delle persone.

Per la cronaca, ecco la spiegazione del perché generalmente non è una buona idea analizzare l' lsoutput: http://mywiki.wooledge.org/ParsingLs

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

Quanto sopra è inefficiente , perché xargsdeve invocare rmuna volta per ogni nome file.
La tua piattaforma xargspotrebbe consentirti di risolvere questo problema:

Se hai GNU xargs , usa -d '\n', il che rende xargsogni riga di input un argomento separato, ma passa quanti più argomenti si adatta a una riga di comando contemporaneamente :

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

-r( --no-run-if-empty) assicura che rmnon sia invocato se non c'è input.

Se hai BSD xargs (incluso su macOS ), puoi usare -0per gestire NULinput separati, dopo aver prima tradotto le nuove righe in NUL( 0x0) caratteri., Che passa anche (in genere) tutti i nomi di file contemporaneamente (funzionerà anche con GNU xargs):

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

Spiegazione:

  • ls -tpstampa i nomi degli elementi del filesystem ordinati in base alla loro recente modifica, in ordine decrescente (gli ultimi elementi modificati per primi) ( -t), con le directory stampate con un trascinamento /per contrassegnarli come tali ( -p).
  • grep -v '/$'quindi elimina le directory dall'elenco risultante, omettendo le -vrighe ( ) che hanno un trailing /( /$).
    • Avvertenza : poiché un collegamento simbolico che punta a una directory non è tecnicamente di per sé una directory, tali collegamenti simbolici non saranno esclusi.
  • tail -n +6salta le prime 5 voci nell'elenco, in effetti restituendo tutti, tranne i 5 file modificati più di recente, se presenti.
    Si noti che per escludere i Nfile, è N+1necessario passare a tail -n +.
  • xargs -I {} rm -- {}(e le sue varianti) quindi invoca rmsu tutti questi file; se non ci sono partite, xargsnon farà nulla.
    • xargs -I {} rm -- {}definisce segnaposto {}che rappresenta ciascuna riga di input nel suo insieme , quindi rmviene quindi richiamato una volta per ogni riga di input, ma con nomi di file con spazi incorporati gestiti correttamente.
    • --in tutti i casi garantisce che tutti i nomi di file che capita di iniziare con -non sono scambiati per le opzioni di rm.

Una variante del problema originale, nel caso in cui i file corrispondenti debbano essere elaborati singolarmente o raccolti in un array di 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 migliore della maggior parte delle altre risposte qui, quindi sono felice di dare il mio supporto, anche se considero ignorare il caso Newline come una cosa da fare solo con cautela.
Charles Duffy,

2
Se non lssi trova nella directory corrente, i percorsi dei file conterranno '/', il che significa che grep -v '/'non corrisponderà a nulla. Credo grep -v '/$'sia quello che vuoi escludere solo le directory.
waldol1,

1
@ waldol1: grazie; Ho aggiornato la risposta per includere il tuo suggerimento, il che rende grepconcettualmente più chiaro il comando. Si noti, tuttavia, che il problema descritto non sarebbe emerso con un singolo percorso di directory; ad esempio, ls -p /private/varcontinuerebbe a stampare solo semplici nomi di file. Solo se hai passato più argomenti di file (in genere tramite un glob) vedresti i percorsi effettivi nell'output; ad es. ls -p /private/var/*(e vedresti anche i contenuti delle corrispondenti sottodirectory, a meno che tu non abbia incluso anche -d).
mklement0,

108

Rimuovere tutti tranne 5 (o qualsiasi numero) dei file più recenti in una directory.

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

2
Ne avevo bisogno per considerare solo i miei file di archivio. cambia ls -tinls -td *.bz2
James T Snell,

3
L'ho usato per le directory cambiandolo in rm -rf ls -t | awk 'NR>1'(volevo solo il più recente). Grazie!
lohiaguitar91,

11
ls -t | awk 'NR>5' | xargs rm -f se preferisci le pipe e devi eliminare l'errore se non c'è nulla da eliminare.
H2ONaCl

16
Conciso e leggibile, forse, ma pericoloso da usare; se si tenta di eliminare un file creato con touch 'hello * world', questo eliminerebbe assolutamente tutto nella directory corrente .
Charles Duffy,

1
Anche se questo ha avuto una risposta nel 2008, funziona come un fascino e proprio quello che mi serviva per eliminare semplicemente i vecchi backup da una directory specifica. Eccezionale.
Rens Tillmann,

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

Questa versione supporta i nomi con spazi:

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

20
Questo comando non gestirà correttamente i file con spazi nei nomi.
tylerl,

5
(ls -t|head -n 5;ls)è un gruppo di comandi . Stampa due volte i 5 file più recenti. sortmette insieme linee identiche. uniq -urimuove i duplicati, in modo che rimangano tutti tranne i 5 file più recenti. xargs rmchiama rmciascuno di loro.
Fabien,

15
Questo elimina tutti i tuoi file se ne hai 5 o meno! Aggiungi --no-run-if-emptya xargscome in per (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rmfavore aggiorna la risposta.
Gonfi den Tschal,

3
Anche quello che "supporta i nomi con spazi" è pericoloso. Considera un nome che contiene virgolette letterali: touch 'foo " bar'eliminerà l'intero resto del comando.
Charles Duffy,

2
... è più sicuro da usare xargs -d $'\n'rispetto quotazioni iniettare nel suo sito web, anche se NUL-delimita il flusso di input (che richiede l'uso di qualcosa di diverso lsper davvero fare il bene) è l'opzione ideale.
Charles Duffy

59

Variante più semplice della risposta di thelsdj:

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

ls -tr visualizza tutti i file, prima i più vecchi (-t prima i più recenti, -r indietro).

head -n -5 visualizza tutte tranne le ultime 5 righe (ovvero i 5 file più recenti).

xargs rm chiama rm per ogni file selezionato.


15
È necessario aggiungere --no-run-if-empty a xargs in modo che non fallisca quando ci sono meno di 5 file.
Tom,

ls -1tr | capo -n -5 | xargs rm <---------- devi aggiungere un -1 a ls o non otterrai un output dell'elenco per la testa che funzioni correttamente contro
Al Joslin

3
@AlJoslin, -1è predefinito quando l'output è verso una pipeline, quindi non è obbligatorio qui. Questo ha problemi molto più grandi, legati al comportamento predefinito di xargsquando si analizzano nomi con spazi, virgolette, ecc.
Charles Duffy,

sembra che il --no-run-if-emptynon sia riconosciuto nella mia shell. Sto usando Cmder su Windows.
Stupido

Potrebbe essere necessario utilizzare l' -0opzione se i nomi dei file potrebbero contenere spazi bianchi. Non l'ho ancora testato comunque. 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

Richiede GNU find per -printf e GNU sort per -z e GNU awk per "\ 0" e GNU xargs per -0, ma gestisce i file con newline o spazi incorporati.


2
Se vuoi rimuovere le directory, cambia -f in a -d e aggiungi a -r a rm. trova . -maxdepth 1 -type d -printf '% T @% p \ 0' | ordina -r -z -n | awk 'BEGIN {RS = "\ 0"; ORS = "\ 0"; FS = ""} NR> 5 {sub ("^ [0-9] * (. [0-9] *)?", ""); stampa} '| xargs -0 rm -rf
alex

1
A colpo d'occhio, sono sorpreso dalla complessità (o, del resto, necessità) della awklogica. Mi mancano alcuni requisiti all'interno della domanda del PO che lo rendono necessario?
Charles Duffy,

@Charles Duffy: il sub () rimuove il timestamp, che è ciò su cui è ordinato. Il timestamp prodotto da "% T @" può includere una parte di frazione. La divisione nello spazio con FS interrompe i percorsi con spazi incorporati. Suppongo che rimuovere attraverso i primi lavori spaziali, ma sia quasi altrettanto difficile da leggere. I separatori RS e ORS non possono essere impostati sulla riga di comando, perché sono NUL.
wnoise,

1
@wnoise, il mio approccio abituale a questo è di eseguire il pipe in un while read -r -d ' '; IFS= -r -d ''; do ...loop shell - la prima lettura termina nello spazio, mentre la seconda passa al NUL.
Charles Duffy,

@Charles Duffy: sono sempre diffidente nei confronti del guscio grezzo, forse a causa delle preoccupazioni di citazioni bizantine. Ora penso che GNU sed -z -e 's/[^ ]* //; 1,5d'sia il più chiaro. (o forse sed -n -z -e 's/[^ ]* //; 6,$p'.
wnoise,

14

Tutte queste risposte falliscono quando ci sono directory nella directory corrente. Ecco qualcosa che funziona:

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

Questo:

  1. funziona quando ci sono directory nella directory corrente

  2. tenta di rimuovere ogni file anche se non è stato possibile rimuovere quello precedente (a causa di autorizzazioni, ecc.)

  3. fallisce quando il numero di file nella directory corrente è eccessivo e xargsnormalmente ti rovinerebbe (il-x )

  4. non soddisfa gli spazi nei nomi dei file (forse stai usando il sistema operativo sbagliato?)


5
Cosa succede se findrestituisce più nomi di file di quanti possano essere passati su una singola riga di comando ls -t? (Suggerimento: si ottengono più esecuzioni di ls -t, ognuna delle quali è ordinata solo individualmente, anziché avere un ordinamento corretto a livello globale; pertanto, questa risposta è gravemente interrotta quando si esegue con directory sufficientemente grandi).
Charles Duffy,

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

Elenca i nomi dei file in base al tempo di modifica, citando ciascun nome file. Escludere i primi 3 (3 i più recenti). Rimuovi rimanenti.

MODIFICA dopo un utile commento di mklement0 (grazie!): Corretto -n + 3 argomento, e nota che questo non funzionerà come previsto se i nomi dei file contengono nuove righe e / o la directory contiene sottodirectory.


L' -Qopzione non sembra esistere sulla mia macchina.
Pierre-Adrien Buisson,

4
Hmm, l'opzione è stata nelle utility GNU core per ~ 20 anni, ma non è menzionata nelle varianti BSD. Sei su un mac?
Segna il

Lo sono davvero. Non pensavo che ci fossero differenze per questo tipo di comandi davvero basilari tra sistemi aggiornati. Grazie per la tua risposta !
Pierre-Adrien Buisson,

3
@Mark: ++ per -Q. Sì, -Qè un'estensione GNU (ecco le specifiche POSIXls ). Un piccolo avvertimento (raramente un problema in pratica): -Qcodifica le nuove righe incorporate nei nomi dei file come letterali \n, che rmnon riconosceranno. Per escludere i primi 3 , l' xargsargomento deve +4. Infine, un avvertimento che si applica anche alla maggior parte delle altre risposte: il tuo comando funzionerà come previsto se non ci sono sottodirectory nella directory corrente.
mklement0

1
Quando non c'è nulla da rimuovere, hai call xargs con --no-run-if-emptyopzione:ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Olivier Lecrivain

8

Ignorare le nuove righe significa ignorare la sicurezza e la buona codifica. wnoise ha avuto l'unica buona risposta. Ecco una sua variante che mette i nomi dei file in un array $ x

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

2
Suggerirei di cancellare IFS- altrimenti rischieresti di perdere spazi bianchi finali dai nomi dei file. Può limitarsi al comando read:while IFS= read -rd ''; do
Charles Duffy,

1
perchè "${REPLY#* }"?
msciwoj,

4

Se i nomi dei file non hanno spazi, funzionerà:

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

Se i nomi dei file hanno spazi, qualcosa del genere

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

Logica di base:

  • ottenere un elenco dei file in ordine temporale, una colonna
  • ottieni tutto tranne i primi 5 (n = 5 per questo esempio)
  • prima versione: invia quelli a rm
  • seconda versione: gen uno script che li rimuoverà correttamente

Non dimenticare il while readtrucco per gestire gli spazi: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
pizzicato il

1
@pinkeen, non abbastanza sicuro come indicato lì. while IFS= read -r dsarebbe un po 'meglio - -revita che i letterali backslash vengano consumati reade IFS=impedisce il taglio automatico degli spazi bianchi finali.
Charles Duffy,

4
A proposito, se uno è preoccupato per i nomi di file ostili, questo è un approccio estremamente pericoloso. Considera un file creato con touch $'hello \'$(rm -rf ~)\' world'; le virgolette letterali all'interno del nome file contrasterebbero le virgolette letterali che stai aggiungendo sed, determinando l'esecuzione del codice all'interno del nome file.
Charles Duffy,

1
(per essere chiari, "questo" sopra si riferiva al | shmodulo, che è quello con la vulnerabilità dell'iniezione di shell).
Charles Duffy,

2

Con zsh

Supponendo che non ti interessi delle directory attuali e non avrai più di 999 file (scegli un numero più grande se vuoi, o crea un ciclo while).

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

In *(.om[6,999]), i .file dei mezzi, l' oordinamento dei mezzi in alto, i mmezzi per data di modifica (mettere aper il tempo di accesso o cper la modifica dell'inode), [6,999]scelgono un intervallo di file, quindi non prima il 5.


Intrigante, ma per la mia vita non sono riuscito a far funzionare il qualificatore glob ( om) di ordinamento (qualsiasi ordinamento che ho provato non ha mostrato alcun effetto - né su OSX 10.11.2 (provato con zsh 5.0.8 e 5.1.1) , né su Ubuntu 14.04 (zsh 5.0.2)) - cosa mi sto perdendo? Per quanto riguarda l'endpoint gamma: non c'è bisogno di hard-code, è sufficiente utilizzare -1per indicare l'ultima voce e quindi includere tutti i file rimanenti: [6,-1].
mklement0

2

Mi rendo conto che questo è un vecchio thread, ma forse qualcuno ne trarrà beneficio. Questo comando troverà i file nella directory corrente:

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

Questo è un po 'più robusto di alcune delle risposte precedenti in quanto consente di limitare il dominio di ricerca ai file corrispondenti alle espressioni. Innanzitutto, trova i file corrispondenti alle condizioni che desideri. Stampa quei file con i timestamp accanto a loro.

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

Quindi, ordinali in base ai timestamp:

sort -r -z -n

Quindi, elimina i 4 file più recenti dall'elenco:

tail -n+5

Prendi la seconda colonna (il nome file, non il timestamp):

awk '{ print $2; }'

E poi avvolgi tutto in una dichiarazione for:

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

Questo potrebbe essere un comando più dettagliato, ma ho avuto molta più fortuna nel riuscire a indirizzare i file condizionali ed eseguire comandi più complessi contro di loro.


1

trovato interessante cmd in Sed-Onliners - Elimina le ultime 3 righe - è perfetto per un altro modo di scuoiare il gatto (okay no) ma idea:

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

Rimuove tutti tranne i 10 file più recenti (la maggior parte dei recenti)

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

Se meno di 10 file non viene rimosso alcun file e si avrà: testa di errore: conteggio righe illegale - 0

Per contare i file con bash


1

Avevo bisogno di una soluzione elegante per la busybox (router), tutte le soluzioni xargs o array erano inutili per me - nessun comando disponibile lì. find and mtime non è la risposta corretta in quanto stiamo parlando di 10 articoli e non necessariamente di 10 giorni. La risposta di Espo è stata la più breve, pulita e probabilmente la più inversa.

Errore con spazi e quando nessun file deve essere eliminato sono entrambi risolti semplicemente nel modo standard:

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

Versione un po 'più educativa: possiamo fare tutto se usiamo awk in modo diverso. Normalmente, uso questo metodo per passare (restituire) variabili da awk a sh. Mentre leggiamo tutto il tempo che non si può fare, mi permetto di dissentire: ecco il metodo.

Esempio di file .tar senza problemi relativi agli spazi nel nome file. Per provare, sostituire "rm" con "ls".

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

Spiegazione:

ls -td *.tarelenca tutti i file .tar ordinati per ora. Per applicare a tutti i file nella cartella corrente, rimuovere la parte "d * .tar"

awk 'NR>7... salta le prime 7 righe

print "rm \"" $0 "\"" costruisce una riga: rm "nome file"

eval lo esegue

Dal momento che stiamo usando rm, non vorrei usare il comando sopra in uno script! L'uso più saggio è:

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

Nel caso di usare il ls -tcomando non farà alcun danno su esempi sciocchi come: touch 'foo " bar'etouch 'hello * world' . Non che abbiamo mai creato file con tali nomi nella vita reale!

Nota a margine. Se volessimo passare una variabile a sh in questo modo, modificheremmo semplicemente la stampa (modulo semplice, nessuno spazio tollerato):

print "VarName="$1

per impostare la variabile VarNamesul valore di $1. È possibile creare più variabili in una volta sola. Questa VarNamediventa una normale variabile sh e successivamente può essere normalmente utilizzata in uno script o in una shell. Quindi, per creare variabili con awk e restituirle alla 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
xargssenza -0o come minimo -d $'\n'è inaffidabile; osserva come si comporta con un file con spazi o virgolette nel suo nome.
Charles Duffy,

0

L'ho trasformato in uno script bash shell. Utilizzo: keep NUM DIRdove NUM è il numero di file da conservare e DIR è la directory da cancellare.

#!/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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.