Supprimer tous les fichiers X sauf les plus récents dans bash

157

Existe-t-il un moyen simple, dans un environnement UNIX assez standard avec bash, d'exécuter une commande pour supprimer tous les fichiers X sauf les plus récents d'un répertoire?

Pour donner un peu plus d'exemple concret, imaginez une tâche cron écrivant un fichier (par exemple, un fichier journal ou une sauvegarde tarée) dans un répertoire toutes les heures. Je voudrais un moyen d'avoir un autre travail cron en cours d'exécution qui supprimerait les fichiers les plus anciens de ce répertoire jusqu'à ce qu'il y en ait moins, disons, 5.

Et juste pour être clair, il n'y a qu'un seul fichier présent, il ne devrait jamais être supprimé.

Matt Sheppard
la source

Réponses:

117

Les problèmes avec les réponses existantes:

  • incapacité à gérer les noms de fichiers avec des espaces incorporés ou des retours à la ligne.
    • dans le cas de solutions qui invoquent rmdirectement sur une commande substitution ( rm `...`) sans guillemets , il y a un risque supplémentaire de globbing involontaire.
  • l'incapacité de faire la distinction entre les fichiers et les répertoires (par exemple, si les répertoires se trouvaient parmi les 5 éléments du système de fichiers les plus récemment modifiés, vous conserveriez effectivement moins de 5 fichiers et l'application rmaux répertoires échouera).

La réponse de wnoise aborde ces problèmes, mais la solution est spécifique à GNU (et assez complexe).

Voici une solution pragmatique et conforme à POSIX qui ne comporte qu'une seule mise en garde : elle ne peut pas gérer les noms de fichiers avec des sauts de ligne intégrés - mais je ne considère pas cela comme une préoccupation du monde réel pour la plupart des gens.

Pour mémoire, voici l'explication des raisons pour lesquelles ce n'est généralement pas une bonne idée d'analyser la lssortie: http://mywiki.wooledge.org/ParsingLs

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

Ce qui précède est inefficace , car xargsdoit être invoqué rmune fois pour chaque nom de fichier.
Votre plateforme xargspeut vous permettre de résoudre ce problème:

Si vous avez GNU xargs , utilisez -d '\n', ce qui fait de xargschaque ligne d'entrée un argument distinct, tout en transmettant autant d'arguments que possible sur une ligne de commande à la fois :

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

-r( --no-run-if-empty) garantit que ce rmn'est pas appelé s'il n'y a pas d'entrée.

Si vous avez BSD xargs (y compris sur macOS ), vous pouvez utiliser -0pour gérer les NULentrées séparées, après avoir d'abord traduit les sauts de ligne en NUL( 0x0) chars., Qui transmet également (généralement) tous les noms de fichiers à la fois (fonctionnera également avec GNU xargs):

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

Explication:

  • ls -tpimprime les noms des éléments du système de fichiers triés selon leur date de modification, par ordre décroissant (les éléments les plus récemment modifiés en premier) ( -t), avec les répertoires imprimés avec une fin /pour les marquer comme tels ( -p).
  • grep -v '/$'puis élimine les répertoires de la liste résultante, en omettant ( -v) les lignes qui ont un fin /( /$).
    • Attention : comme un lien symbolique qui pointe vers un répertoire n'est techniquement pas lui-même un répertoire, de tels liens symboliques ne seront pas exclus.
  • tail -n +6ignore les 5 premières entrées de la liste, renvoyant en fait tous les fichiers modifiés sauf les 5 derniers, le cas échéant.
    Notez que pour exclure des Nfichiers, N+1doit être passé à tail -n +.
  • xargs -I {} rm -- {}(et ses variantes) invoque alors rmsur tous ces fichiers; s'il n'y a aucune correspondance, xargsne fera rien.
    • xargs -I {} rm -- {}définit un espace réservé {}qui représente chaque ligne d'entrée dans son ensemble , il rmest donc appelé une fois pour chaque ligne d'entrée, mais avec les noms de fichiers avec des espaces incorporés gérés correctement.
    • --dans tous les cas, garantit que les noms de fichiers commençant par -ne sont pas confondus avec les options de rm.

Une variante du problème d'origine, au cas où les fichiers correspondants doivent être traités individuellement ou collectés dans un tableau 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
mklement0
la source
2
Certainement mieux que la plupart des autres réponses ici, donc je suis heureux d'apporter mon soutien, même dans la mesure où je considère ignorer le cas de nouvelle ligne comme une chose à faire uniquement avec prudence.
Charles Duffy
2
Si vous ne le faites lspas dans le répertoire courant, les chemins d'accès aux fichiers contiendront «/», ce qui signifie que grep -v '/'cela ne correspondra à rien. Je crois que grep -v '/$'c'est ce que vous voulez exclure uniquement les répertoires.
waldol1
1
@ waldol1: Merci; J'ai mis à jour la réponse pour inclure votre suggestion, ce qui rend également la grepcommande plus claire sur le plan conceptuel. Notez, cependant, que le problème que vous décrivez n'aurait pas surgi avec un seul chemin de répertoire; par exemple, ls -p /private/varn'imprimerait que de simples noms de fichiers. Ce n'est que si vous passez plusieurs arguments de fichier (généralement via un glob) que vous verrez les chemins réels dans la sortie; par exemple, ls -p /private/var/*(et vous verriez également le contenu des sous-répertoires correspondants, sauf si vous l'avez également inclus -d).
mklement0
108

Supprimez tous les fichiers les plus récents d'un répertoire, sauf 5 (ou n'importe quel nombre).

rm `ls -t | awk 'NR>5'`
Espo
la source
2
J'avais besoin de cela pour ne considérer que mes fichiers d'archive. changer ls -tpourls -td *.bz2
James T Snell
3
Je l'ai utilisé pour les répertoires en le changeant en rm -rf ls -t | awk 'NR>1'(je ne voulais que le plus récent). Merci!
lohiaguitar91
11
ls -t | awk 'NR>5' | xargs rm -f si vous préférez les tubes et que vous devez supprimer l'erreur s'il n'y a rien à supprimer.
H2ONaCl
16
Concis et lisible, peut-être, mais dangereux à utiliser; si vous essayez de supprimer un fichier créé avec touch 'hello * world', cela supprimerait absolument tout dans le répertoire actuel .
Charles Duffy
1
Même si cela a été répondu en 2008, cela fonctionne comme un charme et exactement ce dont j'avais besoin pour simplement supprimer les anciennes sauvegardes d'un répertoire spécifique. Impressionnant.
Rens Tillmann
86
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm

Cette version prend en charge les noms avec des espaces:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
thelsdj
la source
20
Cette commande ne gérera pas correctement les fichiers avec des espaces dans les noms.
tylerl
5
(ls -t|head -n 5;ls)est un groupe de commandes . Il imprime deux fois les 5 fichiers les plus récents. sortmet ensemble des lignes identiques. uniq -usupprime les doublons, de sorte que tous les fichiers sauf les 5 plus récents restent. xargs rmappelle rmchacun d'eux.
Fabien
15
Cela supprime tous vos fichiers si vous en avez 5 ou moins! Ajouter --no-run-if-emptyà xargscomme (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rms'il vous plaît mettre à jour la réponse.
Gonfi den Tschal
3
Même celui qui "prend en charge les noms avec des espaces" est dangereux. Considérez un nom qui contient des guillemets littéraux: touch 'foo " bar'supprimera tout le reste de la commande.
Charles Duffy
2
... il est plus sûr à utiliser xargs -d $'\n'que d'injecter des guillemets dans votre contenu, bien que la délimitation NUL du flux d'entrée (qui nécessite d'utiliser autre chose que lsde vraiment bien faire) est l'option idéale.
Charles Duffy
59

Variante plus simple de la réponse de thelsdj:

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

ls -tr affiche tous les fichiers, le plus ancien en premier (-t le plus récent en premier, -r inverse).

head -n -5 affiche toutes les lignes sauf les 5 dernières (c'est-à-dire les 5 fichiers les plus récents).

xargs rm appelle rm pour chaque fichier sélectionné.

Fabien
la source
15
Besoin d'ajouter --no-run-if-empty à xargs pour qu'il n'échoue pas lorsqu'il y a moins de 5 fichiers.
Tom
ls -1tr | tête -n -5 | xargs rm <---------- vous devez ajouter un -1 au ls ou vous n'obtiendrez pas une sortie de liste pour que head fonctionne correctement contre
Al Joslin
3
@AlJoslin, -1est la valeur par défaut lorsque la sortie est vers un pipeline, donc ce n'est pas obligatoire ici. Cela pose des problèmes beaucoup plus importants, liés au comportement par défaut xargslors de l'analyse des noms avec des espaces, des guillemets, etc.
Charles Duffy
semble que le --no-run-if-emptyn'est pas reconnu dans ma coquille. J'utilise Cmder sur Windows.
StayFoolish
Il peut être nécessaire d'utiliser l' -0option si les noms de fichiers peuvent contenir des espaces. Je ne l'ai pas encore testé. source
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

Requiert GNU find pour -printf, et GNU sort pour -z, et GNU awk pour "\ 0", et GNU xargs pour -0, mais gère les fichiers avec des retours à la ligne ou des espaces intégrés.

wnoise
la source
2
Si vous voulez supprimer des répertoires, changez simplement le -f en -d et ajoutez un -r au rm. trouver . -maxdepth 1 -type d -printf '% T @% p \ 0' | trier -r -z -n | awk 'BEGIN {RS = "\ 0"; ORS = "\ 0"; FS = ""} NR> 5 {sub ("^ [0-9] * (. [0-9] *)?", ""); imprimer} '| xargs -0 rm -rf
alex
1
En un coup d'œil, je suis surpris de la complexité (ou, d'ailleurs, de la nécessité) de la awklogique. Est-ce que je manque certaines exigences dans la question du PO qui le rendent nécessaire?
Charles Duffy
@Charles Duffy: Le sub () supprime l'horodatage, qui est ce qui est trié. L'horodatage produit par "% T @" peut inclure une partie fractionnaire. Le fractionnement de l'espace avec FS rompt les chemins avec des espaces intégrés. Je suppose que le retrait du premier espace fonctionne, mais il est presque aussi difficile à lire. Les séparateurs RS et ORS ne peuvent pas être définis sur la ligne de commande, car ce sont des NUL.
wnoise
1
@wnoise, mon approche habituelle à ce sujet est de passer dans une while read -r -d ' '; IFS= -r -d ''; do ...boucle shell - la première lecture se termine sur l'espace, tandis que la seconde passe au NUL.
Charles Duffy
@Charles Duffy: Je me méfie toujours de la coquille brute, peut-être à cause de préoccupations byzantines. Je pense maintenant que GNU sed -z -e 's/[^ ]* //; 1,5d'est le plus clair. (ou peut-être sed -n -z -e 's/[^ ]* //; 6,$p'.
wnoise
14

Toutes ces réponses échouent lorsqu'il y a des répertoires dans le répertoire courant. Voici quelque chose qui fonctionne:

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

Ce:

  1. fonctionne quand il y a des répertoires dans le répertoire courant

  2. essaie de supprimer chaque fichier même si le précédent n'a pas pu être supprimé (en raison des autorisations, etc.)

  3. échoue en toute sécurité lorsque le nombre de fichiers dans le répertoire actuel est excessif et xargsvous foutrait normalement (le -x)

  4. ne prend pas en charge les espaces dans les noms de fichiers (peut-être que vous utilisez le mauvais système d'exploitation?)

Charles Wood
la source
5
Que se passe-t-il si findrenvoie plus de noms de fichiers que ce qui peut être passé sur une seule ligne de commande ls -t? (Astuce: vous obtenez plusieurs exécutions de ls -t, dont chacune n'est triée qu'individuellement, plutôt que d'avoir un ordre de tri globalement correct; ainsi, cette réponse est gravement cassée lors de l'exécution avec des répertoires suffisamment grands).
Charles Duffy
12
ls -tQ | tail -n+4 | xargs rm

Répertoriez les noms de fichiers par heure de modification, en citant chaque nom de fichier. Excluez les 3 premiers (3 les plus récents). Retirez le reste.

EDIT après un commentaire utile de mklement0 (merci!): Argument corrigé -n + 3, et notez que cela ne fonctionnera pas comme prévu si les noms de fichiers contiennent des nouvelles lignes et / ou le répertoire contient des sous-répertoires.

marque
la source
L' -Qoption ne semble pas exister sur ma machine.
Pierre-Adrien Buisson
4
Hmm, l'option est dans les utils du noyau GNU depuis ~ 20 ans, mais n'est pas mentionnée dans les variantes BSD. Êtes-vous sur un Mac?
Mark
Je suis en effet. Je ne pensais pas qu'il y avait des différences pour ce genre de commandes vraiment basiques entre les systèmes à jour. Merci pour votre réponse !
Pierre-Adrien Buisson le
3
@Mark: ++ pour -Q. Oui, -Qc'est une extension GNU (voici la spécification POSIXls ). Une petite mise en garde (rarement un problème en pratique): -Qencode les nouvelles lignes intégrées dans les noms de fichiers comme littéral \n, ce rmqui ne reconnaîtra pas. Pour exclure les 3 premiers , l' xargsargument doit +4. Enfin, une mise en garde qui s'applique à la plupart des autres réponses également: votre commande ne fonctionnera comme prévu que s'il n'y a pas de sous-répertoires dans le répertoire actuel.
mklement0
1
Quand il n'y a rien à supprimer, vous avez des call xargs avec --no-run-if-emptyoption:ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Olivier Lecrivain
8

Ignorer les nouvelles lignes, c'est ignorer la sécurité et un bon codage. wnoise avait la seule bonne réponse. Voici une variante de son qui met les noms de fichiers dans un tableau $ x

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Ian Kelling
la source
2
Je suggérerais d'effacer IFS- sinon vous risqueriez de perdre les espaces blancs de fin des noms de fichiers. Peut étendre cela à la commande de lecture:while IFS= read -rd ''; do
Charles Duffy
1
pourquoi "${REPLY#* }"?
msciwoj
4

Si les noms de fichiers n'ont pas d'espaces, cela fonctionnera:

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

Si les noms de fichiers ont des espaces, quelque chose comme

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

Logique de base:

  • obtenir une liste des fichiers par ordre chronologique, une colonne
  • obtenir tout sauf les 5 premiers (n = 5 pour cet exemple)
  • première version: envoyez-les à rm
  • deuxième version: générer un script qui les supprimera correctement
Mark Harrison
la source
N'oubliez pas l' while readastuce pour gérer les espaces: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
pinkeen
1
@pinkeen, pas tout à fait sûr comme indiqué ici. while IFS= read -r dserait un peu mieux - le -rempêche les littéraux de barre oblique inverse d'être consommés par read, et IFS=empêche le rognage automatique des espaces de fin.
Charles Duffy
4
BTW, si l'on s'inquiète des noms de fichiers hostiles, c'est une approche extrêmement dangereuse. Considérez un fichier créé avec touch $'hello \'$(rm -rf ~)\' world'; les guillemets littéraux à l'intérieur du nom de fichier contreraient les guillemets littéraux avec lesquels vous ajoutez sed, ce qui entraînerait l'exécution du code dans le nom de fichier.
Charles Duffy
1
(pour être clair, le "ceci" ci-dessus faisait référence au | shformulaire, qui est celui avec la vulnérabilité d'injection de shell).
Charles Duffy
2

Avec zsh

En supposant que vous ne vous souciez pas des répertoires actuels et que vous n'aurez pas plus de 999 fichiers (choisissez un plus grand nombre si vous le souhaitez, ou créez une boucle while).

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

Dans *(.om[6,999]), les .fichiers moyens, l' oordre de tri des moyens vers le haut, les mmoyens par date de modification (mis apour le temps d'accès ou cpour le changement d'inode), le [6,999]choisit une plage de fichiers, donc ne rm le 5 en premier.

lolesque
la source
Intrigant, mais pour la vie de moi, je n'ai pas pu faire fonctionner le qualificatif de tri global ( om) (tout tri que j'ai essayé n'a montré aucun effet - ni sur OSX 10.11.2 (essayé avec zsh 5.0.8 et 5.1.1) , ni sur Ubuntu 14.04 (zsh 5.0.2)) - que me manque-t-il?. En ce qui concerne le point final de la plage: pas besoin de coder en dur il, utilisez simplement de -1se référer à la dernière entrée et inclut donc tous les fichiers restants: [6,-1].
mklement0
2

Je me rends compte que c'est un vieux fil, mais peut-être que quelqu'un en profitera. Cette commande trouvera les fichiers dans le répertoire courant:

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

C'est un peu plus robuste que certaines des réponses précédentes car cela permet de limiter votre domaine de recherche aux fichiers correspondant aux expressions. Tout d'abord, trouvez les fichiers correspondant aux conditions souhaitées. Imprimez ces fichiers avec les horodatages à côté d'eux.

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

Ensuite, triez-les par horodatage:

sort -r -z -n

Ensuite, supprimez les 4 fichiers les plus récents de la liste:

tail -n+5

Prenez la 2ème colonne (le nom du fichier, pas l'horodatage):

awk '{ print $2; }'

Et puis enveloppez tout cela dans une déclaration for:

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

Cela peut être une commande plus verbeuse, mais j'ai eu beaucoup plus de chance de pouvoir cibler des fichiers conditionnels et d'exécuter des commandes plus complexes contre eux.

TopherGopher
la source
1

trouvé cmd intéressant dans Sed-Onliners - Supprimez les 3 dernières lignes - trouvez-le parfait pour une autre façon d'écorcher le chat (d'accord pas) mais idée:

 #!/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
Tim
la source
1

Supprime tous les fichiers sauf les 10 derniers (les plus récents)

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

Si moins de 10 fichiers aucun fichier n'est supprimé et vous aurez: tête d'erreur: nombre de lignes illégal - 0

Pour compter les fichiers avec bash

Fabrice
la source
1

J'avais besoin d'une solution élégante pour le busybox (routeur), tous les xargs ou solutions de baie étaient inutiles pour moi - aucune commande de ce type n'est disponible là-bas. find and mtime n'est pas la bonne réponse car nous parlons de 10 éléments et pas nécessairement de 10 jours. La réponse d'Espo était la plus courte et la plus propre et probablement la plus universelle.

Les erreurs avec les espaces et lorsqu'aucun fichier ne doit être supprimé sont simplement résolus de la manière standard:

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

Version un peu plus éducative: nous pouvons tout faire si nous utilisons awk différemment. Normalement, j'utilise cette méthode pour passer (renvoyer) des variables de awk au sh. Comme nous lisons tout le temps ce qui ne peut être fait, je prie de différer: voici la méthode.

Exemple pour les fichiers .tar sans problème concernant les espaces dans le nom de fichier. Pour tester, remplacez "rm" par "ls".

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

Explication:

ls -td *.tarrépertorie tous les fichiers .tar triés par heure. Pour appliquer à tous les fichiers du dossier courant, supprimez la partie "d * .tar"

awk 'NR>7... saute les 7 premières lignes

print "rm \"" $0 "\"" construit une ligne: rm "nom de fichier"

eval l'exécute

Puisque nous utilisons rm, je n'utiliserais pas la commande ci-dessus dans un script! Une utilisation plus sage est:

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

Dans le cas de l'utilisation de la ls -tcommande ne fera aucun mal sur des exemples aussi stupides que: touch 'foo " bar'ettouch 'hello * world' . Non pas que nous ayons jamais créé des fichiers avec de tels noms dans la vraie vie!

Sidenote. Si nous voulions passer une variable au sh de cette façon, nous modifierions simplement l'impression (forme simple, aucun espace toléré):

print "VarName="$1

pour définir la variable VarNamesur la valeur de $1. Plusieurs variables peuvent être créées en une seule fois. Cela VarNamedevient une variable sh normale et peut être normalement utilisée dans un script ou un shell par la suite. Donc, pour créer des variables avec awk et les rendre au shell:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"
Pila
la source
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
Pavel Tankov
la source
2
xargssans -0ou au strict minimum -d $'\n'n'est pas fiable; observez comment cela se comporte avec un fichier avec des espaces ou des guillemets dans son nom.
Charles Duffy
0

J'en ai fait un script shell bash. Utilisation: keep NUM DIRoù NUM est le nombre de fichiers à conserver et DIR est le répertoire à nettoyer.

#!/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
Jonc
la source