Eliminar todos los archivos X más recientes en bash

157

¿Hay una manera simple, en un entorno bastante estándar de UNIX con bash, de ejecutar un comando para eliminar todos los archivos X más recientes de un directorio?

Para dar un poco más de un ejemplo concreto, imagine un trabajo cron escribiendo un archivo (por ejemplo, un archivo de registro o una copia de seguridad protegida) en un directorio cada hora. Me gustaría una forma de ejecutar otro trabajo cron que eliminaría los archivos más antiguos de ese directorio hasta que haya menos de, por ejemplo, 5.

Y para ser claros, solo hay un archivo presente, nunca se debe eliminar.

Matt Sheppard
fuente

Respuestas:

117

Los problemas con las respuestas existentes:

  • incapacidad para manejar nombres de archivos con espacios incrustados o nuevas líneas.
    • en el caso de soluciones que invocan rmdirectamente en una sustitución de comando sin comillas ( rm `...`), existe un riesgo adicional de bloqueo involuntario.
  • incapacidad para distinguir entre archivos y directorios (es decir, si los directorios se encontraran entre los 5 elementos del sistema de archivos modificados más recientemente, efectivamente retendría menos de 5 archivos, y la aplicación rma los directorios fallará).

La respuesta de wnoise aborda estos problemas, pero la solución es específica de GNU (y bastante compleja).

Aquí hay una solución pragmática y compatible con POSIX que viene con una sola advertencia : no puede manejar nombres de archivo con líneas nuevas incrustadas , pero no considero que sea una preocupación del mundo real para la mayoría de las personas.

Para el registro, aquí está la explicación de por qué generalmente no es una buena idea analizar la lssalida: http://mywiki.wooledge.org/ParsingLs

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

Lo anterior es ineficiente , porque xargstiene que invocar rmuna vez para cada nombre de archivo.
Su plataforma xargspuede permitirle resolver este problema:

Si tiene GNU xargs , use -d '\n', lo que hace que xargscada línea de entrada sea un argumento separado, pero pasa tantos argumentos como quepan en una línea de comando a la vez :

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

-r( --no-run-if-empty) asegura que rmno se invoque si no hay entrada.

Si tiene BSD xargs (incluido en macOS ), puede usarlo -0para manejar la NULentrada separada, después de traducir primero las nuevas líneas a NUL( 0x0) caracteres, que también pasa (típicamente) todos los nombres de archivo a la vez (también funcionará con GNU xargs):

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

Explicación:

  • ls -tpimprime los nombres de los elementos del sistema de archivos ordenados por la última vez que se modificaron, en orden descendente (los elementos modificados más recientemente primero) ( -t), con directorios impresos con un final /para marcarlos como tales ( -p).
  • grep -v '/$'luego elimina los directorios de la lista resultante, omitiendo las -vlíneas ( ) que tienen un final /( /$).
    • Advertencia : dado que un enlace simbólico que apunta a un directorio técnicamente no es en sí un directorio, dichos enlaces simbólicos no se excluirán.
  • tail -n +6se salta las primeras 5 entradas en el perfil, en efecto, volviendo todo , pero los 5 archivos más recientemente modificados, en su caso.
    Tenga en cuenta que para excluir Narchivos, N+1debe pasarse a tail -n +.
  • xargs -I {} rm -- {}(y sus variaciones) luego se invoca rmen todos estos archivos; Si no hay coincidencias, xargsno hará nada.
    • xargs -I {} rm -- {}define el marcador de posición {}que representa cada línea de entrada como un todo , por lo que rmse invoca una vez para cada línea de entrada, pero con nombres de archivo con espacios incrustados manejados correctamente.
    • --en todos los casos asegura que cualquier nombre de archivo con el que empiece -no se confunda con las opciones de rm.

Una variación del problema original, en caso de que los archivos coincidentes deban procesarse individualmente o recopilarse en una matriz de 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
fuente
2
Ciertamente mejor que la mayoría de las otras respuestas aquí, así que estoy feliz de prestar mi apoyo, incluso en la medida en que considero que ignorar el caso de la nueva línea es algo que se debe hacer con precaución.
Charles Duffy
2
Si no lsestá en el directorio actual, las rutas a los archivos contendrán '/', lo que significa que grep -v '/'no coincidirán con nada. Creo que grep -v '/$'es lo que desea excluir solo directorios.
waldol1
1
@ waldol1: Gracias; He actualizado la respuesta para incluir su sugerencia, lo que también hace que el grepcomando sea conceptualmente más claro. Sin embargo, tenga en cuenta que el problema que describe no habría surgido con una sola ruta de directorio; por ejemplo, ls -p /private/varsolo imprimiría simples nombres de archivo. Solo si pasa varios argumentos de archivo (generalmente a través de un globo) vería rutas reales en la salida; por ejemplo, ls -p /private/var/*(y también verá el contenido de los subdirectorios coincidentes, a menos que también lo haya incluido -d).
mklement0
108

Elimine todos menos 5 (o cualquier número) de los archivos más recientes en un directorio.

rm `ls -t | awk 'NR>5'`
Espo
fuente
2
Necesitaba esto para considerar solo mis archivos. cambiar ls -tals -td *.bz2
James T Snell
3
Usé esto para directorios cambiándolo a rm -rf ls -t | awk 'NR>1'(solo quería el más reciente). ¡Gracias!
lohiaguitar91
11
ls -t | awk 'NR>5' | xargs rm -f si prefiere tuberías y necesita suprimir el error si no hay nada que eliminar.
H2ONaCl
16
Conciso y legible, tal vez, pero peligroso de usar; si intenta eliminar un archivo creado con touch 'hello * world', esto eliminaría absolutamente todo en el directorio actual .
Charles Duffy
1
Aunque esto fue respondido en 2008, funciona de maravilla y justo lo que necesitaba para simplemente eliminar las copias de seguridad antiguas de un directorio específico. Increíble.
Rens Tillmann
86
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm

Esta versión admite nombres con espacios:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
thelsdj
fuente
20
Este comando no manejará correctamente los archivos con espacios en los nombres.
tylerl
55
(ls -t|head -n 5;ls)es un grupo de comandos . Imprime los 5 archivos más recientes dos veces. sortpone líneas idénticas juntas. uniq -uelimina los duplicados, de modo que quedan todos menos los 5 archivos más recientes. xargs rmllama rma cada uno de ellos.
Fabien
15
¡Esto elimina todos tus archivos si tienes 5 o menos! Agregar --no-run-if-emptya xargscomo en (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rmpor favor actualice la respuesta.
Gonfi den Tschal
3
Incluso el que "admite nombres con espacios" es peligroso. Considere un nombre que contenga comillas literales: touch 'foo " bar'eliminará todo el resto del comando.
Charles Duffy
2
... es más seguro de usar xargs -d $'\n'que a cotizaciones inyectar en su contenido, aunque NUL-delimita el flujo de entrada (que requiere el uso de algo que no sea lsa realmente hacer a la derecha) es la opción ideal.
Charles Duffy
59

Variante más simple de la respuesta de thelsdj:

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

ls -tr muestra todos los archivos, el más antiguo primero (-t el más nuevo primero, -r al revés).

head -n -5 muestra todas menos las 5 últimas líneas (es decir, los 5 archivos más nuevos).

xargs rm llama a rm para cada archivo seleccionado.

Fabien
fuente
15
Es necesario agregar --no-run-if-empty a xargs para que no falle cuando hay menos de 5 archivos.
Tom
ls -1tr | cabeza -n -5 | xargs rm <---------- necesita agregar un -1 al ls o no obtendrá una salida de la lista para que head funcione correctamente
Al Joslin
3
@AlJoslin, -1es predeterminado cuando la salida es a una tubería, por lo que no es obligatorio aquí. Esto tiene problemas mucho mayores, relacionados con el comportamiento predeterminado xargsal analizar nombres con espacios, comillas, etc.
Charles Duffy el
Parece que --no-run-if-emptyno se reconoce en mi shell. Estoy usando Cmder en Windows.
StayFoolish
Es posible que deba usar la -0opción si los nombres de archivo pueden contener espacios en blanco. Sin embargo, aún no lo he probado. fuente
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

Requiere GNU find para -printf, y GNU sort para -z, y GNU awk para "\ 0", y GNU xargs para -0, pero maneja archivos con líneas o espacios incrustados.

wnoise
fuente
2
Si desea eliminar directorios, simplemente cambie -f a -d y agregue -r al rm. encontrar . -maxdepth 1 -tipo d -printf '% T @% p \ 0' | ordenar -r -z -n | awk 'BEGIN {RS = "\ 0"; ORS = "\ 0"; FS = ""} NR> 5 {sub ("^ [0-9] * (. [0-9] *)?", ""); print} '| xargs -0 rm -rf
alex
1
De un vistazo, me sorprende la complejidad (o, para el caso, la necesidad) de la awklógica. ¿Me faltan algunos requisitos dentro de la pregunta del OP que lo hacen necesario?
Charles Duffy
@Charles Duffy: el sub () elimina la marca de tiempo, que es lo que está ordenado. La marca de tiempo producida por "% T @" puede incluir una fracción. Dividir en el espacio con FS rompe caminos con espacios incrustados. Supongo que eliminar a través del primer espacio funciona, pero es casi tan difícil de leer. Los separadores RS y ORS no se pueden establecer en la línea de comando, porque son NUL.
wnoise
1
@wnoise, mi enfoque habitual es conectarme a un while read -r -d ' '; IFS= -r -d ''; do ...bucle de shell : la primera lectura termina en el espacio, mientras que la segunda pasa al NUL.
Charles Duffy el
@Charles Duffy: Siempre desconfío de la cáscara cruda, tal vez debido a preocupaciones de citas bizantinas. Ahora creo que GNU sed -z -e 's/[^ ]* //; 1,5d'es el más claro. (o tal vez sed -n -z -e 's/[^ ]* //; 6,$p'.
wnoise
14

Todas estas respuestas fallan cuando hay directorios en el directorio actual. Aquí hay algo que funciona:

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

Esta:

  1. funciona cuando hay directorios en el directorio actual

  2. intenta eliminar cada archivo incluso si no se pudo eliminar el anterior (debido a permisos, etc.)

  3. falla a salvo cuando el número de archivos en el directorio actual es excesivo y xargsnormalmente lo atornillaría (el -x)

  4. no atiende espacios en los nombres de archivo (¿tal vez está utilizando el sistema operativo incorrecto?)

Charles Wood
fuente
55
¿Qué sucede si finddevuelve más nombres de archivos de los que se pueden pasar en una sola línea de comando ls -t? (Sugerencia: obtienes varias ejecuciones ls -t, cada una de las cuales está ordenada individualmente, en lugar de tener un orden de clasificación globalmente correcto; por lo tanto, esta respuesta se rompe mal cuando se ejecuta con directorios suficientemente grandes).
Charles Duffy el
12
ls -tQ | tail -n+4 | xargs rm

Enumere los nombres de archivo por hora de modificación, citando cada nombre de archivo. Excluir los primeros 3 (3 más recientes). Eliminar el resto.

EDITE después de un comentario útil de mklement0 (¡gracias!): Corregido el argumento -n + 3, y tenga en cuenta que esto no funcionará como se esperaba si los nombres de archivo contienen nuevas líneas y / o el directorio contiene subdirectorios.

marca
fuente
La -Qopción no parece existir en mi máquina.
Pierre-Adrien Buisson
44
Hmm, la opción ha estado en las utilidades principales de GNU durante ~ 20 años, pero no se menciona en las variantes BSD. ¿Estás en una Mac?
Mark
Estoy en ello. No pensé que hubiera diferencias para este tipo de comandos realmente básicos entre los sistemas actualizados. Gracias por tu respuesta !
Pierre-Adrien Buisson
3
@ Mark: ++ para -Q. Sí, -Qes una extensión GNU (aquí está la especificación POSIXls ). Una pequeña advertencia (rara vez es un problema en la práctica): -Qcodifica nuevas líneas incrustadas en nombres de archivos como literales \n, lo rmque no reconocerá. Para excluir los primeros 3 , el xargsargumento debe +4. Finalmente, una advertencia que también se aplica a la mayoría de las otras respuestas: su comando solo funcionará según lo previsto si no hay subdirectorios en el directorio actual.
mklement0
1
Cuando no hay nada que eliminar, tiene llamadas xargs con la --no-run-if-emptyopción:ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Olivier Lecrivain
8

Ignorar las nuevas líneas es ignorar la seguridad y la buena codificación. wnoise tuvo la única buena respuesta. Aquí hay una variación de él que pone los nombres de archivo en una matriz $ x

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Ian Kelling
fuente
2
Sugeriría borrar IFS, de lo contrario correría el riesgo de perder el espacio en blanco al final de los nombres de archivo. Puede abarcar eso con el comando de lectura:while IFS= read -rd ''; do
Charles Duffy
1
¿Por qué "${REPLY#* }"?
msciwoj
4

Si los nombres de archivo no tienen espacios, esto funcionará:

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

Si los nombres de archivo tienen espacios, algo así como

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

Lógica básica:

  • obtener una lista de los archivos en orden de tiempo, una columna
  • obtener todos menos los primeros 5 (n = 5 para este ejemplo)
  • primera versión: envíelas a rm
  • segunda versión: gen un script que los eliminará correctamente
Mark Harrison
fuente
No olvides el while readtruco para lidiar con espacios: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
pinkeen
1
@pinkeen, no es tan seguro como se da allí. while IFS= read -r dsería un poco mejor: -revita que los literales de barra invertida sean consumidos ready IFS=evita el recorte automático del espacio en blanco al final.
Charles Duffy
44
Por cierto, si uno está preocupado por los nombres de archivo hostiles, este es un enfoque extremadamente peligroso. Considere un archivo creado con touch $'hello \'$(rm -rf ~)\' world'; las comillas literales dentro del nombre del archivo contrarrestarían las comillas literales con las que está agregando sed, dando como resultado que se ejecute el código dentro del nombre del archivo.
Charles Duffy el
1
(para ser claros, el "esto" anterior se refería al | shformulario, que es el que tiene la vulnerabilidad de inyección de shell).
Charles Duffy el
2

Con zsh

Asumiendo que no le interesan los directorios actuales y que no tendrá más de 999 archivos (elija un número mayor si lo desea, o cree un ciclo while).

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

En *(.om[6,999])los .archivos de medios, el oorden de clasificación de los medios, los mmedios por fecha de modificación (puesto apara el tiempo de acceso o cpara el cambio de inodo), [6,999]elige un rango de archivo, por lo que no rm los 5 primero.

lolesque
fuente
Intrigante, pero por mi vida no pude hacer funcionar el calificador global de clasificación ( om) (cualquier clasificación que he intentado no mostró ningún efecto, ni en OSX 10.11.2 (probado con zsh 5.0.8 y 5.1.1) , ni en Ubuntu 14.04 (zsh 5.0.2)): ¿qué me estoy perdiendo? En cuanto a la gama de punto final: no hay necesidad de codificar, basta con utilizar -1para referirse a la última entrada y por lo tanto incluye todos los archivos restantes: [6,-1].
mklement0
2

Me doy cuenta de que este es un hilo viejo, pero tal vez alguien se beneficie de esto. Este comando encontrará archivos en el directorio actual:

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

Esto es un poco más robusto que algunas de las respuestas anteriores, ya que permite limitar su dominio de búsqueda a archivos que coincidan con expresiones. Primero, encuentre archivos que coincidan con las condiciones que desee. Imprima esos archivos con las marcas de tiempo junto a ellos.

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

A continuación, ordénelos por las marcas de tiempo:

sort -r -z -n

Luego, elimine los 4 archivos más recientes de la lista:

tail -n+5

Tome la segunda columna (el nombre del archivo, no la marca de tiempo):

awk '{ print $2; }'

Y luego envuelva todo en una declaración for:

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

Este puede ser un comando más detallado, pero tuve mucha mejor suerte al poder dirigir archivos condicionales y ejecutar comandos más complejos contra ellos.

TopherGopher
fuente
1

encontró cmd interesante en Sed-Onliners - Elimina las últimas 3 líneas - encuentra perfecto para otra forma de pelar al gato (está bien, no) pero 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
tim
fuente
1

Elimina todos menos los 10 últimos archivos (la mayoría recientes)

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

Si hay menos de 10 archivos, no se eliminará ningún archivo y tendrá: cabeza de error: recuento de líneas ilegales - 0

Para contar archivos con bash

fabrice
fuente
1

Necesitaba una solución elegante para el busybox (enrutador), todas las soluciones de xargs o array me resultaron inútiles; no hay tal comando disponible allí. find and mtime no es la respuesta adecuada, ya que estamos hablando de 10 elementos y no necesariamente de 10 días. La respuesta de Espo fue la más corta, más limpia y probablemente la más irreversible.

Los errores con espacios y cuando no se van a eliminar archivos se resuelven simplemente de la manera estándar:

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

Versión un poco más educativa: podemos hacerlo todo si usamos awk de manera diferente. Normalmente, uso este método para pasar (devolver) variables del awk al sh. Como leemos todo el tiempo que no se puede hacer, ruego diferir: aquí está el método.

Ejemplo para archivos .tar sin problemas con respecto a los espacios en el nombre del archivo. Para probar, reemplace "rm" con el "ls".

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

Explicación:

ls -td *.tarenumera todos los archivos .tar ordenados por hora. Para aplicar a todos los archivos en la carpeta actual, elimine la parte "d * .tar"

awk 'NR>7... salta las primeras 7 líneas

print "rm \"" $0 "\"" construye una línea: rm "nombre de archivo"

eval lo ejecuta

Como estamos usando rm, ¡no usaría el comando anterior en un script! El uso más sabio es:

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

En el caso de usar el ls -tcomando no hará ningún daño en ejemplos tan tontos como: touch 'foo " bar'ytouch 'hello * world' . ¡No es que alguna vez creamos archivos con tales nombres en la vida real!

Nota al margen. Si quisiéramos pasar una variable al sh de esta manera, simplemente modificaríamos la impresión (forma simple, sin espacios tolerados):

print "VarName="$1

para establecer la variable VarNameal valor de $1. Se pueden crear múltiples variables de una vez. Esto se VarNameconvierte en una variable sh normal y se puede usar normalmente en un script o shell después. Entonces, para crear variables con awk y devolverlas al shell:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"
Pila
fuente
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
fuente
2
xargssin -0o como mínimo -d $'\n'es poco confiable; observe cómo se comporta esto con un archivo con espacios o comillas en su nombre.
Charles Duffy el
0

Hice esto en un script de shell bash. Uso: keep NUM DIRdonde NUM es el número de archivos que se deben mantener y DIR es el directorio que se desea eliminar.

#!/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
Espadaña
fuente