Wie kann ich sed verwenden, um eine mehrzeilige Zeichenfolge zu ersetzen?

243

Mir ist aufgefallen, dass wenn ich \nein Muster zum Ersetzen mit hinzufüge sed, es nicht übereinstimmt. Beispiel:

$ cat > alpha.txt
This is
a test
Please do not
be alarmed

$ sed -i'.original' 's/a test\nPlease do not/not a test\nBe/' alpha.txt

$ diff alpha.txt{,.original}

$ # No differences printed out

Wie kann ich das zum Laufen bringen?

Belmin Fernandez
quelle
Problemumgehung hier: unix.stackexchange.com/a/445666/61742 . Natürlich ist es nicht performatisch! Andere gute Optionen zum Durchführen eines Austauschs gemäß Ihren Anforderungen sind awk, perl und python. Es gibt viele andere, aber ich glaube, dass awk in den verschiedenen Linux-Distributionen (zum Beispiel) am universellsten ist. Vielen Dank!
Eduardo Lucio

Antworten:

235

Im einfachsten Aufruf von sed enthält es eine Textzeile im Musterbereich, d. H. 1 Zeile \nbegrenzter Text aus der Eingabe. Die einzelne Zeile im Musterbereich hat keine \n... Deshalb findet Ihr Regex nichts.

Sie können mehrere Zeilen in den Musterraum einlesen und die Dinge überraschend gut, aber mit mehr als normalem Aufwand manipulieren. Sed verfügt über eine Reihe von Befehlen, die diese Art von Dingen ermöglichen ... Hier ist ein Link zu einer Befehlsübersicht für sed . Es ist das beste, das ich gefunden habe und das mich ins Rollen gebracht hat.

Vergessen Sie jedoch die "Einzeiler" -Idee, sobald Sie mit der Verwendung der Mikrobefehle von sed beginnen. Es ist nützlich, es wie ein strukturiertes Programm anzuordnen, bis Sie das Gefühl dafür bekommen ... Es ist überraschend einfach und ebenso ungewöhnlich. Sie können es sich als die "Assemblersprache" der Textbearbeitung vorstellen.

Zusammenfassung: Verwenden Sie sed für einfache Dinge, und vielleicht auch für etwas mehr, aber im Allgemeinen bevorzugen die meisten Leute etwas anderes, wenn es über das Arbeiten mit einer einzelnen Zeile hinausgeht ...
Ich lasse jemanden etwas anderes vorschlagen Ich bin mir nicht sicher, was die beste Wahl sein würde. (Ich würde sed verwenden, aber das liegt daran, dass ich Perl nicht gut genug kenne.)


sed '/^a test$/{
       $!{ N        # append the next line when not on the last line
         s/^a test\nPlease do not$/not a test\nBe/
                    # now test for a successful substitution, otherwise
                    #+  unpaired "a test" lines would be mis-handled
         t sub-yes  # branch_on_substitute (goto label :sub-yes)
         :sub-not   # a label (not essential; here to self document)
                    # if no substituion, print only the first line
         P          # pattern_first_line_print
         D          # pattern_ltrunc(line+nl)_top/cycle
         :sub-yes   # a label (the goto target of the 't' branch)
                    # fall through to final auto-pattern_print (2 lines)
       }    
     }' alpha.txt  

Hier ist es das gleiche Skript, kondensierte in dem, was ist offensichtlich schwerer zu lesen und die Arbeit mit, aber einige würden unschlüssig nennen einen Einzeiler

sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;ty;P;D;:y}}' alpha.txt

Hier ist mein Befehl "Spickzettel"

:  # label
=  # line_number
a  # append_text_to_stdout_after_flush
b  # branch_unconditional             
c  # range_change                     
d  # pattern_delete_top/cycle          
D  # pattern_ltrunc(line+nl)_top/cycle 
g  # pattern=hold                      
G  # pattern+=nl+hold                  
h  # hold=pattern                      
H  # hold+=nl+pattern                  
i  # insert_text_to_stdout_now         
l  # pattern_list                       
n  # pattern_flush=nextline_continue   
N  # pattern+=nl+nextline              
p  # pattern_print                     
P  # pattern_first_line_print          
q  # flush_quit                        
r  # append_file_to_stdout_after_flush 
s  # substitute                                          
t  # branch_on_substitute              
w  # append_pattern_to_file_now         
x  # swap_pattern_and_hold             
y  # transform_chars                   
Peter.O
quelle
167
Erschieße mich jetzt. Schlechteste Syntax aller Zeiten!
Gili
53
Dies ist eine fantastische Erklärung, aber ich bin geneigt, @Gili zuzustimmen.
gatoatigrado
11
Ihr Spickzettel hat alles.
konsolebox
3
Sie benötigen keine Beschriftung, um den tBefehl hier zu verwenden - wenn Sie keine Beschriftung erhalten, verzweigt er standardmäßig zum Ende des Skripts. Tut sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;t;P;D}}' alpha.txtalso unter allen Umständen genau das Gleiche wie Ihr Befehl. Das gleiche gilt natürlich auch für diese bestimmte Datei, sed '/test/{N;s/.*/not a test\nBe/}' alpha.txtaber mein erstes Beispiel ist für alle möglichen Dateien logisch äquivalent . Beachten Sie auch, dass \nin einer Ersatzzeichenfolge kein Zeilenumbruch erzeugt wird. Dazu benötigen Sie einen Backslash "\" gefolgt von einem tatsächlichen Zeilenumbruch.
Wildcard
9
Beachten Sie, dass diese Syntax GNU-spezifisch ist ( #Befehl nicht von der vorherigen getrennt, \nin RHS von s). Mit GNU können sedSie auch -zNUL-getrennte Datensätze verwenden (und dann die gesamte Eingabe schlürfen, wenn es sich um Text handelt (der per Definition keine NULs enthält)).
Stéphane Chazelas
181

Verwenden Sie perlanstelle von sed:

$ perl -0777 -i.original -pe 's/a test\nPlease do not/not a test\nBe/igs' alpha.txt
$ diff alpha.txt{,.original}
2,3c2,3
< not a test
< Be
---
> a test
> Please do not

-pi -eist Ihre Standardbefehlszeilenfolge "Replace In Place", und -0777 bewirkt, dass Perl Dateien als Ganzes schlürft. Weitere Informationen finden Sie unter perldoc perlrun .

Codehead
quelle
3
Vielen Dank! Für mehrzeiliges Arbeiten gewinnt Perl zweifellos! Am Ende habe ich "$ perl -pi -e" / bar / baz / "fileA" verwendet, um die Datei an Ort und Stelle zu ändern.
Nicholas Tolley Cottrell
3
Es kommt sehr häufig vor, dass das Originalposter sedmit awk oder perl angefragt und geantwortet wird. Ich denke, es ist kein Thema, daher tut es mir leid, aber ich habe einen Minuspunkt abgegeben.
Rho Phi
68
+1 & nicht einverstanden mit Roberto. Oft wurden Fragen speziell formuliert, um bessere Methoden nicht zu kennen. Wenn es keinen inhaltlichen Kontextunterschied gibt (wie hier), sollten optimale Lösungen mindestens so viel Profil erhalten wie die fragenspezifischen.
Geotheory
56
Ich denke, die sedAntwort oben beweist, dass eine Perl-Antwort zum Thema gehört.
Reinierpost
7
Ein bisschen einfacher: Mit "-p0e" ist das "-0777" nicht nötig. unix.stackexchange.com/a/181215/197502
Weidenrinde
96

Ich denke, es ist besser, das \nSymbol durch ein anderes Symbol zu ersetzen und dann wie gewohnt zu arbeiten:

zB nicht bearbeiteter Quellcode:

cat alpha.txt | sed -e 's/a test\nPlease do not/not a test\nBe/'

kann geändert werden in:

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test\rPlease do not/not a test\rBe/'  | tr '\r' '\n'

Wenn jemand es nicht weiß, \nendet die UNIX-Linie, \r\n- Windows, \r- klassisches Mac OS. Normaler UNIX-Text verwendet kein \rSymbol, daher ist es sicher, es für diesen Fall zu verwenden.

Sie können auch ein exotisches Symbol verwenden, um \ n vorübergehend zu ersetzen. Als Beispiel - \ f (Formularvorschubsymbol). Weitere Symbole finden Sie hier .

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'
xara
quelle
11
+1 für diesen cleveren Hack! Besonders nützlich ist der Rat, ein exotisches Symbol zu verwenden, um eine neue Zeile vorübergehend zu ersetzen, es sei denn, Sie sind sich über den Inhalt der Datei, die Sie bearbeiten, absolut sicher.
6.
Dies funktioniert nicht wie unter OS X beschrieben. Stattdessen müssen alle Instanzen von \rim Argument bis seddurch ersetzt werden $(printf '\r').
abeboparebop
@abeboparebop: tolle Entdeckung! 👍 Installieren Sie alternativ GNU sed mit Homebrew: stackoverflow.com/a/30005262
ssc
@abeboparebop, Unter OSX müssen Sie nur einen $vor dem sed-String einfügen, um zu verhindern, dass der \rin einen konvertiert wird r. Kurzes Beispiel: sed $'s/\r/~/'. Vollständiges Beispiel:cat alpha.txt | tr '\n' '\r' | sed $'s/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
wisbucky
40

Alles in allem kann das Verschlingen der gesamten Datei der schnellste Weg sein.

Die grundlegende Syntax lautet wie folgt:

sed -e '1h;2,$H;$!d;g' -e 's/__YOUR_REGEX_GOES_HERE__...'

Wohlgemerkt, das Verschlingen der gesamten Datei ist möglicherweise keine Option, wenn die Datei enorm groß ist. In solchen Fällen bieten andere hier bereitgestellte Antworten maßgeschneiderte Lösungen, die garantiert einen geringen Speicherbedarf aufweisen.

In allen anderen Hack- und Slash-Situationen erledigt man den Job , wenn man nur das Präfix -e '1h;2,$H;$!d;g'gefolgt von dem ursprünglichen sedRegex-Argument verwendet.

z.B

$ echo -e "Dog\nFox\nCat\nSnake\n" | sed -e '1h;2,$H;$!d;g' -re 's/([^\n]*)\n([^\n]*)\n/Quick \2\nLazy \1\n/g'
Quick Fox
Lazy Dog
Quick Snake
Lazy Cat

Was macht -e '1h;2,$H;$!d;g'das?

Die 1, 2,$, $!Teile sind Linie , die Grenzlinien - Spezifizierer , die direkt folgenden Befehl läuft auf.

  • 1: Nur erste Zeile
  • 2,$: Alle Zeilen ab der Sekunde
  • $!: Jede andere Zeile als die letzte

So erweitert geschieht dies auf jeder Zeile eines N-Zeilen-Eingangs.

  1: h, d
  2: H, d
  3: H, d
  .
  .
N-2: H, d
N-1: H, d
  N: H, g

Der gBefehl erhält keinen Zeilenspezifizierer, aber der vorhergehende dBefehl enthält eine spezielle Klausel " Start next cycle. ", Die verhindert, dass galle Zeilen außer der letzten ausgeführt werden.

Zur Bedeutung der einzelnen Befehle:

  • Das erste hgefolgt von Hs in jeder Zeile kopiert die Eingabezeilen in sedden Hold-Bereich . (Denken Sie an einen beliebigen Textpuffer.)
  • Danach dverwirft jede Zeile von verhindert diese Zeilen an den Ausgang geschrieben wird. Der Laderaum bleibt jedoch erhalten.
  • Schließlich wird in der allerletzten Zeile gdie Akkumulation jeder Zeile aus dem Haltebereich wiederhergestellt , sodass sedder reguläre Ausdruck für die gesamte Eingabe (und nicht zeilenweise) ausgeführt werden kann Spiel auf \ns.
antak
quelle
38

sedhat drei Befehle mehrzeiligen Operationen zu verwalten: N, Dund P(vergleichen sie mit normalen n , dund p).

In diesem Fall können Sie die erste Zeile Ihres Musters Nabgleichen, die zweite Zeile an den Musterbereich anhängen und dann sIhre Ersetzung vornehmen.

So etwas wie:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}
andcoz
quelle
2
Das ist fantastisch! Einfacher als die akzeptierte Antwort und trotzdem effektiv.
Jeyk
Und alle diejenigen , die den Halteraum ( G, H, x...). Mit dem sBefehl können auch weitere Zeilen in den Musterbereich eingefügt werden .
Stéphane Chazelas
Diese Lösung funktioniert nicht mit folgenden Fall "Dies ist \ na Test \ na Test \ n Bitte nicht alarmiert werden"
mug896
@ mug896 Sie müssen höchstwahrscheinlich mehrere NBefehle
loa_in_
15

Sie können, aber es ist schwierig . Ich empfehle, zu einem anderen Werkzeug zu wechseln. Wenn es einen regulären Ausdruck gibt, der niemals mit einem Teil des zu ersetzenden Textes übereinstimmt, können Sie ihn in GNU awk als Trennzeichen für awk-Datensätze verwenden.

awk -v RS='a' '{gsub(/hello/, "world"); print}'

Wenn Ihre Suchzeichenfolge nie zwei aufeinanderfolgende Zeilenumbrüche enthält, können Sie den "Absatzmodus" von awk verwenden (eine oder mehrere Leerzeilen trennen Datensätze).

awk -v RS='' '{gsub(/hello/, "world"); print}'

Eine einfache Lösung besteht darin, Perl zu verwenden und die Datei vollständig in den Speicher zu laden.

perl -0777 -pe 's/hello/world/g'
Gilles
quelle
1
Wie wende ich den Perl-Befehl auf eine Datei an?
Sebix
2
@sebix perl -0777 -pe '…' <input-file >output-file. So ändern Sie eine Datei an Ort und Stelle:perl -0777 -i -pe '…' filename
Gilles
3
Siehe auch GNU sed‚s - -zOption (hinzugefügt im Jahr 2012 nach , dass Antwort geschrieben wurde): seq 10 | sed -z 's/4\n5/a\nb/'.
Stéphane Chazelas
7

Ich denke, das ist die sed-Lösung für 2 passende Zeilen.

sed -n '$!N;s@a test\nPlease do not@not a test\nBe@;P;D' alpha.txt

Wenn Sie 3 passende Zeilen haben wollen, dann ...

sed -n '1{$!N};$!N;s@aaa\nbbb\nccc@xxx\nyyy\nzzz@;P;D'

Wenn Sie 4 übereinstimmende Zeilen möchten, dann ...

sed -n '1{$!N;$!N};$!N;s@ ... @ ... @;P;D'

Wenn das Ersatzteil in den "s" -Befehlszeilen schrumpft, dann ist das etwas komplizierter

# aaa\nbbb\nccc shrink to one line "xxx"

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@xxx@;$!N;$!N};P;D'

Wenn der Repacement-Teil Linien wachsen lässt, ist dies etwas komplizierter

# aaa\nbbb\nccc grow to five lines vvv\nwww\nxxx\nyyy\nzzz

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@vvv\nwww\nxxx\nyyy\nzzz@;P;s/.*\n//M;P;s/.*\n//M};P;D'
mug896
quelle
Dies sollte seinen Weg nach oben machen! Ich habe nur das "-i" anstelle von "-n" für die zweizeilige Ersetzung verwendet, weil ich das benötige und es übrigens auch im Beispiel des Fragestellers ist.
Nagev
5
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt

Hier /a test/,/Please do not/wird als Textblock (mehrzeilig) cder Änderungsbefehl gefolgt von neuem Text betrachtetnot a test \nBe

Für den Fall, dass der zu ersetzende Text sehr lang ist, würde ich ex- Syntax vorschlagen .

gibies
quelle
oops das problem ist, dass sed den gesamten eventuellen text zwischen / a test / und / Please do / as well ... ersetzt :(
noonex
4
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out

Erweitern Sie einfach Ihr Fenster bei der Eingabe ein wenig.

Das ist ziemlich einfach. Neben der Standardsubstitution; Sie brauchen nur $!N, Pund Dhier.

mikeserv
quelle
4

Abgesehen von Perl ist ein allgemeiner und praktischer Ansatz für die mehrzeilige Bearbeitung von Streams (und auch Dateien):

Erstellen Sie zuerst einen neuen UNIQUE-Zeilentrenner, wie Sie möchten

$ S=__ABC__                     # simple
$ S=__$RANDOM$RANDOM$RANDOM__   # better
$ S=$(openssl rand -hex 16)     # ultimate

Dann ersetzen Sie in Ihrem sed-Befehl (oder einem anderen Tool) \ n durch $ {S} wie

$ cat file.txt | awk 1 ORS=$S |  sed -e "s/a test${S}Please do not/not a test\nBe/" | awk 1 RS=$S > file_new.txt

(awk ersetzt ASCII-Zeilentrennzeichen durch deins und umgekehrt.)

Gast
quelle
2

Dies ist eine kleine Modifikation der cleveren Antwort von xara, damit sie unter OS X funktioniert (ich verwende 10.10):

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test$(printf '\r')Please do not/not a test$(printf '\r')Be/'  | tr '\r' '\n'

Anstatt explizit zu verwenden \r, müssen Sie verwenden $(printf '\r').

abeboparebop
quelle
1
Beachten Sie, dass Sie die Shell-Syntax nur verwenden können , um auf entkommene Literale zu verweisen, während printf '\r'(oder echo -e '\r') ordnungsgemäß funktionieren $'\r'. Beispiel: echo hi$'\n'thereGibt eine neue Zeile zwischen hiund aus there. In ähnlicher Weise können Sie die gesamte Zeichenfolge \ echo $'hi\nthere'
umbrechen
1

Ich wollte einer Datei mit sed ein paar Zeilen HTML hinzufügen (und landete hier). Normalerweise würde ich nur Perl verwenden, aber ich war auf einer Box, die sed, bash und sonst nicht viel hatte. Ich fand heraus, dass, wenn ich den String in eine einzelne Zeile änderte und bash / sed das \ t \ n interpolieren ließ, alles klappte:

HTML_FILE='a.html' #contains an anchor in the form <a name="nchor" />
BASH_STRING_A='apples'
BASH_STRING_B='bananas'
INSERT="\t<li>$BASH_STRING_A<\/li>\n\t<li>$BASH_STRING_B<\/li>\n<a name=\"nchor\"\/>"
sed -i "s/<a name=\"nchor"\/>/$INSERT/" $HTML_FILE

Es wäre sauberer, eine Funktion zu haben, die den doppelten Anführungszeichen und Schrägstrichen entgeht, aber manchmal ist die Abstraktion der Dieb der Zeit.

Alexx Roche
quelle
1

GNU sedhat eine -zOption, die es erlaubt, die Syntax zu verwenden, die das OP anzuwenden versucht hat. ( Manpage )

Beispiel:

$ cat alpha.txt
This is
a test
Please do not
be alarmed
$ sed -z 's/a test\nPlease do not\nbe/not a test\nBe/' -i alpha.txt
$ cat alpha.txt
This is
not a test
Be alarmed

Beachten Sie: Wenn Sie ^und verwenden $, stimmen sie jetzt mit dem Anfang und dem Ende von Zeilen überein, die mit einem NUL-Zeichen (nicht \n) getrennt sind. \nVergessen Sie nicht, das gFlag für globale Ersetzungen (z s/.../.../g. B. ) zu verwenden , um sicherzustellen, dass Übereinstimmungen in allen (durch Trennzeichen getrennten) Zeilen ersetzt werden .


Credits: @ stéphane-chazelas zuerst erwähnt -z in einem Kommentar oben.

Peterino
quelle
0

Sed unterbricht die Eingabe in Zeilenumbrüchen. Es bleibt nur eine Zeile pro Schleife.
Daher gibt es keine Möglichkeit, eine \n(Newline) zuzuordnen, wenn der Musterbereich sie nicht enthält.

Es gibt jedoch eine Möglichkeit, mit der Schleife zwei aufeinanderfolgende Zeilen im Musterbereich zu halten :

sed 'N;l;P;D' alpha.txt

Fügen Sie die erforderliche Verarbeitung zwischen dem N und dem P hinzu (ersetzen Sie das l).

In diesem Fall (2 Zeilen):

$ sed 'N;s/a test\nPlease do not/not a test\nBe/;P;D' alpha.txt
This is
not a test
Be
be alarmed

Oder für drei Zeilen:

$ sed -n '1{$!N};$!N;s@a test\nPlease do not\nbe@not a test\nDo\nBe@;P;D' alpha.txt 
This is
not a test
Do
Be alarmed

Dies setzt voraus, dass die gleiche Anzahl von Zeilen ersetzt wird.

Isaac
quelle