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

C’è un modo semplice, in un ambiente UNIX piuttosto standard con bash, di eseguire un comando per eliminare tutti gli ultimi file X da una directory?

Per dare un esempio un po ‘più concreto, immagina qualche cron job che scrive un file (ad esempio, un file di registro o un backup tar tarato) in una directory ogni ora. Mi piacerebbe avere un altro processo cron in esecuzione che rimuova i file più vecchi in quella directory finché non ci sono meno di, per esempio, 5.

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

I problemi con le risposte esistenti:

  • incapacità di gestire nomi di file con spazi incorporati o newline.
    • nel caso di soluzioni che invocano rm direttamente su una sostituzione di comando non quotata ( rm `...` ), c’è un ulteriore rischio di globbing non intenzionale.
  • incapacità di distinguere tra file e directory (ad esempio, se le directory si trovassero tra le 5 voci del file system modificate più di recente, si conserverebbero effettivamente meno di 5 file e l’applicazione di rm alle directory fallirà).

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

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

Per la cronaca, ecco la spiegazione del perché in genere non è una buona idea analizzare l’output di ls : http://mywiki.wooledge.org/ParsingLs

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

Quanto sopra è inefficiente , perché xargs deve invocare rm una volta per ogni nome di file.
Gli xargs della tua piattaforma ti consentono di risolvere questo problema:

Se hai GNU xargs , usa -d '\n' , il che rende xargs considera ogni riga di input un argomento separato, ma passa tutti gli argomenti che si adatteranno 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 rm non venga invocato se non ci sono input.

Se hai BSD xargs (incluso su OS X ), puoi usare -0 per gestire l’input NUL separato, dopo aver prima convertito le newline in NUL ( 0x0 ) caratteri, che passa anche (tipicamente) 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 -tp stampa i nomi degli elementi del filesystem ordinati secondo la loro recente modifica, in ordine decrescente (gli ultimi elementi modificati per primi) ( -t ), con le directory stampate con un trailing / per contrassegnarli come tali ( -p ).
  • grep -v '/$' elimina quindi le directory dall’elenco risultante, omettendo le righe ( -v ) che hanno un trailing / ( /$ ).
    • Avvertenza : poiché un collegamento simbolico che punta a una directory tecnicamente non è di per sé una directory, tali collegamenti simbolici non saranno esclusi.
  • tail -n +6 salta le prime 5 voci nell’elenco, in effetti restituisce tutti i 5 file modificati più di recente, se presenti.
    Nota che per escludere N file, N+1 deve essere passato a tail -n + .
  • xargs -I {} rm -- {} (e le sue varianti) quindi invoca su rm su tutti questi file; se non ci sono corrispondenze, xargs non farà nulla.
    • xargs -I {} rm -- {} definisce segnaposto {} che rappresenta ogni riga di input nel suo complesso , quindi rm viene quindi richiamato una volta per ogni riga di input, ma con nomi di file con spazi incorporati gestiti correttamente.
    • -- in tutti i casi assicura che tutti i nomi di file che iniziano con - non siano scambiati per opzioni da rm .

Una variazione sul problema originale, nel caso in cui i file corrispondenti debbano essere elaborati individualmente 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 -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 

Questa versione supporta nomi con spazi:

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

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

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

Variante più semplice della risposta di thelsdj:

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

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

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

xargs rm chiama rm per ogni file selezionato.

 find . -maxdepth 1 -type f -printf '%[email protected] %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 la ricerca GNU per -printf, e GNU sort per -z, e GNU awk per “\ 0”, e GNU xargs per -0, ma gestisce i file con nuove linee o spazi incorporati.

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. prova a rimuovere ogni file anche se non è ansible rimuovere quello precedente (a causa di permessi, ecc.)

  3. non è sicuro quando il numero di file nella directory corrente è eccessivo e gli xargs ti xargs normalmente (il -x )

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

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

Elenca i nomi dei file per tempo di modifica, citando ogni nome di file. Escludi i primi 3 (3 più recenti). Rimuovi rimanenti.

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

Ignorare i newline sta ignorando la sicurezza e una buona codifica. il wnoise ha avuto l’unica buona risposta. Ecco una sua variazione che mette i nomi dei file in una matrice $ x

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

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

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

Se i nomi dei file hanno spazi, qualcosa di simile

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

Logica di base:

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

Con zsh

Supponendo che non ti interessino le directory presenti 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]) , il . significa file, il o significa ordinare in ordine, il m significa per data di modifica (metti a per tempo di accesso o c per cambio inode), il [6,999] sceglie un intervallo di file, quindi non rm il primo.

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

 for F in $(find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%[email protected] %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. In primo luogo, 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 '%[email protected] %p\n' 

Quindi, ordinali in base al timestamp:

 sort -r -z -n 

Quindi, elimina i 4 file più recenti dalla lista:

 tail -n+5 

Prendi la 2a colonna (il nome del file, non il timestamp):

 awk '{ print $2; }' 

E poi avvolgere l’intera cosa in una dichiarazione for:

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

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

trovato interessante cmd in Sed-Onliner – 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 
 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 

Ho fatto questo in uno script di shell bash. Utilizzo: keep NUM DIR dove NUM è il numero di file da conservare e DIR è la directory da scrub.

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

Rimuove tutti tranne i 10 ultimi (più recenti) file

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

Se meno di 10 file non vengono rimossi, avrai: test d’errore: numero di righe illegali – 0

Per contare i file con bash

In esecuzione su Debian (assumendo lo stesso su altre distro ho: rm: imansible rimuovere la directory `.. ‘

che è abbastanza fastidioso ..

Comunque ho ottimizzato quanto sopra e ho aggiunto anche grep al comando. Nel mio caso ho 6 file di backup in una directory es. File1.tar file2.tar file3.tar ecc. E voglio cancellare solo il file più vecchio (rimuovi il primo file nel mio caso)

Lo script che ho eseguito per eliminare il file più vecchio era:

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

Questo (come sopra) cancella il primo dei miei file, ad es. File1.tar questo lascia anche essere con file2 file3 file4 file5 e file6