Script shell pour déplacer les fichiers les plus anciens?

14

Comment écrire un script pour déplacer uniquement les 20 fichiers les plus anciens d'un dossier à un autre? Existe-t-il un moyen de récupérer les fichiers les plus anciens dans un dossier?

user11598
la source
Inclusion ou exclusion de sous-répertoires? Et doit-il être fait de manière récursive (dans une arborescence de répertoires)?
maxschlepzig
2
De nombreux systèmes de fichiers (la plupart?) * Nix ne stockent pas la date de création, vous ne pouvez donc pas déterminer le fichier le plus ancien avec certitude. Les attributs généralement disponibles sont atime(dernier accès), ctime(dernier changement d'autorisation) et mtime(dernière modification) ... par exemple. ls -tand find's printf "%T" use mtime... Il semble, selon ce lien , que mes ext4partitions sont capables de gérer une date de création, mais lset findet statn'ont pas (encore) les options appropriées ...
Peter.O

Réponses:

13

L'analyse de la sortie de lsn'est pas fiable .

À la place, utilisez findpour localiser les fichiers et sortles classer par horodatage. Par exemple:

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)

Que fait tout ça?

Tout d'abord, les findcommandes localisent tous les fichiers et répertoires dans le répertoire courant ( .), mais pas dans les sous-répertoires du répertoire courant ( -maxdepth 1), puis imprime:

  • Un horodatage
  • Un espace
  • Le chemin d'accès relatif au fichier
  • Un caractère NULL

L'horodatage est important. Le %T@spécificateur de format pour -printfse décompose en T, qui indique "Dernière heure de modification" du fichier (mtime) et @, qui indique "Secondes depuis 1970", y compris les fractions de seconde.

L'espace n'est qu'un délimiteur arbitraire. Le chemin d'accès complet au fichier est pour que nous puissions y faire référence plus tard, et le caractère NULL est un terminateur car il s'agit d'un caractère illégal dans un nom de fichier et nous permet ainsi de savoir avec certitude que nous avons atteint la fin du chemin vers le fichier.

J'ai inclus 2>/dev/nullafin que les fichiers auxquels l'utilisateur n'est pas autorisé à accéder soient exclus, mais les messages d'erreur les excluant sont supprimés.

Le résultat de la findcommande est une liste de tous les répertoires du répertoire courant. La liste est dirigée vers sortlaquelle est chargé de:

  • -z Traitez NULL comme le caractère de fin de ligne au lieu de la nouvelle ligne.
  • -n Trier numériquement

Puisque secondes depuis 1970 monte toujours, nous voulons le fichier dont l'horodatage était le plus petit nombre. Le premier résultat de sortsera la ligne contenant le plus petit horodatage numéroté. Il ne reste plus qu'à extraire le nom du fichier.

Les résultats du find, sortpipeline est passé par la substitution de processus pour whilelequel il est lu comme si elle était un fichier sur stdin. whileà son tour, invoque readpour traiter l'entrée.

Dans le contexte de, readnous définissons la IFSvariable sur rien, ce qui signifie que les espaces blancs ne seront pas interprétés de manière inappropriée comme un délimiteur. readest dit -r, ce qui désactive l' expansion d'échappement, et -d $'\0', ce qui rend le séparateur de fin de ligne NULL, correspondant à la sortie de notre find, sortpipeline.

Le premier bloc de données, qui représente le chemin de fichier le plus ancien précédé de son horodatage et d'un espace, est lu dans la variable line. Ensuite, la substitution de paramètres est utilisée avec l'expression #*, qui remplace simplement tous les caractères depuis le début de la chaîne jusqu'au premier espace, y compris l'espace, sans rien. Cela supprime l'horodatage de modification, ne laissant que le chemin d'accès complet au fichier.

À ce stade, le nom du fichier est stocké $fileet vous pouvez en faire ce que vous voulez. Lorsque vous avez fini de faire quelque chose avec $filel' whileinstruction, la boucle readsera exécutée et la commande sera exécutée à nouveau, en extrayant le morceau suivant et le nom de fichier suivant.

N'y a-t-il pas un moyen plus simple?

Non. Les moyens plus simples sont bogués.

Si vous utilisez ls -tet dirigez vers headou tail(ou quoi que ce soit ), vous casserez les fichiers avec des retours à la ligne dans les noms de fichiers. Si vous avez mv $(anything)ensuite des fichiers avec un espace dans le nom, cela entraînera une rupture. Si vous avez mv "$(anything)"ensuite des fichiers avec des retours à la ligne dans le nom, cela entraînera une rupture. Si vous readne le faites pas, -d $'\0'vous allez casser des fichiers avec des espaces dans leurs noms.

Peut-être que dans certains cas spécifiques, vous savez avec certitude qu'une méthode plus simple est suffisante, mais vous ne devriez jamais écrire des hypothèses comme celle-ci dans des scripts si vous pouvez éviter de le faire.

Solution

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

Appelez comme:

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

Pour déplacer les 20 fichiers les plus anciens de /var/log/foo/vers /mnt/backup/.

Notez que j'inclus des fichiers et des répertoires. Pour les fichiers, ajoutez uniquement -type fà l' findinvocation.

Merci

Merci à enzotib et Павел Танков pour les améliorations apportées à cette réponse.

Sorpigal
la source
Le tri ne doit pas utiliser -n. Au moins dans ma version, il ne trie pas correctement les nombres décimaux. Vous devez soit supprimer le point de la date, soit utiliser des -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rzdates ISO ou autre chose.
l0b0
@ l0b0: Cette limitation m'est connue. Je suppose qu'il suffit de ne pas exiger ce niveau de granularité (c'est-à-dire que le tri au-delà de la .doit être hors de propos pour vous.) Ce serait plus clair à dire sort -z -n -t. -k1.
Sorpigal
@ l0b0: Votre solution présente le même bogue, peu importe: %TSaffiche également une "partie fractionnaire" qui serait sous la forme 00.0000000000, vous perdez donc également la granularité. Un GNU récent sortpourrait résoudre ce problème en utilisant -Vun "tri de version", qui traitera ce type de virgule flottante comme prévu.
Sorpigal
Non, car je fais un tri de chaîne sur "AAAA-MM-JJThh: mm: ss" plutôt qu'un tri numérique. Le tri par chaîne ne se soucie pas des décimales, il devrait donc fonctionner jusqu'à l'année 10000 :)
l0b0
@ l0b0: Un tri de chaîne %T@fonctionnerait également, car il est complété par zéro.
Sorpigal
4

C'est plus simple dans zsh, où vous pouvez utiliser le Om qualificatif glob pour trier les correspondances par date (le plus ancien en premier) et le [1,20]qualificatif pour ne conserver que les 20 premières correspondances:

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

Ajouter le D qualificatif si vous souhaitez également inclure des fichiers de points. Ajoutez .si vous souhaitez faire correspondre uniquement les fichiers standard et non les répertoires.

Si vous n'avez pas zsh, voici un one-liner Perl (vous pouvez le faire en moins de 80 caractères, mais à des frais supplémentaires de clarté):

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

Avec seulement des outils POSIX ou même bash ou ksh, le tri des fichiers par date est une tâche difficile. Vous pouvez le faire facilement avec ls, mais l'analyse de la sortie de lsest problématique, donc cela ne fonctionne que si les noms de fichiers ne contiennent que des caractères imprimables autres que les retours à la ligne.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Gilles 'SO- arrête d'être méchant'
la source
4

Combinez la ls -tsortie avec tailou head.

Exemple simple, qui ne fonctionne que si tous les noms de fichiers contiennent uniquement des caractères imprimables autres que des espaces et \[*?:

 mv $(ls -1tr | head -20) other_folder
ktf
la source
1
Ajoutez l'option -A à ls:ls -1Atr
Arcege
1
-1, dangereux. Ici , permettez - moi de l' artisanat un exemple: touch $'foo\n*'. Que se passe-t-il si vous exécutez mv "$ (ls)" avec ce fichier là?
Sorpigal
1
@Sorpigal Sérieusement? C'est un peu faible de dire "Permettez-moi de trouver un exemple que vous avez spécifiquement dit ne fonctionnera pas. Hé, regardez, cela ne fonctionne pas"
Michael Mrozek
1
@Sorpigal Ce n'est pas une mauvaise idée, cela fonctionne dans 99% des cas. La réponse est "si vous avez des fichiers avec des noms normaux, cela fonctionne. Si vous êtes un fou qui incorpore des nouvelles lignes dans leurs noms de fichiers, ce ne sera pas le cas". C'est tout à fait correct
Michael Mrozek
1
@MichaelMrozek: C'est une mauvaise idée et c'est mauvais car ça échoue parfois. Si vous avez la possibilité de faire ce qui échoue parfois et ce qui ne fonctionne pas, vous devez prendre l'option qui ne fonctionne pas (et celle qui le fait est mauvaise). Faites ce que vous voulez de manière interactive, mais dans un fichier script et lorsque vous donnez des conseils, faites-le correctement.
Sorpigal
3

Vous pouvez utiliser GNU find pour cela:

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

Où find affiche l'heure de modification (en secondes à partir de 1970) et le nom de chaque fichier du répertoire courant, la sortie est triée selon le premier champ, les 20 plus anciennes sont filtrées et déplacées vers dest_dir. Supprimez le echosi vous avez testé la ligne de commande.

maxschlepzig
la source
2

Personne n'a (encore) publié d'exemple bash qui répond aux caractères de nouvelle ligne intégrés (quoi que ce soit) dans le nom de fichier, alors en voici un. Il déplace les 3 fichiers réguliers les plus anciens (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

Ceci est l' extrait de données de test

# 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

Voici l' extrait des résultats de vérification

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
la source
+1, seule réponse utile avant la mienne (et il est toujours bon d'avoir des données de test.)
Sorpigal
0

C'est plus simple à faire avec GNU find. Je l'utilise tous les jours sur mon DVR Linux pour supprimer des enregistrements de mon système de vidéosurveillance datant de plus d'un jour.

Voici la syntaxe:

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

N'oubliez pas que cela finddéfinit un jour comme 24 heures à partir du moment de l'exécution. Par conséquent, les fichiers modifiés pour la dernière fois à 23 h ne seront pas supprimés à 1 h.

Vous pouvez même combiner findavec cron, afin que les suppressions puissent être planifiées automatiquement en exécutant la commande suivante en tant que root:

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

Vous pouvez toujours obtenir plus d'informations sur finden consultant sa page de manuel:

man find
Jonathan Frank
la source
0

comme les autres réponses ne correspondent pas à mon objectif et aux questions, ce shell est testé sur 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
la source