L'analyse de la sortie de ls
n'est pas fiable .
À la place, utilisez find
pour localiser les fichiers et sort
les 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 find
commandes 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 -printf
se 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/null
afin 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 find
commande est une liste de tous les répertoires du répertoire courant. La liste est dirigée vers sort
laquelle 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 sort
sera la ligne contenant le plus petit horodatage numéroté. Il ne reste plus qu'à extraire le nom du fichier.
Les résultats du find
, sort
pipeline est passé par la substitution de processus pour while
lequel il est lu comme si elle était un fichier sur stdin. while
à son tour, invoque read
pour traiter l'entrée.
Dans le contexte de, read
nous définissons la IFS
variable sur rien, ce qui signifie que les espaces blancs ne seront pas interprétés de manière inappropriée comme un délimiteur. read
est 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
, sort
pipeline.
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é $file
et vous pouvez en faire ce que vous voulez. Lorsque vous avez fini de faire quelque chose avec $file
l' while
instruction, la boucle read
sera 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 -t
et dirigez vers head
ou 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 read
ne 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' find
invocation.
Merci
Merci à enzotib et Павел Танков pour les améliorations apportées à cette réponse.