Nella shell, come posso tail
l'ultimo file creato in una directory?
Nella shell, come posso tail
l'ultimo file creato in una directory?
Risposte:
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 .bashrc
e 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 tail
correttamente. 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.
Per i curiosi questa è la noiosa spiegazione di come funziona, perché è sicura e perché probabilmente altri metodi non lo sono.
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 find
supporto -print0
come GNU:
find . -print0
Ma questo elenco non ci dice ancora quale è il più recente, quindi è necessario includere tali informazioni. Ho scelto di utilizzare l' -printf
opzione find che mi consente di specificare quali dati vengono visualizzati nell'output. Non tutte le versioni di find
supporto -printf
(non è standard) ma GNU find lo fa. Se ti trovi senza di -printf
te, dovrai fare affidamento su -exec stat {} \;
quale punto devi rinunciare a tutte le speranze di portabilità, dato che stat
non è 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 1
impedisce find
di elencare i contenuti delle sottodirectory e \! -type d
salta 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.
Per impostazione predefinita, si sort
aspetta che i suoi input siano record delimitati da newline. Se hai GNU sort
puoi chiedergli di aspettarti record delimitati da null usando invece l' -z
opzione .; per standard sort
non 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 sort
due 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 sort
il carattere delimitatore di campo e -k 1,2
specifica i numeri di campo: primo e secondo ( sort
conta 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 sort
che guarderemo prima 1000000000
e poi 0000000000
quando ordiniamo questo record. L' -n
opzione dice sort
di 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
è -r
per "reverse". Per impostazione predefinita, l'output di un ordinamento numerico sarà prima il numero più basso, lo -r
modifica 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.
Poiché l'elenco dei percorsi dei file emerge da sort
esso 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 head
e tail
non 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 IFS
modo che l'elenco dei file non sia soggetto a suddivisione in parole. Successivamente dico read
due 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 record
modo che ogni volta che il while
ciclo itera abbia un singolo timestamp e un singolo nome di file. Nota che -d
è un'estensione GNU; se hai solo uno standard read
questa tecnica non funzionerà e avrai poca possibilità.
Sappiamo che la record
variabile ha tre parti, tutte delimitate da caratteri punto. Utilizzando l' cut
utilità è possibile estrarne una parte.
printf '%s' "$record" | cut -d. -f3-
Qui l'intero record viene passato printf
e 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 cut
due cose: prima di -d
tutto dovrebbe essere un delimitatore specifico per identificare i campi (come nel caso sort
del delimitatore .
). Al secondo cut
viene -f
richiesto 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 cut
leggerà 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: break
esce dal ciclo senza lasciarlo passare al secondo percorso del file.
L'unica cosa che rimane è in esecuzione tail
sul 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àtail
che 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 -
.
tail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
head
e tail
non accettano switch per farli funzionare su input delimitati da null." La mia sostituzione head
: … | grep -zm <number> ""
.
tail `ls -t | head -1`
Se sei preoccupato per i nomi di file con spazi,
tail "`ls -t | head -1`"
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 -1
per ottenere la prima riga (file).
L'output di quel comando (il file più recente) viene quindi passato tail
per 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.
-1
è necessario, lo ls
fa per te quando è in una pipa. Confronta ls
e ls|cat
, per esempio.
Sui sistemi POSIX, non è possibile ottenere la voce della directory "ultima creazione". Ogni voce della directory ha atime
, mtime
e ctime
, contrariamente a Microsoft Windows, ctime
non 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 ls
comando. Questo fa funzionare lo snippet con quasi tutti i nomi di file.
In zsh
:
tail *(.om[1])
Vedi: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers , qui m
indica il tempo di modifica m[Mwhms][-|+]n
e il precedente o
significa che è ordinato in un modo (lo O
ordina nell'altro modo). I .
mezzi solo i file regolari. Tra parentesi [1]
seleziona il primo elemento. Per scegliere tre usi [1,3]
, per ottenere l'uso più vecchio [-1]
.
È bello in breve e non usa ls
.
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
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.
Qualcuno l'ha pubblicato e poi lo ha cancellato per qualche motivo, ma questo è l'unico che funziona, quindi ...
tail -f `ls -tr | tail`
ls
non è la cosa più intelligente da fare ...
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`
Spiegazione: