Comment récupérer les derniers N fichiers dans un répertoire?

15

J'ai de nombreux fichiers classés par nom de fichier dans un répertoire. Je souhaite copier les N fichiers finaux (disons N = 4) dans mon répertoire personnel. Comment dois-je procéder?

cp ./<the final 4 files> ~/

Sibbs Gambling
la source
1
Sur toutes les réponses ci-dessous, vous devrez peut-être ajouter un "LC_ALL = ..." devant les commandes utilisant les plages, afin que leurs plages utilisent les paramètres régionaux appropriés pour vous (les paramètres régionaux peuvent changer et l'ordre des caractères peut alors changer Ex., dans certains endroits, [az] peut contenir toutes les lettres de l'alphabet à l'exception de "Z", car les lettres sont ordonnées: aAbBcC .... zZ. Il existe d'autres variantes (lettres accentuées et encore plus de commandes exotiques Un choix habituel est:LC_ALL=C
Olivier Dulac

Réponses:

23

Cela peut être facilement fait avec les tableaux bash / ksh93 / zsh:

a=(*)
cp -- "${a[@]: -4}" ~/

Cela fonctionne pour tous les noms de fichiers non masqués même s'ils contiennent des espaces, des tabulations, des retours à la ligne ou d'autres caractères difficiles (en supposant qu'il y ait au moins 4 fichiers non masqués dans le répertoire actuel avec bash).

Comment ça fonctionne

  • a=(*)

    Cela crée un tableau aavec tous les noms de fichiers. Les noms de fichiers renvoyés par bash sont triés par ordre alphabétique. (Je suppose que c'est ce que vous entendez par "trié par nom de fichier". )

  • ${a[@]: -4}

    Cela renvoie les quatre derniers éléments du tableau a(à condition que le tableau contienne au moins 4 éléments bash).

  • cp -- "${a[@]: -4}" ~/

    Cela copie les quatre derniers noms de fichier dans votre répertoire personnel.

Pour copier et renommer en même temps

Cela copiera les quatre derniers fichiers uniquement dans le répertoire de base et, en même temps, ajoutera la chaîne a_au nom du fichier copié:

a=(*)
for fname in "${a[@]: -4}"; do cp -- "$fname" ~/a_"$fname"; done

Copiez à partir d'un répertoire différent et renommez également

Si nous utilisons a=(./some_dir/*)au lieu de a=(*), alors nous avons le problème du répertoire attaché au nom de fichier. Une solution est:

a=(./some_dir/*) 
for f in "${a[@]: -4}"; do cp "$f"  ~/a_"${f##*/}"; done

Une autre solution consiste à utiliser un sous-shell et cddans le répertoire du sous-shell:

(cd ./some_dir && a=(*) && for f in "${a[@]: -4}"; do cp -- "$f" ~/a_"$f"; done) 

Une fois le sous-shell terminé, le shell nous renvoie au répertoire d'origine.

S'assurer que la commande est cohérente

La question demande des fichiers "classés par nom de fichier". Cet ordre, souligne Olivier Dulac dans les commentaires, variera d'un endroit à l'autre. S'il est important d'avoir des résultats fixes indépendamment des paramètres de la machine, il est préférable de spécifier explicitement les paramètres régionaux lorsque le tableau aest défini. Par exemple:

LC_ALL=C a=(*)

Vous pouvez découvrir les paramètres régionaux dans lesquels vous vous trouvez actuellement en exécutant la localecommande.

John1024
la source
9

Si vous utilisez, zshvous pouvez mettre entre parenthèses ()une liste de soi-disant qualificateurs glob qui sélectionnent les fichiers souhaités. Dans votre cas, ce serait

cp *(On[1,4]) ~/

Ici, Onles noms de fichiers sont triés par ordre alphabétique dans l'ordre inverse et [1,4]ne prend que les 4 premiers d'entre eux.

Vous pouvez rendre cela plus robuste en sélectionnant uniquement les fichiers simples (à l'exception des répertoires, des canaux, etc.) avec ., et également en ajoutant --à la cpcommande afin de traiter les fichiers dont les noms commencent -correctement, donc:

cp -- *(.On[1,4]) ~

Ajoutez le Dqualificatif si vous souhaitez également considérer les fichiers cachés (fichiers dot):

cp -- *(D.On[1,4]) ~
jimmij
la source
2
Ou: *([-4,-1]).
Stéphane Chazelas
2
@ StéphaneChazelas oui, permet de clarifier pour les futurs lecteurs que le tri alphabétique dans le sens normal ( on) est un comportement par défaut, donc pas besoin de le spécifier, et -devant les nombres compte les noms de fichiers à partir du bas.
jimmij
7

Voici une solution utilisant des commandes bash extrêmement simples.

find . -maxdepth 1 -type f | sort | tail -n 4 | while read -r file; do cp "$file" ~/; done

Explication:

find . -maxdepth 1 -type f

Recherche tous les fichiers du répertoire actuel.

sort

Trie par ordre alphabétique.

tail -n 4

Afficher uniquement les 4 dernières lignes.

while read -r file; do cp "$file" ~/; done

Boucles sur chaque ligne exécutant la commande de copie.

gswong
la source
6

Tant que vous trouvez le tri du shell agréable, vous pouvez simplement faire:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
cp "$@" /path/to/target/dir

Ceci est très similaire à la bashsolution de tableau spécifique offerte, mais devrait être portable sur n'importe quel shell compatible POSIX. Quelques notes sur les deux méthodes:

  1. Il est important que vous meniez vos cparguments avec cp --un ou .un point ou /à la tête de chaque nom. Si vous ne le faites pas, vous risquez un premier -tiret dans cple premier argument de qui peut être interprété comme une option et entraîner l'échec de l'opération ou rendre autrement des résultats inattendus.

    • Même si vous travaillez avec le répertoire courant comme répertoire source, cela se fait facilement comme ... set -- ./*ou array=(./*).
  2. Il est important lorsque vous utilisez les deux méthodes de vous assurer d'avoir au moins autant d'éléments dans votre tableau arg que vous essayez de supprimer - je le fais ici avec une extension mathématique. Le shiftseul problème se produit s'il y a au moins 4 éléments dans le tableau d'arguments - et ne déplace ensuite que les premiers arguments qui font un surplus de 4.

    • Donc, dans un set 1 2 3 4 5cas, cela déplacera l' 1écart, mais dans un set 1 2 3cas, cela ne changera rien.
    • Par exemple: a=(1 2 3); echo "${a[@]: -4}"imprime une ligne vierge.

Si vous copiez d'un répertoire à un autre, vous pouvez utiliser pax:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
pax -rws '|.*/|new_prefix|' "$@" /path/to/target/dir

... qui appliquerait une sedsubstitution -style à tous les noms de fichiers lors de leur copie.

mikeserv
la source
4

S'il n'y a que des fichiers et que leurs noms ne contiennent pas d'espace ou de nouvelle ligne (et que vous n'avez pas modifié $IFS) ou de caractères globaux (ou certains caractères non imprimables avec certaines implémentations de ls), et ne commencent pas par ., alors vous pouvez le faire :

cp -- $(ls | tail -n 4) ~/
Hauke ​​Laging
la source
C'est ce qu'on appelle une substitution de commande et c'est vraiment pratique. tldp.org/LDP/abs/html/commandsub.html#CSPARENS
iyrin
Merci! Ça marche. Est-il possible d'ajouter rapidement un préfixe a_à TOUS les quatre fichiers? J'ai essayé echo "a_$(ls | tail -n 4)", mais il n'ajoute que le premier fichier.
Sibbs Gambling
@SibbsGambling Mon approche ne convient pas à cela, mais la réponse de John1024 peut facilement être adaptée à cela.
Hauke ​​Laging du
5
En général, l'analyse de la lssortie est considérée comme mauvaise, car elle échoue généralement horriblement sur les noms de fichiers qui contiennent non seulement des espaces, mais également des tabulations, des sauts de ligne ou d'autres caractères valides mais "difficiles".
HBruijn
2
@HaukeLaging Même si ce n'est actuellement pas un problème (parce que je n'ai pas de noms de feu "compliqués" dans mon annuaire), je pourrais en avoir dans 2 ans, et tout à coup les choses tombent sur mes pieds ...
glglgl
1

Il s'agit d'une solution bash simple sans code de boucle. Copiez les 4 derniers fichiers du répertoire actuel vers la destination:

$ find . -maxdepth 1 -type f |tail -n -4|xargs cp -t "$destdir"
yasin_alm
la source
1
Comment vous assurez-vous que findvous donnez les fichiers dans l'ordre souhaité? Comment vous assurez-vous que les fichiers contenant des espaces (y compris les retours à la ligne) dans leurs noms sont traités correctement?
Kusalananda