Abrufen von Text vom letzten Marker zum EOF in POSIX.2

8

Ich habe einen Text mit Markierungslinien wie:

aaa
---
bbb
---
ccc

Ich muss einen Text von der letzten Markierung (nicht inklusive) an EOF senden. In diesem Fall wird es sein

ccc

Gibt es einen eleganten Weg in POSIX.2? Im Moment verwende ich zwei Läufe: erstens mit nlund grepfür das letzte Vorkommen mit der jeweiligen Zeilennummer. Dann extrahiere ich die Zeilennummer und verwende sie sed, um den fraglichen Block zu extrahieren.

Die Textsegmente können ziemlich groß sein, daher habe ich Angst, eine Methode zum Hinzufügen von Text zu verwenden, wie wir den Text einem Puffer hinzufügen. Wenn wir auf die Markierung stoßen, leeren wir den Puffer, so dass wir bei EOF unseren letzten Block im haben Puffer.

aikipooh
quelle

Antworten:

6

Wenn Ihre Segmente nicht wirklich riesig sind (wie in: Sie können wirklich nicht so viel RAM sparen, vermutlich weil dies ein winziges eingebettetes System ist, das ein großes Dateisystem steuert), ist ein einziger Durchgang wirklich der bessere Ansatz. Nicht nur, weil es schneller sein wird, sondern vor allem, weil die Quelle ein Stream sein kann, aus dem alle gelesenen und nicht gespeicherten Daten verloren gehen. Dies ist wirklich ein Job für awk, obwohl sed es auch kann.

sed -n -e 's/^---$//' -e 't a' \
       -e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
       -e ':a' -e 'h'              # you are not expected to understand this
awk '{if (/^---$/) {chunk=""}      # separator ==> start new chunk
      else {chunk=chunk $0 RS}}    # append line to chunk
     END {printf "%s", chunk}'     # print last chunk (without adding a newline)

Wenn Sie einen Zwei-Pass-Ansatz verwenden müssen, bestimmen Sie den Zeilenversatz des letzten Trennzeichens und drucken Sie daraus. Oder bestimmen Sie den Byte-Offset und drucken Sie daraus.

</input/file tail -n +$((1 + $(</input/file         # print the last N lines, where N=…
                               grep -n -e '---' |   # list separator line numbers
                               tail -n 1 |          # take the last one
                               cut -d ':' -f 1) ))  # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
    {pos+=length($0 RS)}        # pos contains the current byte offset in the file
    /^---$/ {last=pos}          # last contains the byte offset after the last separator
    END {print last+1}          # print characters from last (+1 because tail counts from 1)
')

Nachtrag: Wenn Sie mehr als POSIX haben, finden Sie hier eine einfache One-Pass-Version, die auf einer gemeinsamen Erweiterung von awk basiert, mit der das Datensatztrennzeichen RSein regulärer Ausdruck sein kann (POSIX lässt nur ein einziges Zeichen zu). Es ist nicht ganz richtig: Wenn die Datei mit einem Datensatztrennzeichen endet, wird der Block vor dem letzten Datensatztrennzeichen anstelle eines leeren Datensatzes gedruckt. Die zweite Version, die verwendet wird, RTvermeidet diesen Fehler, RTist jedoch spezifisch für GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
Gilles 'SO - hör auf böse zu sein'
quelle
@ Gilles: sedfunktioniert gut, aber ich kann das awkBeispiel nicht zum Laufen bringen; es hängt ... und ich erhalte eine Fehlermeldung im 3. Beispiel: cut -f ':' -t 1 ... cut: ungültige Option - 't'
Peter.O
@ fred.bear: Ich habe keine Ahnung, wie das passiert ist - ich habe alle meine Schnipsel getestet, aber irgendwie die Bearbeitung nach dem Kopieren und Einfügen im cutBeispiel durcheinander gebracht . Ich sehe nichts Falsches an dem awkBeispiel, welche Version von awk verwenden Sie und was ist Ihre Testeingabe?
Gilles 'SO - hör auf böse zu sein'
... eigentlich awkfunktioniert die Version .. es dauert nur sehr lange bei einer großen Datei .. die sedVersion hat dieselbe Datei in 0.470s verarbeitet .. Meine Testdaten sind sehr gewichtet ... nur zwei Chunks mit einem einsamen '---' drei Zeilen vom Ende von 1 Million Zeilen ...
Peter.O
@ Gilles .. (Ich denke, ich sollte um 3 Uhr morgens aufhören zu testen. Ich habe irgendwie alle drei "Two Pass" -Waks als eine Einheit getestet :( ... Ich habe jetzt jede einzeln getestet und die zweite ist sehr schnell bei 0,204 Sekunden ... JEDOCH gibt der erste "Zwei-Durchgang" -Wak nur aus: " (Standardeingabe) " (das -l scheint der Schuldige zu sein) ... wie für den dritten "Zwei-Durchgang" -Wak tue ich nicht
Ich gebe
@ fred.bear: Behoben und behoben. Meine Qualitätssicherung ist für diese kurzen Schnipsel nicht sehr gut. Normalerweise kopiere ich sie über eine Befehlszeile, formatiere sie, bemerke dann einen Fehler und versuche, Inline zu beheben, anstatt sie neu zu formatieren. Ich bin gespannt, ob das Zählen von Zeichen effizienter ist als das Zählen von Zeilen (2. gegen 3. Zwei-Pass-Methode).
Gilles 'SO - hör auf böse zu sein'
3

Eine Zwei-Pass-Strategie scheint das Richtige zu sein. Anstelle von sed würde ich verwenden awk(1). Die beiden Pässe könnten so aussehen:

$ LINE=`awk '/^---$/{n=NR}END{print n}' file`

um die Zeilennummer zu erhalten. Und dann den gesamten Text ab dieser Zeilennummer wiedergeben mit:

$ awk "NR>$LINE" file

Dies sollte keine übermäßige Pufferung erfordern.

Mackie Messer
quelle
und dann können sie kombiniert werden:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
Glenn Jackman
Da ich die anderen Einsendungen schon einmal getestet habe, habe ich jetzt auch "Glen Jackman's" über dem Snippet getestet. Es dauert 0,352 Sekunden (mit der gleichen Datendatei, die in meiner Antwort erwähnt wurde) ... Ich bekomme allmählich die Meldung, dass awk schneller sein kann, als ich ursprünglich für möglich gehalten hatte (ich dachte, sed sei ungefähr so ​​gut wie es war, aber es scheint ein Fall von "Pferden für Kurse" zu sein) ...
Peter.O
Sehr interessant, all diese Skripte im Benchmark zu sehen. Gute Arbeit Fred.
Mackie Messer
Die schnellsten Lösungen verwenden tac und tail, die die Eingabedatei tatsächlich rückwärts lesen. Wenn nur awk die Eingabedatei rückwärts lesen könnte ...
Mackie Messer
3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

Die erste sedAusgabe gibt die Zeilennummern der "---" Zeilen aus ...
Die zweite sedextrahiert die letzte Nummer aus der Ausgabe des ersten Sed ...
Addieren Sie 1 zu dieser Nummer, um den Start Ihres "ccc" -Blocks zu erhalten ...
Die dritte 'sed' gibt vom Anfang des "ccc" -Blocks an EOF aus

Update (mit geänderten Informationen zu Gilles-Methoden)

Nun, ich habe darüber nachgedacht, wie sich Glenn Jackmans verhalten tac würde, also habe ich die drei Antworten (zum Zeitpunkt des Schreibens) zeitgetestet ... Die Testdatei (en) enthielten jeweils 1 Million Zeilen (mit ihren eigenen Zeilennummern).
Alle Antworten haben das getan, was erwartet wurde ...

Hier sind die Zeiten ..


Gilles sed (Einzelpass)

# real    0m0.470s
# user    0m0.448s
# sys     0m0.020s

Gilles awk (Einzelpass)

# very slow, but my data had a very large data block which awk needed to cache.

Gilles 'Zwei-Pass' (erste Methode)

# real    0m0.048s
# user    0m0.052s
# sys     0m0.008s

Gilles 'Two-Pass' (zweite Methode) ... sehr schnell

# real    0m0.204s
# user    0m0.196s
# sys     0m0.008s

Gilles 'Zwei-Pass' (dritte Methode)

# real    0m0.774s
# user    0m0.688s
# sys     0m0.012s

Gilles 'gawk' (RT-Methode) ... sehr schnell , aber nicht POSIX.

# real    0m0.221s
# user    0m0.200s
# sys     0m0.020s

Glenn Jackman ... sehr schnell , ist aber nicht POSIX.

# real    0m0.022s
# user    0m0.000s
# sys     0m0.036s

fred.bear

# real    0m0.464s
# user    0m0.432s
# sys     0m0.052s

Mackie Messer

# real    0m0.856s
# user    0m0.832s
# sys     0m0.028s
Peter.O
quelle
Welche meiner Zwei-Pass-Versionen haben Sie aus Neugier getestet und welche Version von awk haben Sie verwendet?
Gilles 'SO - hör auf böse zu sein'
@ Gilles: Ich habe GNU Awk 3.1.6 verwendet (in Ubuntu 10.04 mit 4 GB RAM). Alle Tests haben 1 Million Zeilen im ersten "Block", dann einen "Marker", gefolgt von 2 "Daten" -Zeilen ... Es dauerte 15,540 Sekunden, um eine kleinere Datei mit 100.000 Zeilen zu verarbeiten, aber für die 1.000.000 Zeilen bin ich Jetzt läuft es und es sind mehr als 25 Minuten vergangen. Es verwendet einen Kern zu 100% ... tötet ihn jetzt ... Hier sind einige weitere inkrementelle Tests: lines = 100000 (0m16.026s) - lines = 200000 (2m29.990s) - lines = 300000 (5m23). 393s) - Zeilen = 400000 (11m9.938s)
Peter.O
Hoppla. In meinem obigen Kommentar habe ich Ihre "Two-Pass" -Wak-Referenz verpasst. Das obige Detail gilt für das "Single-Pass" -Waw ... Die Awk-Version ist korrekt ... Ich habe weitere Kommentare zu den verschiedenen "Two-Pass" -Versionen unter Ihrer Antwort abgegeben (eine Änderung der Zeitergebnisse oben)
Peter.O
2

Verwenden Sie " tac ", das die Zeilen einer Datei von Ende bis Anfang ausgibt:

tac afile | awk '/---/ {exit} {print}' | tac
Glenn Jackman
quelle
tacist nicht POSIX, sondern Linux-spezifisch (in GNU-Coreutils und in einigen Busybox-Installationen).
Gilles 'SO - hör auf böse zu sein'
0

Sie könnten einfach verwenden ed

ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN

So funktioniert es: tdupliziert die aktuelle ( .) Zeile - die beim edStart immer die letzte Zeile ist (nur für den Fall, dass das Trennzeichen in der letzten Zeile vorhanden ist), 1,?===?dlöscht alle Zeilen bis einschließlich der vorherigen Übereinstimmung ( edbefindet sich noch in der letzten Zeile) ) $dlöscht dann die (doppelte) letzte Zeile, ,pdruckt den Textpuffer (ersetzen durch w, um die Datei an Ort und Stelle zu bearbeiten) und wird schließlich qbeendet ed.


Wenn Sie wissen, dass die Eingabe mindestens ein Trennzeichen enthält (und es Ihnen egal ist, ob es auch gedruckt wird), dann

sed 'H;/===/h;$!d;x' infile

wäre das kürzere.
So funktioniert es: Es hängt alle Zeilen an den Halten Puffer an, überschreibt den halten Puffer, wenn eine Übereinstimmung auftritt, löscht dalle Zeilen außer der letzten, $wenn es xdie Puffer (und Autoprints) wechselt.

don_crissti
quelle