sed: Liest die gesamte Datei in den Musterbereich, ohne bei einzeiliger Eingabe zu versagen

9

Das Einlesen einer ganzen Datei in den Musterbereich ist nützlich, um Zeilenumbrüche usw. zu ersetzen. und es gibt viele Fälle, in denen Folgendes empfohlen wird:

sed ':a;N;$!ba; [commands...]'

Es schlägt jedoch fehl, wenn die Eingabe nur eine Zeile enthält.

Bei einer Eingabe mit zwei Zeilen wird beispielsweise jede Zeile dem Ersetzungsbefehl unterzogen:

$ echo $'abc\ncat' | sed ':a;N;$!ba; s/a/xxx/g'
xxxbc
cxxxt

Bei der Eingabe einer einzelnen Zeile wird jedoch keine Ersetzung durchgeführt:

$ echo 'abc' | sed ':a;N;$!ba; s/a/xxx/g'
abc

Wie schreibt man einen sedBefehl, um alle Eingaben auf einmal einzulesen und dieses Problem nicht zu haben?

dicktyr
quelle
Ich habe Ihre Frage so bearbeitet, dass sie eine aktuelle Frage enthält. Sie können auf andere Antworten warten, wenn Sie möchten, aber eventuell die beste Antwort als akzeptiert markieren (siehe die Pipe-Schaltfläche links neben der Antwort rechts unter den Aufwärts-Abwärts-Pfeiltasten).
John1024
@ John1024 Danke, schön ein Beispiel zu haben. Wenn ich so etwas finde, erinnere ich mich eher daran, dass "alles falsch ist", aber ich bin froh, dass einige von uns nicht aufgeben. :}
dicktyr
2
Es gibt eine dritte Option! Verwenden Sie die sed -zOption von GNU . Wenn Ihre Datei nicht null hat, wird sie bis zum Ende der Datei gelesen! Gefunden von diesem: stackoverflow.com/a/30049447/582917
CMCDragonkai

Antworten:

12

Es gibt viele Gründe, warum das Einlesen einer ganzen Datei in den Musterbereich schief gehen kann. Das logische Problem in der Frage um die letzte Zeile ist ein häufiges. Es hängt mit dem sedZeilenzyklus zusammen - wenn keine Zeilen mehr vorhanden sind und sedEOF angetroffen wird, wird die Verarbeitung beendet. Wenn Sie also in der letzten Zeile stehen und anweisen sed, eine andere zu bekommen, wird sie genau dort anhalten und nichts mehr tun.

Das heißt, wenn Sie wirklich eine ganze Datei in den Musterbereich lesen müssen, lohnt es sich wahrscheinlich, ein anderes Tool in Betracht zu ziehen. Tatsache ist, sedist gleichbedeutend mit dem Stream- Editor - er ist so konzipiert, dass er jeweils eine Zeile oder einen logischen Datenblock bearbeitet.

Es gibt viele ähnliche Tools, die besser für die Verarbeitung vollständiger Dateiblöcke geeignet sind. edund exzum Beispiel können sie viel von dem sedtun, was sie können, und zwar mit ähnlicher Syntax - und noch viel mehr -, aber anstatt nur einen Eingabestream zu bearbeiten, während er wie ausgegeben in eine Ausgabe umgewandelt sedwird, verwalten sie auch temporäre Sicherungsdateien im Dateisystem . Ihre Arbeit wird nach Bedarf auf die Festplatte gepuffert, und sie werden am Ende der Datei nicht abrupt beendet (und implodieren unter Pufferbelastung viel seltener) . Darüber hinaus bieten sie viele nützliche Funktionen, die sedin einem Stream-Kontext einfach nicht sinnvoll sind, wie Linienmarkierungen, Rückgängigmachen, benannte Puffer, Verknüpfungen und mehr.

sedDie Hauptstärke liegt in der Fähigkeit, Daten zu verarbeiten, sobald sie gelesen werden - schnell, effizient und im Stream. Wenn Sie eine Datei schlürfen, werfen Sie diese weg, und es treten häufig Randprobleme wie das zuletzt erwähnte Zeilenproblem, Pufferüberläufe und eine miserable Leistung auf. Wenn die analysierten Daten bei der Aufzählung von Übereinstimmungen länger werden, wird die Verarbeitungszeit einer Regexp-Engine länger steigt exponentiell an .

In Bezug auf diesen letzten Punkt übrigens: Obwohl ich verstehe, dass der Beispielfall s/a/A/gsehr wahrscheinlich nur ein naives Beispiel ist und wahrscheinlich nicht das eigentliche Skript ist, für das Sie eine Eingabe sammeln möchten, lohnt es sich möglicherweise, sich mit ihm vertraut zu machen y///. Wenn Sie häufig feststellen, dass Sie gein einzelnes Zeichen durch ein anderes ersetzen, ykann dies für Sie sehr nützlich sein. Es ist eine Transformation im Gegensatz zu einer Substitution und geht viel schneller, da es keinen regulären Ausdruck impliziert. Dieser letztere Punkt kann auch nützlich sein, wenn versucht wird, leere //Adressen beizubehalten und zu wiederholen , da er sie nicht betrifft, aber von ihnen beeinflusst werden kann. In jedem Fall y/a/A/ist dies ein einfacheres Mittel, um dasselbe zu erreichen - und Swaps sind ebenso möglich wie:y/aA/Aa/ Dies würde alle Groß- / Kleinbuchstaben wie in einer Zeile gegeneinander austauschen.

Sie sollten auch beachten, dass das Verhalten, das Sie beschreiben, wirklich nicht das ist, was sowieso passieren soll.

Von GNUs info sedim Abschnitt GEMEINSAM BERICHTETE BUGS :

  • N Befehl in der letzten Zeile

    • Die meisten Versionen sedbeenden, ohne etwas zu drucken, wenn der NBefehl in der letzten Zeile einer Datei ausgegeben wird. GNU seddruckt den Musterbereich vor dem Beenden, es sei denn, der -nBefehlsschalter wurde angegeben. Diese Wahl ist beabsichtigt.

    • Zum Beispiel sed N foo barwürde das Verhalten von davon abhängen, ob foo eine gerade oder eine ungerade Anzahl von Zeilen hat. Wenn Sie ein Skript schreiben, um die nächsten Zeilen nach einer Musterübereinstimmung zu lesen, werden Sie bei herkömmlichen Implementierungen von sedgezwungen, so etwas wie /foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }nur zu schreiben /foo/{ N;N;N;N;N;N;N;N;N; }.

    • In jedem Fall besteht die einfachste Problemumgehung darin, $d;NSkripte zu verwenden, die auf dem herkömmlichen Verhalten basieren, oder die POSIXLY_CORRECTVariable auf einen nicht leeren Wert zu setzen.

Die POSIXLY_CORRECTUmgebungsvariable wird erwähnt, da POSIX angibt, dass sedEOF beim Versuch, eine EOF zu Nverwenden, ohne Ausgabe beendet werden soll. In diesem Fall verstößt die GNU-Version jedoch absichtlich gegen den Standard. Beachten Sie auch, dass, selbst wenn das Verhalten oben gerechtfertigt ist, davon ausgegangen wird, dass es sich bei dem Fehler um eine Stream-Bearbeitung handelt, bei der nicht eine ganze Datei in den Speicher geschlürft wird.

Der Standard definiert Ndas Verhalten folgendermaßen:

  • N

    • Hängen Sie die nächste Eingabezeile abzüglich der abschließenden \nEwline an den Musterbereich an und verwenden Sie eine eingebettete \nEwline, um das angehängte Material vom Originalmaterial zu trennen. Beachten Sie, dass sich die aktuelle Zeilennummer ändert.

    • Wenn keine nächste Eingabezeile verfügbar ist, Nverzweigt das Befehlsverb zum Ende des Skripts und wird beendet, ohne einen neuen Zyklus zu starten oder den Musterbereich in die Standardausgabe zu kopieren.

In diesem Sinne werden in der Frage einige andere GNU-Ismen demonstriert - insbesondere die Verwendung der Klammern für :Label, bRanch und {Funktionskontext }. Als Faustregel gilt, dass jeder sedBefehl, der einen beliebigen Parameter akzeptiert, an einer \nneuen Zeile im Skript abgegrenzt wird. Also die Befehle ...

:arbitrary_label_name; ...
b to_arbitrary_label_name; ...
//{ do arbitrary list of commands } ...

... sind alle sehr wahrscheinlich fehlerhaft, abhängig von der sedImplementierung, die sie liest. Tragbar sollten sie geschrieben werden:

...;:arbitrary_label_name
...;b to_arbitrary_label_name
//{ do arbitrary list of commands
}

Das gleiche gilt für r, w, t, a, i, und c (und möglicherweise ein paar mehr , dass ich im Moment bin zu vergessen) . In fast allen Fällen könnten sie auch geschrieben werden:

sed -e :arbitrary_label_name -e b\ to_arbitary_label_name -e \
    "//{ do arbitrary list of commands" -e \}

... wo die neue -execution-Anweisung für das \newline-Trennzeichen steht. Wenn der GNU- infoText eine traditionelle sedImplementierung vorschlägt , müssen Sie Folgendes tun :

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }

... es sollte eher sein ...

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N
}

... das stimmt natürlich auch nicht. Das Skript auf diese Weise zu schreiben ist ein wenig albern. Es gibt viel einfachere Mittel, um dasselbe zu tun, wie:

printf %s\\n foo . . . . . . |
sed -ne 'H;/foo/h;x;//s/\n/&/3p;tnd
         //!g;x;$!d;:nd' -e 'l;$a\' \
     -e 'this is the last line' 

... welche druckt:

foo
.
.
.
foo\n.\n.\n.$
.$
this is the last line

... weil der tBefehl est - wie die meisten sedBefehle - vom Zeilenzyklus abhängt, um sein Rückgaberegister zu aktualisieren, und hier der Zeilenzyklus den größten Teil der Arbeit ausführen darf. Dies ist ein weiterer Kompromiss, den Sie eingehen, wenn Sie eine Datei schlürfen. Der Zeilenzyklus wird nie wieder aktualisiert, und so viele Tests verhalten sich abnormal.

Der obige Befehl riskiert nicht, die Eingabe zu überschreiten, da nur einige einfache Tests durchgeführt werden, um zu überprüfen, was beim Lesen gelesen wird. Bei Halt werden alle Zeilen an den Haltebereich angehängt, aber wenn eine Zeile übereinstimmt /foo/, wird der halte Bereich überschrieben . Die Puffer werden als nächstes xgeändert, und eine bedingte s///Ersetzung wird versucht, wenn der Inhalt des Puffers mit dem //zuletzt adressierten Muster übereinstimmt . Mit anderen Worten, es wird //s/\n/&/3pversucht, die dritte neue Zeile im Haltebereich durch sich selbst zu ersetzen und die Ergebnisse auszudrucken, wenn der Haltebereich derzeit übereinstimmt /foo/. Wenn tdies erfolgreich ist , verzweigt sich das Skript zum not delete-Label, das das Skript überprüft lund abschließt .

In dem Fall, dass beide /foo/und eine dritte neue Zeile im Haltebereich nicht miteinander abgeglichen werden können, //!gwird der Puffer überschrieben, wenn er /foo/nicht übereinstimmt, oder, wenn er übereinstimmt, wird der Puffer überschrieben, wenn eine \nneue Zeile nicht übereinstimmt (wodurch er /foo/durch ersetzt wird) selbst) . Dieser kleine subtile Test verhindert, dass sich der Puffer für lange Strecken unnötig füllt, /foo/und stellt sicher, dass der Prozess schnell bleibt, da sich die Eingabe nicht stapelt. In einem No- /foo/oder //s/\n/&/3pFail-Fall werden die Puffer erneut ausgetauscht und jede Zeile bis auf die letzte wird dort gelöscht.

Das Letzte - die letzte Zeile $!d- ist eine einfache Demonstration, wie ein Top-Down- sedSkript erstellt werden kann, um mehrere Fälle einfach zu behandeln. Wenn Ihre allgemeine Methode darin besteht, unerwünschte Fälle, die mit den allgemeinsten beginnen und auf die spezifischsten hinarbeiten, zu beseitigen, können Randfälle einfacher behandelt werden, da sie einfach mit Ihren anderen gewünschten Daten und wann bis zum Ende des Skripts durchfallen dürfen Sie haben nur noch die gewünschten Daten. Es kann jedoch weitaus schwieriger sein, solche Randfälle aus einer geschlossenen Schleife abzurufen.

Und hier ist das Letzte, was ich zu sagen habe: Wenn Sie wirklich eine ganze Datei einlesen müssen, können Sie es ertragen, etwas weniger Arbeit zu erledigen, indem Sie sich auf den Leitungszyklus verlassen, um dies für Sie zu tun. Normalerweise verwenden Sie Next und next für Lookahead - weil sie vor dem Leitungszyklus vorrücken . Anstatt eine geschlossene Schleife redundant innerhalb einer Schleife zu implementieren - da der sedLeitungszyklus ohnehin nur eine einfache Leseschleife ist -, ist es wahrscheinlich einfacher, Eingaben wahllos zu sammeln:

sed 'H;1h;$!d;x;...'

... die die gesamte Datei sammeln oder pleite gehen.


eine Randnotiz über Nund Verhalten der letzten Zeile ...

Ich habe zwar nicht die Tools zum Testen zur Verfügung, aber bedenken Sie, dass sich das NLesen und die direkte Bearbeitung anders verhält, wenn die bearbeitete Datei die Skriptdatei für das nächste Durchlesen ist.

mikeserv
quelle
1
Das Unbedingte an die Herste Stelle zu setzen ist schön.
Bis zum
@mikeserv Danke für deine Eingabe. Ich kann einen potenziellen Nutzen darin sehen, den Leitungszyklus beizubehalten, aber wie ist es weniger Arbeit?
Dicktyr
@dicktyr Nun, die Syntax benötigt einige Verknüpfungen, :a;$!{N;ba}wie oben erwähnt - es ist auf lange Sicht einfacher, Standardformulare zu verwenden, wenn Sie versuchen, reguläre Ausdrücke auf unbekannten Systemen auszuführen. Aber das war nicht wirklich das, was ich meinte: Sie implementieren eine geschlossene Schleife - Sie können nicht so einfach in die Mitte kommen, wenn Sie möchten, wie Sie möchten, indem Sie sich verzweigen - unerwünschte Daten bereinigen - und den Zyklus zulassen. Es ist wie eine Top-Down-Sache - alles, sedwas es tut, ist ein direktes Ergebnis dessen, was es gerade getan hat. Vielleicht sehen Sie es anders - aber wenn Sie es versuchen, wird das Skript möglicherweise einfacher.
Mikeserv
11

Es schlägt fehl, weil der NBefehl vor dem Mustervergleich $!(nicht in der letzten Zeile) kommt und sed vor jeder Arbeit beendet wird:

N.

Fügen Sie dem Musterbereich eine neue Zeile hinzu und hängen Sie dann die nächste Eingabezeile an den Musterbereich an. Wenn keine Eingabe mehr erfolgt, wird sed beendet, ohne dass weitere Befehle verarbeitet werden .

Dies kann leicht behoben werden, um auch mit einzeiligen Eingaben zu arbeiten (und in jedem Fall klarer zu sein), indem einfach die Befehle Nund bnach dem Muster gruppiert werden :

sed ':a;$!{N;ba}; [commands...]'

Es funktioniert wie folgt:

  1. :a Erstellen Sie ein Label mit dem Namen "a".
  2. $! wenn nicht die letzte Zeile, dann
  3. NFügen Sie die nächste Zeile an den Musterbereich an (oder beenden Sie sie, wenn keine nächste Zeile vorhanden ist) und baverzweigen Sie die Bezeichnung 'a'.

Leider ist es nicht portabel (da es auf GNU-Erweiterungen basiert), aber die folgende Alternative (von @mikeserv vorgeschlagen) ist portabel:

sed 'H;1h;$!d;x; [commands...]'
dicktyr
quelle
Ich habe dies hier gepostet, weil ich die Informationen nicht an anderer Stelle gefunden habe und sie verfügbar machen wollte, damit andere Probleme mit der Verbreitung vermeiden können :a;N;$!ba;.
Dicktyr
Danke fürs Schreiben! Denken Sie daran, dass es auch in Ordnung ist, Ihre eigene Antwort zu akzeptieren. Sie müssen nur eine Weile warten, bevor das System dies zulässt.
Terdon