Pregunta

¿Existe una forma sencilla, en un entorno UNIX bastante estándar con bash, de ejecutar un comando para eliminar todos los archivos X excepto los más recientes de un directorio?

Para dar un ejemplo un poco más concreto, imagine un trabajo cron que escribe un archivo (por ejemplo, un archivo de registro o una copia de seguridad con tar) en un directorio cada hora.Me gustaría encontrar una forma de ejecutar otro trabajo cron que elimine los archivos más antiguos de ese directorio hasta que queden menos de, digamos, 5.

Y para que quede claro, solo hay un archivo presente y nunca debe eliminarse.

¿Fue útil?

Solución

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 rm directamente en una sustitución de comando sin comillas (rm `...`), existe un riesgo adicional de globalización involuntaria.
  • incapacidad para distinguir entre archivos y directorios (es decir, si directorios estuviera entre los 5 elementos del sistema de archivos modificados más recientemente, efectivamente conservarías menos de 5 archivos y aplicando rm a directorios fallará).

la respuesta de ruido aborda estos problemas, pero la solución es ÑU-específico (y bastante complejo).

Aquí hay un pragmático, Solución compatible con POSIX que viene solo con una advertencia:no puede manejar nombres de archivos con incrustados nuevas líneas - pero no considero que eso sea una preocupación del mundo real para la mayoría de la gente.

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

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

Lo anterior es ineficiente, porque xargs tiene que invocar rm una vez por cada Nombre del archivo.
Tu plataforma xargs puede permitirle resolver este problema:

Si usted tiene ÑU xargs, usar -d '\n', que hace xargs considere cada línea de entrada como un argumento separado, pero pase tantos argumentos como quepan en una línea de comando En seguida:

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

-r (--no-run-if-empty) asegura que rm no se invoca si no hay entrada.

Si usted tiene BSD xargs (incluso en OSX), puedes usar -0 manejar NUL-entrada separada, después de traducir primero las nuevas líneas a NUL (0x0) chars., que también pasa (normalmente) todos los nombres de archivos En seguida (también funcionará con GNU xargs):

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

Explicación:

  • ls -tp imprime los nombres de los elementos del sistema de archivos ordenados según la fecha de modificación, 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 del listado resultante, omitiendo (-v) líneas que tienen un final / (/$).
    • Advertencia:Desde un enlace simbólico que apunta a un directorio técnicamente no es en sí mismo un directorio, dichos enlaces simbólicos no ser excluido.
  • tail -n +6 se salta el primero 5 entradas en el listado, devolviendo de hecho todas pero los 5 archivos modificados más recientemente, si los hubiera.
    Tenga en cuenta que para excluir N archivos, N+1 debe ser pasado a tail -n +.
  • xargs -I {} rm -- {} (y sus variaciones) luego invoca en rm en todos estos archivos;si no hay ninguna coincidencia, xargs no hará nada.
    • xargs -I {} rm -- {} define marcador de posición {} que representa cada línea de entrada como un todo, entonces rm Luego se invoca una vez para cada línea de entrada, pero los nombres de archivos con espacios incrustados se manejan correctamente.
    • -- En todos los casos se garantiza que cualquier nombre de archivo que comience con - no se confunden con opciones por rm.

A variación sobre el problema original, en caso de que sea necesario procesar los archivos coincidentes individualmente o recogido 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

Otros consejos

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

rm `ls -t | awk 'NR>5'`
(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

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 reverso).

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.

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, GNU sort para -z, GNU awk para "\0" y GNU xargs para -0, pero maneja archivos con nuevas líneas o espacios incrustados.

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

Este:

  1. funciona cuando hay directorios en el directorio actual

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

  3. falla a prueba de fallos cuando la cantidad de archivos en el directorio actual es excesiva y xargs normalmente te arruinaría (el -x)

  4. no admite espacios en los nombres de archivos (¿quizás estás usando el sistema operativo incorrecto?)

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

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

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

Ignorar las nuevas líneas es ignorar la seguridad y la buena codificación.El ruido tenía la única buena respuesta.Aquí hay una variación suya que coloca los nombres de los archivos en una matriz $x

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

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

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

Si los nombres de archivos tienen espacios, algo 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 temporal, una columna
  • obtenga todos menos los primeros 5 (n=5 para este ejemplo)
  • primera versión:envíalos a rm
  • segunda versión:generar un script que los eliminará correctamente

con zsh

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

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

En *(.om[6,999]), el . significa archivos, el o significa ordenar, el m significa por fecha de modificación (poner a por tiempo de acceso o c para el cambio de inodo), el [6,999] elige un rango de archivos, por lo que no marca los 5 primero.

Me doy cuenta de que este es un hilo antiguo, pero tal vez alguien se beneficie de él.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 sólido que algunas de las respuestas anteriores, ya que permite limitar su dominio de búsqueda a archivos que coincidan con expresiones.Primero, busque archivos que coincidan con las condiciones que desee.Imprima esos archivos con las marcas de tiempo al lado.

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

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

sort -r -z -n

Luego, elimina 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 resume todo eso en una declaración for:

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

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

Encontré un cmd interesante en Sed-Onliners - Elimine las últimas 3 líneas - Lo encuentro perfecto para otra forma de despellejar 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

Elimina todos menos los 10 archivos más recientes (los más recientes)

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

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

Para contar archivos con bash

Necesitaba una solución elegante para el Busybox (enrutador), todos los xargs o soluciones de matriz eran inútiles para mí; ese comando no estaba disponible allí.find y 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 breve, clara y probablemente la más universal.

El error con espacios y cuando no se deben eliminar archivos se resuelven simplemente de la forma 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, discrepo:Aquí está el método.

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

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

Explicación:

ls -td *.tar enumera 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... se salta las primeras 7 líneas

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

eval lo ejecuta

Ya que estamos usando rm, ¡No usaría el comando anterior en un script!Un uso más inteligente es:

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

En el caso de utilizar ls -t El comando no hará ningún daño en ejemplos tan tontos como: touch 'foo " bar' y touch 'hello * world'.¡No es que alguna vez creemos archivos con esos 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, no se toleran espacios):

print "VarName="$1

para establecer la variable VarName al valor de $1.Se pueden crear múltiples variables de una sola vez.Este VarName se convierte en una variable sh normal y luego se puede usar normalmente en un script o shell.Entonces, para crear variables con awk y devolverlas al shell:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"
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

Convertí esto en un script de shell bash.Uso: keep NUM DIR donde NUM es la cantidad de archivos que se conservarán y DIR es el directorio que se borrará.

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

Ejecutando en Debian (supongamos que es lo mismo en otras distribuciones que obtengo:habitación:no se puede eliminar el directorio `..'

lo cual es bastante molesto..

De todos modos modifiqué lo anterior y también agregué grep al comando.En mi caso tengo 6 archivos de respaldo en un directorio, por ejemplo.file1.tar file2.tar file3.tar, etc. y quiero eliminar solo el archivo más antiguo (eliminar el primer archivo en mi caso)

El script que ejecuté para eliminar el archivo más antiguo fue:

ls -c1 -t | Archivo GREP | Awk 'nr> 5' | xargs rm

Esto (como arriba) elimina el primero de mis archivos, p.file1.tar esto también deja estar con file2 file3 file4 file5 y file6

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top