comment `tail` le dernier fichier dans un répertoire

20

En shell, comment puis-je trouver taille dernier fichier créé dans un répertoire?

Itay Moav -Malimovka
la source
1
Allez, les programmeurs doivent faire la queue!
2010
La fermeture sert uniquement à passer en superutilisateur ou en défaut de serveur. La question vivra là-bas, et plus de gens qui pourraient être intéressés la trouveront.
Mnementh
Le vrai problème ici est de trouver le fichier de mise à jour le plus récent dans le répertoire et je pense que cela a déjà été répondu (ici ou sur Super User, je ne me souviens pas).
dmckee

Réponses:

24

Ne pas analyser la sortie de ls! L'analyse de la sortie de ls est difficile et peu fiable .

Si vous devez le faire, je vous recommande d'utiliser find. À l'origine, j'avais ici un exemple simple simplement pour vous donner l'essentiel de la solution, mais comme cette réponse semble quelque peu populaire, j'ai décidé de la réviser pour fournir une version qui est sûre à copier / coller et à utiliser avec toutes les entrées. Êtes-vous assis confortablement? Nous allons commencer avec un oneliner qui vous donnera le dernier fichier dans le répertoire courant:

tail -- "$(find . -maxdepth 1 -type f -printf '%T@.%p\0' | sort -znr -t. -k1,2 | while IFS= read -r -d '' -r record ; do printf '%s' "$record" | cut -d. -f3- ; break ; done)"

Pas tout à fait un oneliner maintenant, n'est-ce pas? Le voici à nouveau comme une fonction shell et formaté pour une lecture plus facile:

latest-file-in-directory () {
    find "${@:-.}" -maxdepth 1 -type f -printf '%T@.%p\0' | \
            sort -znr -t. -k1,2 | \
            while IFS= read -r -d '' -r record ; do
                    printf '%s' "$record" | cut -d. -f3-
                    break
            done
}

Et maintenant que comme oneliner:

tail -- "$(latest-file-in-directory)"

Si tout le reste échoue, vous pouvez inclure la fonction ci-dessus dans votre .bashrcet considérer le problème résolu, avec une mise en garde. Si vous vouliez simplement faire le travail, vous n'avez pas besoin de lire plus loin.

La mise en garde avec ceci est qu'un nom de fichier se terminant par une ou plusieurs nouvelles lignes ne sera toujours pas transmis tailcorrectement. Contourner ce problème est compliqué et je considère suffisant que si un tel nom de fichier malveillant est rencontré, le comportement relativement sûr de rencontrer une erreur "No such file" se produira au lieu de quelque chose de plus dangereux.

Détails juteux

Pour les curieux, c'est l'explication fastidieuse de son fonctionnement, pourquoi il est sûr et pourquoi d'autres méthodes ne le sont probablement pas.

Danger, Will Robinson

Tout d'abord, le seul octet qui soit sûr pour délimiter les chemins de fichiers est nul car c'est le seul octet universellement interdit dans les chemins de fichiers sur les systèmes Unix. Il est important lors de la gestion d'une liste de chemins de fichiers de n'utiliser que null comme délimiteur et, lors de la remise d'un seul chemin de fichier d'un programme à un autre, de le faire d'une manière qui ne s'étouffe pas sur des octets arbitraires. Il existe de nombreuses façons apparemment correctes de résoudre ce problème et d'autres qui échouent en supposant (même accidentellement) que les noms de fichiers n'auront ni nouvelles lignes ni espaces. Aucune de ces hypothèses n'est sûre.

Pour les besoins d'aujourd'hui, la première étape consiste à obtenir une liste de fichiers délimités par des valeurs nulles. C'est assez facile si vous avez un findsupport -print0tel que GNU:

find . -print0

Mais cette liste ne nous dit toujours pas laquelle est la plus récente, nous devons donc inclure cette information. Je choisis d'utiliser le -printfcommutateur find qui me permet de spécifier quelles données apparaissent dans la sortie. Pas toutes les versions de findsupport -printf(ce n'est pas standard) mais GNU find le fait. Si vous vous retrouvez sans, -printfvous devrez vous fier -exec stat {} \;à quel point vous devez abandonner tout espoir de portabilité, ce qui statn'est pas standard non plus. Pour l'instant, je vais continuer en supposant que vous avez des outils GNU.

find . -printf '%T@.%p\0'

Ici, je demande le format printf %T@qui est le temps de modification en secondes depuis le début de l'époque Unix suivi d'un point puis suivi d'un nombre indiquant des fractions de seconde. J'ajoute à cela une autre période, puis %p(qui est le chemin d'accès complet au fichier) avant de terminer avec un octet nul.

Maintenant j'ai

find . -maxdepth 1 \! -type d -printf '%T@.%p\0'

Cela peut aller de soi, mais pour être complet, il -maxdepth 1empêche findde lister le contenu des sous-répertoires et \! -type dignore les répertoires que vous ne voudrez probablement pas tail. Jusqu'à présent, j'ai des fichiers dans le répertoire actuel avec des informations sur l'heure de modification, alors maintenant je dois trier par heure de modification.

Le mettre dans le bon ordre

Par défaut, sortattend que son entrée soit des enregistrements délimités par des sauts de ligne. Si vous avez GNU, sortvous pouvez lui demander de s'attendre à des enregistrements séparés par des valeurs nulles à la place en utilisant le -zcommutateur .; pour le standard sortil n'y a pas de solution. Je ne suis intéressé que par le tri par les deux premiers nombres (secondes et fractions de seconde) et je ne veux pas trier par le nom de fichier réel, donc je dis sortdeux choses: d'abord, il doit considérer le point ( .) comme un délimiteur de champ et deuxièmement, il ne doit utiliser les premier et deuxième champs que pour déterminer comment trier les enregistrements.

| sort -znr -t. -k1,2

Tout d'abord, je regroupe trois options courtes qui ne prennent aucune valeur ensemble; -znrest juste une façon concise de dire -z -n -r). Après cela -t .(l'espace est facultatif) indique sortle caractère délimiteur de champ et -k 1,2spécifie les numéros de champ: premier et deuxième ( sortcompte les champs à partir de un, pas de zéro). N'oubliez pas qu'un exemple d'enregistrement pour le répertoire actuel ressemblerait à:

1000000000.0000000000../some-file-name

Cela signifie sortque vous regarderez d'abord 1000000000puis 0000000000lors de la commande de ce disque. L' -noption indique sortd'utiliser la comparaison numérique lors de la comparaison de ces valeurs, car les deux valeurs sont des nombres. Cela peut ne pas être important car les nombres sont de longueur fixe mais cela ne fait pas de mal.

L'autre commutateur donné sortest -rpour "inverser". Par défaut, la sortie d'un tri numérique sera en premier les nombres les plus bas, la -rmodifie pour qu'elle répertorie les derniers nombres en bas et les premiers en premier. Étant donné que ces chiffres sont des horodatages plus élevés, cela signifie que les nouveaux sont plus récents, ce qui place le plus récent enregistrement au début de la liste.

Juste les morceaux importants

Comme la liste des chemins de fichiers qui en émerge sorta maintenant la réponse souhaitée que nous recherchons tout en haut. Reste à trouver un moyen de supprimer les autres enregistrements et de supprimer l'horodatage. Malheureusement, même GNU headet tailn'acceptent pas les commutateurs pour les faire fonctionner sur une entrée délimitée par des valeurs nulles. Au lieu de cela, j'utilise une boucle while comme une sorte de pauvre head.

| while IFS= read -r -d '' record

Tout d'abord, je désactive IFSla liste des fichiers pour qu'elle ne soit pas soumise au fractionnement de mots. Ensuite, je dis readdeux choses: n'interprétez pas les séquences d'échappement dans l'entrée ( -r) et l'entrée est délimitée par un octet nul ( -d); ici, la chaîne vide ''est utilisée pour indiquer "pas de délimiteur" aka délimité par null. Chaque enregistrement sera lu dans la variable de recordsorte que chaque fois que la whileboucle itère, il ait un seul horodatage et un seul nom de fichier. Notez qu'il -ds'agit d'une extension GNU; si vous n'avez qu'une norme, readcette technique ne fonctionnera pas et vous avez peu de recours.

Nous savons que la recordvariable comporte trois parties, toutes délimitées par des caractères de période. En utilisant l' cututilitaire, il est possible d'en extraire une partie.

printf '%s' "$record" | cut -d. -f3-

Ici, le dossier entier est transmis à printfet de là canalisé vers cut; en bash, vous pouvez simplifier davantage en utilisant une chaîne ici pour cut -d. -3f- <<<"$record"de meilleures performances. Nous disons cutdeux choses: d'abord avec -dcela il faut un délimiteur spécifique pour identifier les champs (comme avec sortle délimiteur .utilisé). Le second cutest chargé -fd'imprimer uniquement les valeurs de champs spécifiques; la liste des champs est donnée sous la forme d'une plage 3-qui indique la valeur du troisième champ et de tous les champs suivants. Cela signifie que cutva lire et ignorer tout jusqu'à et y compris la seconde .qu'il trouve dans l'enregistrement, puis imprimer le reste, qui est la partie du chemin d'accès au fichier.

Après avoir imprimé le dernier chemin de fichier, il n'est pas nécessaire de continuer: breakquitte la boucle sans la laisser passer au deuxième chemin de fichier.

La seule chose qui reste est en cours taild' exécution sur le chemin de fichier renvoyé par ce pipeline. Vous avez peut-être remarqué dans mon exemple que j'ai fait cela en enfermant le pipeline dans un sous-shell; ce que vous n'avez peut-être pas remarqué, c'est que j'ai mis le sous-shell entre guillemets. Ceci est important car au final, même avec tous ces efforts pour être sûr pour tous les noms de fichiers, une extension de sous-shell non citée pourrait toujours casser les choses. Une explication plus détaillée est disponible si vous êtes intéressé. Le deuxième aspect important mais facilement ignoré de l'invocation de tailest que je lui ai fourni l'option --avant d'étendre le nom du fichier. Cela demanderatailqu'aucune autre option n'est spécifiée et que tout ce qui suit est un nom de fichier, ce qui permet de gérer en toute sécurité les noms de fichier commençant par -.

phogg
la source
1
@AakashM: car vous pouvez obtenir des résultats "surprenants", par exemple si un fichier contient des caractères "inhabituels" dans son nom (presque tous les caractères sont légaux).
John Zwinck
6
Les personnes qui utilisent des caractères spéciaux dans leurs noms de fichiers méritent tout ce qu'elles obtiennent :-)
6
Voir paxdiablo faire cette remarque était déjà assez douloureux, mais deux personnes l'ont voté! Les personnes qui écrivent des logiciels buggy méritent intentionnellement tout ce qu'elles obtiennent.
John Zwinck
4
Donc, la solution ci-dessus ne fonctionne pas sur osx en raison du manque d'option -printf dans find, mais ce qui suit ne fonctionne que sur osx en raison de différences dans la commande stat ... peut-être que cela aidera toujours quelqu'untail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
audio.zoom
2
"Malheureusement, même GNU headet tailn'acceptent pas les commutateurs pour les faire fonctionner sur une entrée délimitée par des valeurs nulles." Mon remplacement pour head: … | grep -zm <number> "".
Kamil Maciorowski
22
tail `ls -t | head -1`

Si vous vous inquiétez des noms de fichiers avec des espaces,

tail "`ls -t | head -1`"
Pointu
la source
1
Mais que se passe-t-il lorsque votre dernier fichier comporte des espaces ou des caractères spéciaux? Utilisez $ () au lieu de `` et citez votre sous-shell pour éviter ce problème.
phogg
J'aime ça. Propre et simple. Comme cela devrait être.
6
Il est facile d'être propre et simple si vous sacrifiez robuste et correct.
phogg
2
Eh bien, cela dépend vraiment de ce que vous faites. Une solution qui fonctionne toujours partout, pour tous les noms de fichiers possibles, est très agréable, mais dans une situation contrainte (fichiers journaux, par exemple, avec des noms connus non étranges), cela peut être inutile.
C'est la solution la plus propre à ce jour. Je vous remercie!
Demisx
4

Vous pouvez utiliser:

tail $(ls -1t | head -1)

La $()construction démarre un sous-shell qui exécute la commande ls -1t(répertoriant tous les fichiers dans l'ordre temporel, un par ligne) et les redirige head -1pour obtenir la première ligne (fichier).

La sortie de cette commande (le fichier le plus récent) est ensuite transmise tailpour être traitée.

Gardez à l'esprit que cela court le risque d'obtenir un répertoire s'il s'agit de l'entrée de répertoire la plus récente créée. J'ai utilisé cette astuce dans un alias pour modifier le fichier journal le plus récent (à partir d'un ensemble rotatif) dans un répertoire qui ne contenait que ces fichiers journaux.


la source
Le -1n'est pas nécessaire, lsfait cela pour vous quand il est dans un tuyau. Comparez lset ls|cat, par exemple.
pause jusqu'à nouvel ordre.
Cela peut être le cas sous Linux. Sous "vrai" Unix, les processus ne changeaient pas leur comportement en fonction de la destination de leur sortie. Cela rendrait le débogage du pipeline vraiment ennuyeux :-)
Hmmm, je ne suis pas sûr que ce soit correct - ISTR doit émettre "ls -C" pour obtenir une sortie au format colonne sous 4.2BSD lors de la transmission de la sortie via un filtre, et je suis presque sûr que ls sous Solaris fonctionne de la même manière. Qu'est-ce que le "One, true Unix" de toute façon?
Citations! Citations! Les noms de fichiers contiennent des espaces!
Norman Ramsey
@TMN: Le seul vrai moyen Unix est de ne pas compter sur ls pour les consommateurs non humains. "Si la sortie est vers un terminal, le format est défini par l'implémentation." - c'est la spécification. Si vous voulez être sûr, vous devez dire ls -1 ou ls -C.
phogg
4

Sur les systèmes POSIX, il n’existe aucun moyen d’obtenir l’entrée de répertoire "créée en dernier". Chaque entrée de répertoire a atime, mtimeet ctime, mais contrairement à Microsoft Windows, le ctimene signifie pas CreationTime, mais "Heure du dernier changement d'état".

Donc, le mieux que vous puissiez obtenir est de "suivre le dernier fichier récemment modifié", ce qui est expliqué dans les autres réponses. J'irais pour cette commande:

tail -f "$ (ls -tr | sed 1q)"

Notez les guillemets autour de la lscommande. Cela permet à l'extrait de fonctionner avec presque tous les noms de fichiers.

Roland Illig
la source
Bon travail. Droit au but. +1
Norman Ramsey
4

Si vous voulez juste voir le changement de taille de fichier, vous pouvez utiliser la montre.

watch -d ls -l
tpal
la source
3

Dans zsh:

tail *(.om[1])

Voir: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers , ici mdénote le temps de modification m[Mwhms][-|+]n, et le précédent osignifie qu'il est trié dans un sens (le Otrie dans l'autre sens). Cela .signifie que les fichiers normaux. Dans les parenthèses [1]choisit le premier élément. Pour en choisir trois [1,3], pour obtenir la plus ancienne utilisation [-1].

C'est bien court et ne l'utilise pas ls.

Anne van Rossum
la source
1

Il y a probablement un million de façons de le faire, mais la façon dont je le ferais est la suivante:

tail `ls -t | head -n 1`

Les bits entre les pointes (les guillemets comme des caractères) sont interprétés et le résultat renvoyé à la queue.

ls -t #gets the list of files in time order
head -n 1 # returns the first line only
iblamefish
la source
2
Les contre-coups sont mauvais. Utilisez plutôt $ ().
William Pursell
1

Un simple:

tail -f /path/to/directory/*

fonctionne très bien pour moi.

Le problème est d'obtenir les fichiers générés après avoir démarré la commande tail. Mais si vous n'en avez pas besoin (comme toutes les solutions ci-dessus ne s'en soucient pas), l'astérisque est simplement une solution plus simple, l'OMI.

bruno.braga
la source
0
tail`ls -tr | tail -1`
user22644
la source
Vous avez oublié un espace là-bas!
Blacklight Shining
0

Quelqu'un l'a posté, puis l'a effacé pour une raison quelconque, mais c'est le seul qui fonctionne, alors ...

tail -f `ls -tr | tail`
Itay Moav -Malimovka
la source
vous devez exclure des répertoires, n'est-ce pas?
Amit
1
J'ai posté cela à l'origine mais je l'ai supprimé car je suis d'accord avec Sorpigal que l'analyse de la sortie de lsn'est pas la chose la plus intelligente à faire ...
ChristopheD
J'en ai besoin rapide et sale, pas de répertoires dedans. Donc, si vous ajoutez votre réponse, je l'accepterai
Itay Moav -Malimovka
0
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`

Explication:

  • ls -lt: liste de tous les fichiers et répertoires triés par heure de modification
  • grep -v ^ d: exclure les répertoires
  • head -2 onwards: analyse du nom de fichier requis
amit
la source
1
+1 pour intelligent, -2 pour analyser la sortie ls, -1 pour ne pas citer le sous-shell, -1 pour une hypothèse magique "champ 8" (ce n'est pas portable!) Et enfin -1 pour trop intelligent . Note globale: -4.
phogg
@Sorpigal a accepté. Heureux d'être le mauvais exemple.
2010
oui, je n'imaginais pas que ce serait mal à bien des
égards
0
tail "$(ls -1tr|tail -1)"
user31894
la source