wie man die neueste Datei in einem Verzeichnis "abfängt"

20

Wie kann ich in tailder Shell die neueste Datei in einem Verzeichnis erstellen?

Itay Moav-Malimovka
quelle
1
Komm schon Schließer, Programmierer müssen Schluss machen!
amit
Das Schließen ist nur für das Verschieben zu Superuser oder Serverfehlern vorgesehen. Die Frage wird dort leben und mehr Leute, die interessiert sein könnten, werden sie finden.
Mnementh
Das eigentliche Problem dabei ist, die zuletzt aktualisierte Datei im Verzeichnis zu finden, und ich glaube, dass dies bereits beantwortet wurde (entweder hier oder beim Superuser, an den ich mich nicht erinnern kann).
dmckee

Antworten:

24

Analysieren Sie nicht die Ausgabe von ls! Das Parsen der Ausgabe von ls ist schwierig und unzuverlässig .

Wenn Sie dies tun müssen, empfehle ich find. Ursprünglich hatte ich hier nur ein einfaches Beispiel, um Ihnen den Kern der Lösung zu erläutern. Da diese Antwort jedoch recht beliebt zu sein scheint, habe ich mich dazu entschlossen, sie zu überarbeiten, um eine Version bereitzustellen, die sicher kopiert / eingefügt und mit allen Eingaben verwendet werden kann. Sitzen Sie bequem? Wir beginnen mit einem Oneliner, der Ihnen die neueste Datei im aktuellen Verzeichnis liefert:

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

Nicht gerade ein Oneliner, oder? Hier ist es wieder als Shell-Funktion und zum leichteren Lesen formatiert:

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
}

Und nun , dass als oneliner:

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

Wenn alles andere fehlschlägt, können Sie die oben genannte Funktion in Ihre Liste aufnehmen .bashrcund das Problem mit einer Einschränkung als gelöst betrachten. Wenn Sie nur die Arbeit erledigen möchten, müssen Sie nicht weiter lesen.

Die Einschränkung dabei ist, dass ein Dateiname, der auf eine oder mehrere Zeilenumbrüche endet, immer noch nicht tailkorrekt übergeben wird. Die Umgehung dieses Problems ist kompliziert, und ich halte es für ausreichend, dass bei Auftreten eines solchen böswilligen Dateinamens das relativ sichere Verhalten eines "No such file" -Fehlers anstelle eines gefährlicheren auftritt.

Saftige Details

Für die Neugierigen ist dies die mühsame Erklärung, wie es funktioniert, warum es sicher ist und warum andere Methoden wahrscheinlich nicht.

Gefahr, Will Robinson

Erstens ist das einzige Byte, das Dateipfade sicher abgrenzen kann, null, da es das einzige Byte ist, das in Dateipfaden auf Unix-Systemen allgemein verboten ist. Wenn Sie eine Liste von Dateipfaden behandeln, ist es wichtig, nur null als Begrenzer zu verwenden und dies auch dann, wenn Sie einen einzelnen Dateipfad von einem Programm zu einem anderen übergeben, auf eine Weise, die keine willkürlichen Bytes beeinträchtigt. Es gibt viele scheinbar richtige Möglichkeiten, um dieses und andere Probleme zu lösen, die fehlschlagen, wenn (auch aus Versehen) angenommen wird, dass in Dateinamen weder neue Zeilen noch Leerzeichen enthalten sind. Keine der beiden Annahmen ist sicher.

Für die heutigen Zwecke besteht der erste Schritt darin, eine durch Nullen getrennte Liste der gefundenen Dateien zu erstellen. Dies ist ziemlich einfach, wenn Sie eine findUnterstützung -print0wie GNUs haben:

find . -print0

Diese Liste sagt uns jedoch immer noch nicht, welches das neueste ist, daher müssen wir diese Informationen einbeziehen. Ich verwende den -printfSchalter find , mit dem ich festlegen kann, welche Daten in der Ausgabe angezeigt werden. Nicht alle Versionen der findUnterstützung -printf(dies ist kein Standard), GNU find jedoch. Wenn Sie sich ohne befinden, müssen -printfSie sich darauf verlassen, -exec stat {} \;an welchem ​​Punkt Sie alle Hoffnung auf Portabilität aufgeben müssen, die auch statnicht Standard ist. Im Moment gehe ich davon aus, dass Sie GNU-Tools haben.

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

Hier frage ich nach dem printf-Format, bei %T@dem es sich um die Änderungszeit in Sekunden seit Beginn der Unix-Epoche handelt, gefolgt von einem Punkt und einer Zahl, die Sekundenbruchteile angibt. Ich füge dazu einen weiteren Punkt und dann %p(das ist der vollständige Pfad zur Datei) hinzu, bevor ich mit einem Null-Byte abschließe.

Jetzt habe ich

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

Es mag selbstverständlich sein, aber aus Gründen der Vollständigkeit wird -maxdepth 1verhindert , dass findder Inhalt von Unterverzeichnissen \! -type daufgelistet wird, und es werden Verzeichnisse übersprungen, die Sie wahrscheinlich nicht möchten tail. Bisher habe ich Dateien im aktuellen Verzeichnis mit Informationen zum Änderungszeitpunkt, daher muss ich jetzt nach diesem Änderungszeitpunkt sortieren.

Es in die richtige Reihenfolge bringen

Standardmäßig werden für sortdie Eingabe durch Zeilenumbrüche getrennte Datensätze erwartet. Wenn Sie GNU haben sort, können Sie es bitten, durch Null getrennte Datensätze zu erwarten, indem Sie den -zSchalter verwenden .; für standard sortgibt es keine lösung. Ich bin nur daran interessiert, nach den ersten beiden Zahlen (Sekunden und Sekundenbruchteilen) zu sortieren, und möchte nicht nach dem tatsächlichen Dateinamen sortieren. Deshalb sage ich sortzwei Dinge: Erstens sollte der Punkt ( .) ein Feldtrennzeichen sein und zweitens, dass es nur das erste und das zweite Feld verwenden sollte, wenn darüber nachgedacht wird, wie die Datensätze sortiert werden.

| sort -znr -t. -k1,2

Zunächst bündele ich drei kurze Optionen, die zusammen keinen Wert haben. -znrist nur eine prägnante Redewendung -z -n -r). Danach -t .(das Leerzeichen ist optional) teilt sortdas Feldtrennzeichen mit und -k 1,2gibt die Feldnummern an: first und second ( sortzählt Felder von eins, nicht von null). Denken Sie daran, dass ein Beispieldatensatz für das aktuelle Verzeichnis wie folgt aussehen würde:

1000000000.0000000000../some-file-name

Dies bedeutet, sortwird zuerst 1000000000und dann 0000000000bei der Bestellung dieses Datensatzes suchen . Die -nOption gibt sortan, dass beim Vergleichen dieser Werte ein numerischer Vergleich verwendet werden soll, da beide Werte Zahlen sind. Dies ist möglicherweise nicht wichtig, da die Nummern eine feste Länge haben, aber es schadet nicht.

Der andere Schalter sortist -rfür "Rückwärts". Standardmäßig wird bei der Ausgabe einer numerischen Sortierung die niedrigste Zahl zuerst ausgegeben. -rSie wird so geändert, dass die niedrigste und die höchste Zahl zuerst aufgelistet werden. Da diese Zahlen Zeitstempel sind, bedeutet dies, dass sie neuer sind und der neueste Datensatz am Anfang der Liste steht.

Nur die wichtigen Teile

Da die Liste der Dateipfade sortnun die gewünschte Antwort enthält, suchen wir ganz oben. Was bleibt, ist eine Möglichkeit zu finden, die anderen Datensätze zu verwerfen und den Zeitstempel zu entfernen. Leider akzeptieren auch GNU headund tailSwitches keine, damit sie mit null-getrennten Eingaben arbeiten. Stattdessen benutze ich eine while-Schleife als eine Art armer Mann head.

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

Zuerst habe ich deaktiviert, IFSdamit die Dateiliste nicht der Wortteilung unterworfen wird. Als nächstes erzähle ich readzwei Dinge: Interpretiere keine Escape-Sequenzen in der Eingabe ( -r) und die Eingabe wird mit einem Null-Byte ( -d) begrenzt; Hier wird die leere Zeichenfolge ''verwendet, um "kein Trennzeichen", auch durch null getrennt, anzugeben. Jeder Datensatz wird in die Variable eingelesen, recordsodass sie bei jedem whileDurchlauf einen einzelnen Zeitstempel und einen einzelnen Dateinamen hat. Beachten Sie, dass dies -deine GNU-Erweiterung ist. Wenn Sie nur einen Standard readhaben, funktioniert diese Technik nicht und Sie haben wenig Rückgriff.

Wir wissen, dass die recordVariable aus drei Teilen besteht, die alle durch Punkte getrennt sind. Mit dem cutDienstprogramm ist es möglich, einen Teil davon zu extrahieren.

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

Hier wird der gesamte Datensatz an printfund von dort weitergeleitet cut; In Bash können Sie dies mit einem Here-String weiter vereinfachen , cut -d. -3f- <<<"$record"um eine bessere Leistung zu erzielen. Wir sagen cutzwei Dinge: Erstens -dsollte dabei ein bestimmtes Trennzeichen zur Identifikation von Feldern (wie beim sortTrennzeichen .) verwendet werden. Second cutwird angewiesen -f, nur Werte aus bestimmten Feldern zu drucken. Die Feldliste wird als Bereich 3-angegeben, der den Wert aus dem dritten Feld und aus allen folgenden Feldern angibt. Dies bedeutet, dass cutalles bis einschließlich der Sekunde ., die im Datensatz gefunden wird, gelesen und ignoriert wird und dann der Rest gedruckt wird, der der Dateipfadteil ist.

Nachdem Sie den neuesten Dateipfad gedruckt haben, müssen Sie nicht mehr weitermachen: breakVerlassen Sie die Schleife, ohne den zweiten Dateipfad aufzurufen.

Das einzige, was übrig bleibt, ist die Ausführung tailauf dem von dieser Pipeline zurückgegebenen Dateipfad. In meinem Beispiel haben Sie vielleicht bemerkt, dass ich dies getan habe, indem ich die Pipeline in eine Subshell eingeschlossen habe. Was Sie vielleicht nicht bemerkt haben, ist, dass ich die Subshell in doppelte Anführungszeichen gesetzt habe. Dies ist wichtig, da eine nicht zitierte Subshell-Erweiterung trotz all dieser Bemühungen, für alle Dateinamen sicher zu sein, letztendlich immer noch zu Problemen führen kann. Eine ausführlichere Erklärung finden Sie bei Interesse. Der zweite wichtige, aber leicht zu übersehende Aspekt beim Aufruf von tailist, dass ich ihm die Option gegeben habe --, bevor ich den Dateinamen erweitert habe. Dies wird anweisentaildass keine weiteren Optionen angegeben werden und alles, was folgt, ein Dateiname ist, wodurch es sicher ist, Dateinamen zu behandeln, die mit beginnen -.

phogg
quelle
1
@AakashM: weil Sie möglicherweise "überraschende" Ergebnisse erhalten, z. B. wenn der Name einer Datei "ungewöhnliche" Zeichen enthält (fast alle Zeichen sind zulässig).
John Zwinck
6
Leute, die Sonderzeichen in ihren Dateinamen verwenden, verdienen alles, was sie bekommen :-)
6
Zu sehen, wie paxdiablo diese Bemerkung machte, war schmerzhaft genug, aber dann stimmten zwei Leute dafür! Leute, die fehlerhafte Software schreiben, verdienen absichtlich alles, was sie bekommen.
John Zwinck
4
Die obige Lösung funktioniert auf osx nicht, da die Option -printf in find fehlt. Die folgende Lösung funktioniert jedoch nur auf osx, da sich der Befehl stat unterscheidet. Vielleicht hilft sie trotzdem jemandemtail -f $(find . -type f -exec stat -f "%m {}" {} \;| sort -n | tail -n 1 | cut -d ' ' -f 2)
audio.zoom
2
"Leider akzeptieren auch GNU headund tailSwitches keine, damit sie mit null-getrennten Eingaben arbeiten." Mein Ersatz für head: … | grep -zm <number> "".
Kamil Maciorowski
22
tail `ls -t | head -1`

Wenn Sie über Dateinamen mit Leerzeichen besorgt sind,

tail "`ls -t | head -1`"
Spitze
quelle
1
Aber was passiert, wenn Ihre neueste Datei Leerzeichen oder Sonderzeichen enthält? Verwenden Sie $ () anstelle von `` und zitieren Sie Ihre Subshell, um dieses Problem zu vermeiden.
Phogg
Ich mag das. Sauber und einfach. So wie es sein sollte.
6
Es ist einfach, sauber und einfach zu sein, wenn man robust und korrekt opfert.
Phogg
2
Nun, es kommt wirklich darauf an, was du tust. Eine Lösung, die immer und überall funktioniert, für alle möglichen Dateinamen, ist sehr nett, aber in einer eingeschränkten Situation (Protokolldateien, zum Beispiel mit bekannten, nicht sonderbaren Namen) kann es unnötig sein.
Dies ist die bisher sauberste Lösung. Vielen Dank!
Demisx
4

Sie können verwenden:

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

Das $()Konstrukt startet eine Sub-Shell, die den Befehl ls -1tausführt (alle Dateien in zeitlicher Reihenfolge, eine pro Zeile) und diese weiterleitet, head -1um die erste Zeile (Datei) zu erhalten.

Die Ausgabe dieses Befehls (die neueste Datei) wird dann tailzur Verarbeitung übergeben.

Beachten Sie, dass dies das Risiko birgt, ein Verzeichnis zu erhalten, wenn dies der zuletzt erstellte Verzeichniseintrag ist. Ich habe diesen Trick in einem Alias ​​verwendet, um die neueste Protokolldatei (aus einem rotierenden Satz) in einem Verzeichnis zu bearbeiten, das nur diese Protokolldateien enthält.


quelle
Das -1ist nicht nötig, lsmacht das für dich, wenn es in einer Pipe ist. Vergleichen lsund ls|catzum Beispiel.
Bis auf weiteres angehalten.
Das kann unter Linux der Fall sein. In "echtem" Unix änderten Prozesse ihr Verhalten nicht, je nachdem, wohin ihre Ausgabe ging. Das würde das Debuggen von Pipelines sehr ärgerlich machen :-)
Hmmm, nicht sicher, ob das richtig ist - ISTR muss "ls -C" ausgeben, um eine spaltenformatierte Ausgabe unter 4.2BSD zu erhalten, wenn die Ausgabe durch einen Filter geleitet wird, und ich bin mir ziemlich sicher, dass ls unter Solaris genauso funktioniert. Was ist eigentlich "ein, wahres Unix"?
Zitate! Zitate! Dateinamen enthalten Leerzeichen!
Norman Ramsey
@TMN: Der einzig wahre Unix-Weg ist, sich nicht auf ls für nicht menschliche Konsumenten zu verlassen. "Wenn die Ausgabe an ein Terminal erfolgt, ist das Format implementierungsdefiniert." - Das ist die Spezifikation. Wenn Sie sicher sein wollen, müssen Sie ls -1 oder ls -C sagen.
Phogg
4

Auf POSIX-Systemen kann der zuletzt erstellte Verzeichniseintrag nicht abgerufen werden. Jeder Verzeichniseintrag hat atime, mtimeund ctimeim Gegensatz zu Microsoft Windows, ctimenicht CreationTime, sondern "Zeitpunkt der letzten Statusänderung".

Das Beste, was Sie bekommen können, ist, "die zuletzt geänderte Datei zu beschneiden", was in den anderen Antworten erklärt wird. Ich würde für diesen Befehl gehen:

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

Beachten Sie die Anführungszeichen um den lsBefehl. Dadurch funktioniert das Snippet mit fast allen Dateinamen.

Roland Illig
quelle
Gute Arbeit. Auf den Punkt. +1
Norman Ramsey
4

Ich möchte nur die Dateigrößenänderung sehen, die Sie beobachten können.

watch -d ls -l
tpal
quelle
3

In zsh:

tail *(.om[1])

Siehe: http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers , hier wird mdie Änderungszeit angegeben m[Mwhms][-|+]n, und das Vorhergehende obedeutet, dass die Sortierung auf die eine Weise erfolgt ( OSortierung auf die andere Weise). Das .bedeutet nur normale Dateien. In den Klammern wird [1]der erste Eintrag ausgewählt. Drei verwenden [1,3], um die älteste Verwendung zu erhalten [-1].

Es ist schön kurz und wird nicht verwendet ls.

Anne van Rossum
quelle
1

Es gibt wahrscheinlich eine Million Möglichkeiten, dies zu tun, aber ich würde es folgendermaßen tun:

tail `ls -t | head -n 1`

Die Bits zwischen den Backticks (die zitatähnlichen Zeichen) werden interpretiert und das Ergebnis an tail zurückgegeben.

ls -t #gets the list of files in time order
head -n 1 # returns the first line only
Iblamefisch
quelle
2
Backticks sind böse. Verwenden Sie stattdessen $ ().
William Pursell
1

Eine einfache:

tail -f /path/to/directory/*

funktioniert gut für mich.

Das Problem besteht darin, Dateien abzurufen, die generiert werden, nachdem Sie den Befehl tail gestartet haben. Wenn Sie dies jedoch nicht benötigen (da sich alle oben genannten Lösungen nicht darum kümmern), ist das Sternchen nur eine einfachere Lösung, IMO.

bruno.braga
quelle
0
tail`ls -tr | tail -1`
user22644
quelle
Sie haben dort ein Leerzeichen vergessen!
Blacklight Shining
0

Jemand hat es gepostet und dann aus irgendeinem Grund gelöscht, aber dies ist die einzige, die funktioniert, also ...

tail -f `ls -tr | tail`
Itay Moav-Malimovka
quelle
Sie müssen Verzeichnisse ausschließen, nicht wahr?
amit
1
Ich habe dies ursprünglich gepostet, aber ich habe es gelöscht, da ich mit Sorpigal übereinstimme, dass das Parsen der Ausgabe von lsnicht die klügste Sache ist ...
ChristopheD
Ich brauche es schnell und dreckig, keine Verzeichnisse drin. Also, wenn Sie Ihre Antwort hinzufügen, werde ich diese akzeptieren
Itay Moav-Malimovka
0
tail -f `ls -lt | grep -v ^d | head -2 | tail -1 | tr -s " " | cut -f 8 -d " "`

Erläuterung:

  • ls -lt: Liste aller Dateien und Verzeichnisse sortiert nach Änderungszeit
  • grep -v ^ d: Verzeichnisse ausschließen
  • head -2 onwards: parst den benötigten Dateinamen
amit
quelle
1
+1 für clever, -2 für das Parsen der ls-Ausgabe, -1 für das Nicht-Zitieren der Subshell, -1 für eine magische "Feld 8" -Annahme (nicht portierbar!) Und schließlich -1 für zu clever . Gesamtnote: -4.
Phogg
@Sorpigal Einverstanden. Glücklich, das schlechte Beispiel zu sein.
amit
Ja, ich hätte nicht
gedacht,
0
tail "$(ls -1tr|tail -1)"
user31894
quelle