¿Script de shell para mover archivos más antiguos?

14

¿Cómo escribo un script para mover solo los 20 archivos más antiguos de una carpeta a otra? ¿Hay alguna manera de tomar los archivos más antiguos en una carpeta?

usuario11598
fuente
¿Incluyendo o excluyendo subdirectorios? ¿Y debe hacerse recursivamente (en un árbol de directorios)?
maxschlepzig
2
Muchos (¿la mayoría?) * Sistemas de archivos nix no almacenan la fecha de creación, por lo que no puede determinar con certeza el archivo más antiguo . Los atributos típicamente disponibles son atime(último acceso), ctime(último cambio de permiso) y mtime(última modificación) ... por ejemplo. ls -ty del hallazgo printf "%T" uso mtime... Parece, de acuerdo con este enlace , que mis ext4particiones son capaces de manejar una fecha de creación, pero lse findy statno tienen las opciones apropiadas (todavía) ...
Peter.O

Respuestas:

13

Analizar la salida de nols es confiable .

En su lugar, utilice findpara localizar los archivos y sortordenarlos por marca de tiempo. Por ejemplo:

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    # do something with $file here
done < <(find . -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

¿Qué está haciendo todo esto?

Primero, los findcomandos localizan todos los archivos y directorios en el directorio actual ( .), pero no en subdirectorios del directorio actual ( -maxdepth 1), luego imprime:

  • Una marca de tiempo
  • Un espacio
  • La ruta relativa al archivo
  • Un personaje NULL

La marca de tiempo es importante. El %T@especificador de formato -printfse desglosa en T, que indica "Tiempo de última modificación" del archivo (mtime) y @, que indica "Segundos desde 1970", incluidos los segundos fraccionarios.

El espacio es simplemente un delimitador arbitrario. La ruta completa al archivo es para que podamos consultarlo más tarde, y el carácter NULL es un terminador porque es un carácter ilegal en un nombre de archivo y, por lo tanto, nos permite saber con seguridad que llegamos al final de la ruta al archivo.

Lo he incluido 2>/dev/nullpara que se excluyan los archivos a los que el usuario no tiene permiso de acceso, pero se suprimen los mensajes de error sobre su exclusión.

El resultado del findcomando es una lista de todos los directorios en el directorio actual. La lista se canaliza a la sortque se indica que:

  • -z Trate NULL como el carácter terminador de línea en lugar de nueva línea.
  • -n Ordenar numéricamente

Como los segundos desde 1970 siempre suben, queremos el archivo cuya marca de tiempo sea el número más pequeño. El primer resultado sortserá la línea que contiene la marca de tiempo numerada más pequeña. Todo lo que queda es extraer el nombre del archivo.

Los resultados de la find, sortla tubería se pasa a través de la sustitución de proceso a whiledonde se lee como si fuera un archivo en la entrada estándar. whilea su vez invoca readpara procesar la entrada.

En el contexto de readestablecemos la IFSvariable en nada, lo que significa que los espacios en blanco no se interpretarán inapropiadamente como un delimitador. readse cuenta -r, que desactiva la expansión de escape, y -d $'\0', lo que hace que el NULL delimitador de fin de línea, haciendo coincidir la salida de nuestra find, sorttubería.

La primera porción de datos, que representa la ruta de archivo más antigua precedida por su marca de tiempo y un espacio, se lee en la variable line. A continuación, la sustitución de parámetros se usa con la expresión #*, que simplemente reemplaza todos los caracteres desde el comienzo de la cadena hasta el primer espacio, incluido el espacio, sin nada. Esto elimina la marca de tiempo de modificación, dejando solo la ruta completa al archivo.

En este punto, el nombre del archivo está almacenado $filey puede hacer lo que quiera con él. Cuando termine de hacer algo con $filela whileinstrucción, readse ejecutará un bucle y el comando se ejecutará nuevamente, extrayendo el siguiente fragmento y el siguiente nombre de archivo.

¿No hay una manera más simple?

No. Las formas más simples son defectuosas.

Si usa ls -ty canaliza hacia heado tail(o cualquier cosa ), romperá los archivos con nuevas líneas en los nombres de archivo. Si mv $(anything)luego los archivos con espacios en blanco en el nombre causarán rotura. Si mv "$(anything)"luego los archivos con líneas nuevas en el nombre causarán roturas. Si readno -d $'\0', entonces usted va a romper en archivos con espacios en blanco en sus nombres.

Quizás en casos específicos usted sabe con certeza que una forma más simple es suficiente, pero nunca debe escribir suposiciones como esa en los scripts si puede evitar hacerlo.

Solución

#!/usr/bin/env bash

# move to the first argument
dest="$1"

# move from the second argument or .
source="${2-.}"

# move the file count in the third argument or 20
limit="${3-20}"

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    echo mv "$file" "$dest"
    let limit-=1
    [[ $limit -le 0 ]] && break
done < <(find "$source" -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

Llamar como:

move-oldest /mnt/backup/ /var/log/foo/ 20

Para mover los 20 archivos más antiguos de /var/log/foo/a /mnt/backup/.

Tenga en cuenta que estoy incluyendo archivos y directorios. Para archivos solo agregue -type fa la findinvocación.

Gracias

Gracias a enzotib y Павел Танков por las mejoras a esta respuesta.

Sorpigal
fuente
El tipo no debe usar -n. Al menos en mi versión, no ordena los números decimales correctamente. Debe eliminar el punto en la fecha o el uso -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz, las fechas ISO u otra cosa.
l0b0
@ l0b0: Esta limitación es conocida por mí. Supongo que es suficiente no requerir ese nivel de granularidad (es decir, ordenar más allá de lo que .debe ser irrelevante para usted). Sería más claro decirlo sort -z -n -t. -k1.
Sorpigal
@ l0b0: su solución exhibe el mismo error, independientemente: %TStambién muestra una "parte fraccional" que estaría en la forma 00.0000000000, por lo que también pierde granularidad. GNU reciente sortpodría resolver este problema mediante el uso -Vde un "tipo de versión", que manejará este tipo de coma flotante como se esperaba.
Sorpigal
No, porque hago una ordenación de cadena en "AAAA-MM-DDThh: mm: ss" en lugar de una ordenación numérica. El orden de cadena no se preocupa por los decimales, por lo que debería funcionar hasta el año 10000 :)
l0b0
@ l0b0: una ordenación de cadenas %T@también funcionaría, entonces, porque está rellenada con ceros.
Sorpigal
4

Es más fácil en zsh, donde puede usar el Om calificador global para ordenar las coincidencias por fecha (la más antigua primero) y el [1,20]calificador para retener solo las primeras 20 coincidencias:

mv -- *(Om[1,20]) target/

Agregue el Dcalificador si también desea incluir archivos de puntos. Agregue .si desea hacer coincidir solo archivos normales y no directorios.

Si no tiene zsh, aquí hay un Perl one-liner (puede hacerlo en menos de 80 caracteres, pero a un costo adicional en claridad):

perl -e '@files = sort {-M $b <=> -M $a} glob("*"); foreach (@files[0..1]) {rename $_, "target/$_" or die "$_: $!"}'

Con solo las herramientas POSIX o incluso bash o ksh, ordenar archivos por fecha es una molestia. Puede hacerlo fácilmente ls, pero analizar el resultado de lses problemático, por lo que esto solo funciona si los nombres de los archivos contienen solo caracteres imprimibles distintos de las nuevas líneas.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Gilles 'SO- deja de ser malvado'
fuente
4

Combinar ls -tsalida con tailo head.

Ejemplo simple, que funciona solo si todos los nombres de archivo contienen solo caracteres imprimibles que no sean espacios en blanco y \[*?:

 mv $(ls -1tr | head -20) other_folder
ktf
fuente
1
Agregue la opción -A a ls:ls -1Atr
Arcege
1
-1, peligroso. Aquí permítanme Craft un ejemplo: touch $'foo\n*'. ¿Qué sucede si ejecuta mv "$ (ls)" con ese archivo allí?
Sorpigal
1
@Sorpigal ¿En serio? Es un poco débil decir "Permítanme proponer un ejemplo que específicamente dijo que no funcionará. Oye, mira, no funciona"
Michael Mrozek
1
@Sorpigal No es una mala idea, funciona en el 99% de los casos. La respuesta es "si tienes archivos con nombres normales, esto funciona. Si eres una persona loca que incrusta nuevas líneas en sus nombres de archivo, no lo hará". Eso es completamente correcto
Michael Mrozek
1
@MichaelMrozek: Es una mala idea y es mala porque a veces falla. Si tiene la opción de hacer lo que falla a veces y lo que no, debe tomar la opción que no lo hace (y la que sí es mala). Haz lo que quieras interactivamente, pero en un archivo de script y cuando des consejos, hazlo correctamente.
Sorpigal
3

Puede usar GNU find para esto:

find -maxdepth 1 -type f -printf '%T@ %p\n' \
  | sort -k1,1 -g | head -20 | sed 's/^[0-9.]\+ //' \
  | xargs echo mv -t dest_dir

Donde find imprime el tiempo de modificación (en segundos desde 1970) y el nombre de cada archivo del directorio actual, la salida se ordena según el primer campo, los 20 más antiguos se filtran y se mueven dest_dir. Elimine el echosi ha probado la línea de comando.

maxschlepzig
fuente
2

Nadie ha publicado (todavía) un ejemplo de bash que atienda caracteres de nueva línea incrustados (incrustado cualquier cosa) en el nombre del archivo, así que aquí hay uno. Mueve los 3 archivos regulares más antiguos (mdate)

move=3
find . -maxdepth 1 -type f -name '*' \
 -printf "%T@\t%p\0" |sort -znk1 | { 
  while IFS= read -d $'\0' -r file; do
      printf "%s\0" "${file#*$'\t'}"
      ((--move==0)) && break
  done } |xargs -0 mv -t dest

Este es el fragmento de datos de prueba

# make test files with names containing \n, \t and "  "
rm -f '('?[1-4]'  |?)'
for f in $'(\n'{1..4}$'  |\t)' ;do sleep .1; echo >"$f" ;done
touch -d "1970-01-01" $'(\n4  |\t)'
ls -ltr '('?[1-4]'  |'?')'; echo
mkdir -p dest

Aquí está el fragmento de verificación de resultados

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
fuente
+1, solo respuesta útil antes de la mía (y siempre es bueno tener datos de prueba)
Sorpigal
0

Es más fácil hacerlo con GNU find. Lo uso todos los días en mi DVR de Linux para eliminar grabaciones de mi sistema de videovigilancia anteriores a un día.

Aquí está la sintaxis:

find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;

Recuerde que finddefine un día como 24 horas desde el momento de la ejecución. Por lo tanto, los archivos modificados por última vez a las 11 p. M. No se eliminarán a la 1 a. M.

Incluso puede combinar findcon cron, por lo que las eliminaciones se pueden programar automáticamente ejecutando el siguiente comando como root:

crontab -e << EOF
@daily /usr/bin/find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;
EOF

Siempre puede obtener más información findal consultar su página de manual:

man find
Jonathan Frank
fuente
0

Como las otras respuestas no se ajustan a mi propósito y las preguntas, este shell se prueba en CentOS 7:

oldestDir=$(find /yourPath/* -maxdepth 0 -type d -printf '%T+ %p\n' | sort | head -n 1 | tr -s ' ' | cut -d ' ' -f 2)
echo "$oldestDir"
rm -rf "$oldestDir"
Spektakulatius
fuente