come `tail` il file più recente in una directory


20

Nella shell, come posso taill'ultimo file creato in una directory?


1
dai chiudiporta, i programmatori devono fare la coda!
Amit

La chiusura è solo per passare a superutente o serverfault. La domanda vivrà lì, e più persone che potrebbero essere interessate la troveranno.
Mnementh

Il vero problema qui è trovare il file di aggiornamento più recente nella directory e credo che abbia già ricevuto una risposta (qui o su Super User, non riesco a ricordare).
dmckee,

Risposte:


24

Evitare Non analizzare l'output di ls! L'analisi dell'output di ls è difficile e inaffidabile .

Se devi farlo, ti consiglio di usare find. Inizialmente avevo qui un semplice esempio semplicemente per darvi il senso della soluzione, ma poiché questa risposta sembra piuttosto popolare, ho deciso di rivederlo per fornire una versione sicura da copiare / incollare e utilizzare con tutti gli input. Sei seduto comodamente? Inizieremo con un oneliner che ti fornirà l'ultimo file nella directory corrente:

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)"

Non è proprio un oneliner ora, vero? Eccolo di nuovo come funzione shell e formattato per una lettura più semplice:

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
}

E ora che come oneliner:

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

Se tutto il resto fallisce, puoi includere la funzione sopra nella tua .bashrce considerare il problema risolto, con un avvertimento. Se volevi semplicemente portare a termine il lavoro, non devi leggere altro.

L'avvertenza è che un nome di file che termina con una o più nuove righe non verrà ancora passato tailcorrettamente. Risolvere questo problema è complicato e ritengo sufficiente che, se si incontra un nome di file così dannoso, si verificherà il comportamento relativamente sicuro di riscontrare un errore "Nessun file di questo tipo" invece di qualcosa di più pericoloso.

Dettagli succosi

Per i curiosi questa è la noiosa spiegazione di come funziona, perché è sicura e perché probabilmente altri metodi non lo sono.

Pericolo, Will Robinson

Prima di tutto, l'unico byte sicuro per delimitare i percorsi dei file è nullo perché è l'unico byte universalmente proibito nei percorsi dei file sui sistemi Unix. È importante quando si gestisce un elenco di percorsi di file utilizzare null solo come delimitatore e, quando si passa anche un singolo percorso di file da un programma a un altro, farlo in modo da non soffocare su byte arbitrari. Esistono molti modi apparentemente corretti per risolvere questo e altri problemi che falliscono supponendo (anche accidentalmente) che i nomi dei file non contengano né nuove righe né spazi. Nessuna ipotesi è sicura.

Per gli scopi odierni il primo passo è quello di ottenere un elenco di file delimitato da null da trovare. Questo è abbastanza semplice se hai un findsupporto -print0come GNU:

find . -print0

Ma questo elenco non ci dice ancora quale è il più recente, quindi è necessario includere tali informazioni. Ho scelto di utilizzare l' -printfopzione find che mi consente di specificare quali dati vengono visualizzati nell'output. Non tutte le versioni di findsupporto -printf(non è standard) ma GNU find lo fa. Se ti trovi senza di -printfte, dovrai fare affidamento su -exec stat {} \;quale punto devi rinunciare a tutte le speranze di portabilità, dato che statnon è neanche standard. Per ora continuerò supponendo che tu abbia gli strumenti GNU.

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

Qui sto chiedendo il formato printf %T@che è il tempo di modifica in secondi dall'inizio dell'epoca Unix seguito da un punto e quindi seguito da un numero che indica le frazioni di secondo. Aggiungo a questo un altro periodo e poi %p(che è il percorso completo del file) prima di terminare con un byte null.

Adesso ho

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

Inutile dire che, per motivi di completezza, -maxdepth 1impedisce finddi elencare i contenuti delle sottodirectory e \! -type dsalta le directory che difficilmente si vorrebbe tail. Finora ho i file nella directory corrente con le informazioni sul tempo di modifica, quindi ora ho bisogno di ordinare per quel tempo di modifica.

Ottenendolo nell'ordine giusto

Per impostazione predefinita, si sortaspetta che i suoi input siano record delimitati da newline. Se hai GNU sortpuoi chiedergli di aspettarti record delimitati da null usando invece l' -zopzione .; per standard sortnon esiste soluzione. Sono interessato solo all'ordinamento per i primi due numeri (secondi e frazioni di secondo) e non voglio ordinare in base al nome del file effettivo, quindi dico sortdue cose: in primo luogo, che dovrebbe considerare il punto ( .) un delimitatore di campo e secondo che dovrebbe usare solo il primo e il secondo campo quando si considera come ordinare i record.

| sort -znr -t. -k1,2

Prima di tutto, sto raggruppando tre opzioni brevi che non hanno valore insieme; -znrè solo un modo conciso di dire -z -n -r). Successivamente -t .(lo spazio è facoltativo) indica sortil carattere delimitatore di campo e -k 1,2specifica i numeri di campo: primo e secondo ( sortconta i campi da uno, non zero). Ricorda che un record di esempio per la directory corrente sarebbe simile a:

1000000000.0000000000../some-file-name

Questo significa sortche guarderemo prima 1000000000e poi 0000000000quando ordiniamo questo record. L' -nopzione dice sortdi usare il confronto numerico quando si confrontano questi valori, perché entrambi i valori sono numeri. Questo potrebbe non essere importante poiché i numeri hanno una lunghezza fissa ma non fa male.

L'altro interruttore dato sortè -rper "reverse". Per impostazione predefinita, l'output di un ordinamento numerico sarà prima il numero più basso, lo -rmodifica in modo tale da elencare per primi i numeri più bassi e quelli più alti. Dal momento che questi numeri sono più alti, i timestamp significheranno più recenti e questo metterà il record più recente all'inizio dell'elenco.

Solo i pezzi importanti

Poiché l'elenco dei percorsi dei file emerge da sortesso ora ha la risposta desiderata che stiamo cercando proprio in alto. Ciò che resta è trovare il modo di scartare gli altri record e di eliminare il timestamp. Sfortunatamente anche GNU heade tailnon accettano switch per farli funzionare su input delimitati da null. Invece uso un ciclo while come una specie di povero head.

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

Innanzitutto ho disattivato in IFSmodo che l'elenco dei file non sia soggetto a suddivisione in parole. Successivamente dico readdue cose: non interpretare le sequenze di escape nell'input ( -r) e l'input è delimitato da un byte null ( -d); qui la stringa vuota ''viene utilizzata per indicare "nessun delimitatore" alias delimitato da null. Ogni record verrà letto nella variabile in recordmodo che ogni volta che il whileciclo itera abbia un singolo timestamp e un singolo nome di file. Nota che -dè un'estensione GNU; se hai solo uno standard readquesta tecnica non funzionerà e avrai poca possibilità.

Sappiamo che la recordvariabile ha tre parti, tutte delimitate da caratteri punto. Utilizzando l' cututilità è possibile estrarne una parte.

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

Qui l'intero record viene passato printfe inviato da lì a cut; in bash si potrebbe semplificare ulteriormente questo utilizzando una stringa di qui per cut -d. -3f- <<<"$record"per migliorare le prestazioni. Diciamo cutdue cose: prima di -dtutto dovrebbe essere un delimitatore specifico per identificare i campi (come nel caso sortdel delimitatore .). Al secondo cutviene -frichiesto di stampare solo valori da campi specifici; l'elenco dei campi viene fornito come un intervallo 3-che indica il valore dal terzo campo e da tutti i campi seguenti. Ciò significa che cutleggerà e ignorerà tutto, incluso il secondo .che trova nel record, quindi stamperà il resto, che è la parte del percorso del file.

Dopo aver stampato il percorso del file più recente, non è necessario andare avanti: breakesce dal ciclo senza lasciarlo passare al secondo percorso del file.

L'unica cosa che rimane è in esecuzione tailsul percorso del file restituito da questa pipeline. Potresti aver notato nel mio esempio che l'ho fatto racchiudendo la pipeline in una subshell; quello che potresti non aver notato è che ho racchiuso la subshell tra virgolette doppie. Questo è importante perché alla fine, anche con tutto questo sforzo per essere al sicuro per qualsiasi nome di file, un'espansione della sottoshell non quotata potrebbe ancora rompere le cose. Una spiegazione più dettagliata è disponibile se sei interessato. Il secondo aspetto importante ma facilmente trascurabile dell'invocazione di tailè che gli ho fornito l'opzione --prima di espandere il nome del file. Questo istruiràtailche non vengono specificate più opzioni e tutto ciò che segue è un nome file, il che rende sicuro gestire i nomi dei file che iniziano con -.


1
@AakashM: perché potresti ottenere risultati "sorprendenti", ad esempio se un file ha caratteri "insoliti" nel suo nome (quasi tutti i caratteri sono legali).
John Zwinck,

6
Le persone che usano caratteri speciali nei loro nomi di file meritano tutto ciò che ottengono :-)

6
Vedere paxdiablo fare quell'osservazione è stato abbastanza doloroso, ma poi due persone hanno votato! Le persone che scrivono software difettosi meritano intenzionalmente tutto ciò che ottengono.
John Zwinck,

4
Quindi la soluzione sopra non funziona su osx a causa della mancanza dell'opzione -printf in find, ma quanto segue funziona solo su osx a causa delle differenze nel comando stat ... forse aiuterà ancora qualcunotail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
audio.zoom

2
"Sfortunatamente anche GNU heade tailnon accettano switch per farli funzionare su input delimitati da null." La mia sostituzione head: … | grep -zm <number> "".
Kamil Maciorowski,

22
tail `ls -t | head -1`

Se sei preoccupato per i nomi di file con spazi,

tail "`ls -t | head -1`"

1
Ma cosa succede quando il tuo ultimo file ha spazi o caratteri speciali? Usa $ () invece di `` e cita la tua subshell per evitare questo problema.
Phogg

Mi piace questo. Pulito e semplice. Come dovrebbe essere.

6
È facile essere puliti e semplici se sacrifichi robusto e corretto.
phogg

2
Beh, dipende da cosa stai facendo, davvero. Una soluzione che funziona sempre ovunque, per tutti i possibili nomi di file, è molto bella, ma in una situazione limitata (file di registro, ad esempio, con nomi non strani noti) potrebbe non essere necessaria.

Questa è la soluzione più pulita finora. Grazie!
Demisx

4

Puoi usare:

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

Il $()costrutto avvia una sotto-shell che esegue il comando ls -1t(elencando tutti i file in ordine di tempo, uno per riga) e eseguendo il piping attraverso head -1per ottenere la prima riga (file).

L'output di quel comando (il file più recente) viene quindi passato tailper essere elaborato.

Tieni presente che questo corre il rischio di ottenere una directory se questa è la voce di directory più recente creata. Ho usato quel trucco in un alias per modificare il file di registro più recente (da un set rotante) in una directory che conteneva solo quei file di registro.


Non -1è necessario, lo lsfa per te quando è in una pipa. Confronta lse ls|cat, per esempio.
In pausa fino a nuovo avviso.

Questo potrebbe essere il caso di Linux. In Unix "vero", i processi non hanno cambiato il loro comportamento in base a dove stava andando il loro output. Ciò renderebbe davvero fastidioso il debug della pipeline :-)

Hmmm, non sono sicuro che sia corretto - ISTR deve emettere "ls -C" per ottenere l'output formattato in colonna in 4.2BSD quando si esegue il piping dell'output attraverso un filtro, e sono abbastanza sicuro che in Solaris funzioni allo stesso modo. Qual è comunque il "One, True Unix"?

Citazioni! Citazioni! I nomi dei file contengono spazi!
Norman Ramsey,

@TMN: L'unico vero modo Unix non è fare affidamento su ls per i consumatori non umani. "Se l'output è su un terminale, il formato è definito dall'implementazione." - questa è la specifica. Se vuoi essere sicuro di dover dire ls -1 o ls -C.
Phogg

4

Sui sistemi POSIX, non è possibile ottenere la voce della directory "ultima creazione". Ogni voce della directory ha atime, mtimee ctime, contrariamente a Microsoft Windows, ctimenon significa CreationTime, ma "Ora dell'ultimo cambio di stato".

Quindi il meglio che puoi ottenere è "accodare l'ultimo file modificato di recente", che è spiegato nelle altre risposte. Vorrei andare per questo comando:

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

Nota le virgolette attorno al lscomando. Questo fa funzionare lo snippet con quasi tutti i nomi di file.


Bel lavoro. Dritto al punto. +1
Norman Ramsey

4

Voglio solo vedere la modifica della dimensione del file che puoi usare watch.

watch -d ls -l


1

Probabilmente ci sono milioni di modi per farlo, ma il modo in cui lo farei è questo:

tail `ls -t | head -n 1`

I bit tra i backtick (la virgoletta come i caratteri) vengono interpretati e il risultato restituito alla coda.

ls -t #gets the list of files in time order
head -n 1 # returns the first line only

2
I bastoncini sono malvagi. Utilizzare invece $ ().
William Pursell

1

Un semplice:

tail -f /path/to/directory/*

funziona bene per me.

Il problema è ottenere file che vengono generati dopo aver avviato il comando tail. Ma se non ne hai bisogno (dal momento che tutte le soluzioni precedenti non se ne occupano), l'asterisco è solo una soluzione più semplice, IMO.



0

Qualcuno l'ha pubblicato e poi lo ha cancellato per qualche motivo, ma questo è l'unico che funziona, quindi ...

tail -f `ls -tr | tail`

devi escludere le directory, vero?
Amit

1
L'ho pubblicato originariamente ma l'ho eliminato poiché concordo con Sorpigal che l'analisi dell'output lsnon è la cosa più intelligente da fare ...
ChristopheD

Ne ho bisogno veloce e sporco, senza directory. Quindi, se aggiungerai la tua risposta, accetterò quella
Itay Moav -Malimovka

0
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`

Spiegazione:

  • ls -lt: elenco di tutti i file e le directory ordinati per ora di modifica
  • grep -v ^ d: esclude le directory
  • dalla testa -2 in poi: analizzando il nome file necessario

1
+1 per intelligente, -2 per l'analisi dell'output ls, -1 per non quotare la subshell, -1 per un'ipotesi "campo 8" magica (non è portatile!) E infine -1 per troppo intelligente . Punteggio complessivo: -4.
phogg

@Sorpigal Concordato. Felice di essere il cattivo esempio però.
Amit

sì, non immaginavo che sarebbe stato sbagliato su così tanti
aspetti

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.