wake-up-neo.com

Wie verwende ich sed, um nur das erste Vorkommen in einer Datei zu ersetzen?

Ich möchte eine große Anzahl von C++ - Quelldateien mit einer zusätzlichen include-Direktive aktualisieren, bevor ein vorhandenes # enthält. Für diese Art von Aufgabe verwende ich normalerweise ein kleines Bash-Skript mit sed, um die Datei neu zu schreiben. 

Wie erhalte ich, dass sed nur das erste Vorkommen einer Zeichenfolge in einer Datei ersetzt, anstatt jedes Vorkommen zu ersetzen?

Wenn ich benutze

sed s/#include/#include "newfile.h"\n#include/

es ersetzt alle #inclusse. 

Alternative Vorschläge, um dasselbe zu erreichen, sind ebenfalls willkommen.

173
David Dibben
 # sed script to change "foo" to "bar" only on the first occurrence
 1{x;s/^/first/;x;}
 1,/foo/{x;/first/s///;x;s/foo/bar/;}
 #---end of script---

oder wenn Sie es bevorzugen: Anmerkung des Herausgebers: Funktioniert nur mit GNUsed.

sed '0,/RE/s//to_that/' file 

Quelle

108
Ben Hoffstein

Schreiben Sie ein Sed-Skript, das nur das erste Vorkommen von "Apple" durch "Banana" ersetzt.

Beispieleingabe: Ausgabe:

     Apple       Banana
     Orange      Orange
     Apple       Apple

Dies ist das einfache Skript: Anmerkung der Redaktion: Funktioniert nur mitGNUsed.

sed '0,/Apple/{s/Apple/Banana/}' filename
231
tim
sed '0,/pattern/s/pattern/replacement/' filename

das hat für mich funktioniert.

beispiel

sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt

Anmerkung des Herausgebers: Beide arbeiten nur mitGNUsed.

50
Sushil

Eine Übersicht der vielen hilfreichen vorhandenen Antworten , ergänzt mit Erläuterungen :

In den Beispielen wird ein vereinfachter Anwendungsfall verwendet: Ersetzen Sie das Wort 'foo' nur in der ersten übereinstimmenden Zeile durch 'bar'.
Aufgrund der Verwendung von ANSI C-quoted strings ($'...') zur Bereitstellung der Beispieleingabezeilen wird als Shell bash, ksh oder zsh angenommen.


GNU sed only:

Ben Hoffsteins Antwort zeigt uns, dass GNU eine -Erweiterung zur POSIX-Spezifikation für sed bereitstellt, die die folgenden 2- Adressform: 0,/re/ (re steht hier für einen beliebigen regulären Ausdruck).

0,/re/erlaubt dem Regex, mit in der allerersten Zeile abzugleichen, auch. Mit anderen Worten: Eine solche Adresse erstellt einen Bereich von der ersten Zeile bis einschließlich der Zeile, die mit re übereinstimmt - unabhängig davon, ob re in der ersten Zeile oder in einer nachfolgenden Zeile vorkommt.

  • Vergleichen Sie dies mit dem POSIX-kompatiblen Formular1,/re/, mit dem ein Bereich erstellt wird, der von der ersten Zeile bis einschließlich der Zeile übereinstimmt, die in nachfolgenden Zeilen mit re übereinstimmt. Mit anderen Worten: Dies erkennt nicht das erste Auftreten einer re-Übereinstimmung, wenn es zufällig in der 1st -Zeile auftritt und verhindert auch die Verwendung der Kurzform //für die Wiederverwendung der letzten Regex verwendet (siehe nächster Punkt).[1]

Wenn Sie eine 0,/re/-Adresse mit einem s/.../.../-Aufruf (Substitution) kombinieren, der den regulären Ausdruck same verwendet, führt Ihr Befehl die Substitution effektiv nur in der Zeile first durch, die mit re übereinstimmt.
sed bietet eine praktische Verknüpfung zur Wiederverwendung des zuletzt angewendeten regulären Ausdrucks : einleeres Trennzeichenpaar, //.

$ sed '0,/foo/ s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo' 
1st bar         # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo

Nur POSIX-Funktionen sed wie BSD (macOS) sed(funktioniert auch mit GNU sed):

Da 0,/re/ nicht verwendet werden kann und das Formular 1,/re/re nicht erkennt, wenn es in der allerersten Zeile auftritt (siehe oben), ist eine spezielle Behandlung für die erste Zeile erforderlich .

MikhailVS's answer erwähnt die Technik, die hier in einem konkreten Beispiel aufgeführt ist:

$ sed -e '1 s/foo/bar/; t' -e '1,// s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar         # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo

Hinweis:

  • Die leere Regex-Verknüpfung // wird hier zweimal verwendet: einmal für den Endpunkt des Bereichs und einmal im Aufruf s; In beiden Fällen wird regex foo implizit wiederverwendet, sodass wir es nicht duplizieren müssen. Dies führt zu kürzerem und besser wartbarem Code.

  • POSIX sed benötigt nach bestimmten Funktionen aktuelle Zeilenumbrüche, z. B. nach dem Namen eines Etiketts oder nach dessen Auslassung, wie dies hier bei t der Fall ist. Die strategische Aufteilung des Skripts in mehrere -e-Optionen ist eine Alternative zur Verwendung eines tatsächlichen Zeilenumbruchs: Beenden Sie jeden -e-Skriptabschnitt dort, wo normalerweise ein Zeilenumbruch erforderlich ist.

1 s/foo/bar/ ersetzt foo nur in der ersten Zeile, falls dort vorhanden. In diesem Fall verzweigt t zum Ende des Skripts (überspringt die verbleibenden Befehle in der Zeile). (Die Funktion t verzweigt nur zu einer Bezeichnung, wenn der letzte Aufruf von s eine tatsächliche Ersetzung durchgeführt hat. Wenn keine Bezeichnung vorhanden ist, wird wie hier das Ende des Skripts verzweigt.).

In diesem Fall wird die Bereichsadresse 1,//, die normalerweise das erste Vorkommen ab Zeile 2 findet, nicht übereinstimmen und der Bereich nicht verarbeitet, da die Adresse bei der Auswertung von ausgewertet wird Die aktuelle Zeile ist bereits 2.

Umgekehrt, wenn in der ersten Zeile keine Übereinstimmung vorhanden ist, wird 1,// eingegeben und findet die wahre erste Übereinstimmung.

Der Nettoeffekt ist derselbe wie bei GNU= sed 's 0,/re/: Es wird nur das erste Vorkommen ersetzt, egal ob es in der 1. Zeile oder einem anderen auftritt.


NON-Range-Ansätze

Potongs Antwort demonstriertloop Techniken die die Notwendigkeit eines Bereichs umgehen ; Da er die Syntax GNU sed verwendet, sind hier die POSIX-kompatiblen Entsprechungen :

Schleifentechnik 1: Führen Sie beim ersten Match die Ersetzung durch und geben Sie dann eine Schleife ein, die einfach die verbleibenden Zeilen druckt, wie sie sind :

$ sed -e '/foo/ {s//bar/; ' -e ':a' -e '$!{n;ba' -e '};}' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo

Loop-Technik 2, nur für kleinere Dateien : liest die gesamte Eingabe in den Speicher und setzt sie dann einzeln um .

$ sed -e ':a' -e '$!{N;ba' -e '}; s/foo/bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo

[1] 1.618 liefert Beispiele für die Vorgänge mit 1,/re/ mit und ohne nachfolgenden s//:
- sed '1,/foo/ s/foo/bar/' <<<$'1foo\n2foo' ergibt $'1bar\n2bar'; d.h. beide Zeilen wurden aktualisiert, da die Zeilennummer 1 mit der ersten Zeile übereinstimmt und der Ausdruck /foo/ - das Ende des Bereichs - nur ab der Zeile next gesucht wird. Daher werden in diesem Fall both -Zeilen ausgewählt, und die s/foo/bar/-Ersetzung wird für beide Zeilen ausgeführt.
- sed '1,/foo/ s//bar/' <<<$'1foo\n2foo\n3foo'failed: mit sed: first RE may not be empty (BSD/macOS) und sed: -e expression #1, char 0: no previous regular expression (GNU), da zum Zeitpunkt der Verarbeitung der 1. Zeile (aufgrund der Zeilennummer 1, die den Bereich beginnt) keine Regex vorhanden ist wurde bereits angewendet, daher bezieht sich // auf nichts.
Mit Ausnahme der speziellen 0,/re/-Syntax von GNU= sed) schließt any, die mit line number beginnt, die Verwendung von // effektiv aus.

36
mklement0

Sie könnten awk verwenden, um etwas Ähnliches zu tun ..

awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c

Erläuterung:

/#include/ && !done

Führt die Aktionsanweisung zwischen {} aus, wenn die Zeile mit "#include" übereinstimmt und wir sie noch nicht verarbeitet haben.

{print "#include \"newfile.h\""; done=1;}

Dies gibt #include "newfile.h" aus, wir müssen den Anführungszeichen entgehen. Dann setzen wir die done-Variable auf 1, so dass wir keine weiteren Includes hinzufügen.

1;

Dies bedeutet "die Zeile ausdrucken" - eine leere Aktion gibt standardmäßig $ 0 aus und druckt die gesamte Zeile aus. Ein Liner und einfacher zu verstehen als sed IMO :-)

22
richq

Eine umfassende Sammlung von Antworten zu linuxtopia sed FAQ . Es wird auch hervorgehoben, dass einige Antworten, die von Benutzern bereitgestellt wurden, nicht mit der Nicht-GNU-Version von sed funktionieren, z 

sed '0,/RE/s//to_that/' file

in nicht-GNU-Version muss sein 

sed -e '1s/RE/to_that/;t' -e '1,/RE/s//to_that/'

Diese Version funktioniert jedoch nicht mit gnu sed. 

Hier ist eine Version, die mit beiden zusammenarbeitet:

-e '/RE/{s//to_that/;:a' -e '$!N;$!ba' -e '}'

ex:

sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename
16
MikhailVS
#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}

Funktionsweise dieses Skripts: Für Zeilen zwischen 1 und dem ersten #include (nach Zeile 1), wenn die Zeile mit #include beginnt, wird die angegebene Zeile vorangestellt.

Befindet sich der erste #include jedoch in Zeile 1, wird die Zeile in Zeile 1 und im nächsten #include vorangestellt. Wenn Sie GNU sed verwenden, hat es eine Erweiterung, bei der 0,/^#include/ (anstelle von 1,) das Richtige tut.

12

Fügen Sie einfach die Anzahl des Vorkommens am Ende hinzu:

sed s/#include/#include "newfile.h"\n#include/1
11
unexist

Eine mögliche Lösung:

    /#include/!{p;d;}
    i\
    #include "newfile.h"
    :
    n
    b

Erläuterung:

  • zeilen lesen, bis wir #include finden, diese Zeilen ausdrucken und neuen Zyklus starten
  • fügen Sie die neue Include-Zeile ein
  • wenn Sie eine Schleife eingeben, in der nur Zeilen gelesen werden (standardmäßig werden diese Zeilen auch von sed gedruckt), kehren wir hier nicht zum ersten Teil des Skripts zurück
8
mitchnull

Ich weiß, dass dies ein alter Beitrag ist, aber ich hatte eine Lösung, die ich verwendet habe:

grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file

Verwenden Sie grundsätzlich grep, um das erste Vorkommen zu finden und dort anzuhalten. Drucken Sie auch die Zeilennummer, dh 5: Zeile. Pipe das in sed und entferne das: und alles danach, so dass du nur noch eine Zeilennummer hinterlässt. Pipe das in sed, das s /.*/ als Ersetzung hinzufügt, um das Ende zu erreichen, das ein einzeiliges Skript ergibt, das in den letzten sed geleitet wird, um als Skript für Datei auszuführen.

wenn also regex = #include und replace = blah ist und das erste Vorkommen, das grep in Zeile 5 findet, dann die zum letzten sed geleiteten Daten 5s /.*/ blah/lautet.

3
Michael Edwards

Wenn jemand hierher gekommen ist, um ein Zeichen für das erste Vorkommen in allen Zeilen (wie ich) zu ersetzen, verwenden Sie Folgendes:

sed '/old/s/old/new/1' file

-bash-4.2$ cat file
123a456a789a
12a34a56
a12
-bash-4.2$ sed '/a/s/a/b/1' file
123b456a789a
12b34a56
b12

Indem Sie beispielsweise 1 zu 2 ändern, können Sie stattdessen alle zweiten a ersetzen.

2
FatihSarigol

Als alternativen Vorschlag können Sie den Befehl ed betrachten.

man 1 ed

teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'

# for in-place file editing use "ed -s file" and replace ",p" with "w"
# cf. http://wiki.bash-hackers.org/howto/edit-ed
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
   H
   /# *include/i
   #include "newfile.h"
   .
   ,p
   q
EOF
2
timo

Verwenden Sie FreeBSD ed und vermeiden Sie den Fehler "keine Übereinstimmung" von ed, falls keine include-Anweisung in einer zu verarbeitenden Datei vorhanden ist: 

teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'

# using FreeBSD ed
# to avoid ed's "no match" error, see
# *emphasized text*http://codesnippets.joyent.com/posts/show/11917 
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
   H
   ,g/# *include/u\
   u\
   i\
   #include "newfile.h"\
   .
   ,p
   q
EOF
2
nazq

Ich habe es endlich verstanden, in einem Bash-Skript zu arbeiten, mit dem ein eindeutiger Zeitstempel in jedes Element eines RSS-Feeds eingefügt wird:

        sed "1,/====RSSpermalink====/s/====RSSpermalink====/${nowms}/" \
            production-feed2.xml.tmp2 > production-feed2.xml.tmp.$counter

Es ändert sich nur das erste Vorkommen. 

${nowms} ist die Zeit in Millisekunden, die von einem Perl-Skript festgelegt wird. $counter ist ein Zähler, der für die Schleifensteuerung im Skript verwendet wird. \ ermöglicht, dass der Befehl in der nächsten Zeile fortgesetzt wird.

Die Datei wird eingelesen und stdout wird in eine Arbeitsdatei umgeleitet.

So wie ich es verstehe, sagt 1,/====RSSpermalink====/ sed, wann er aufhören soll, indem er eine Bereichsbegrenzung einstellt, und dann ist s/====RSSpermalink====/${nowms}/ der bekannte sed-Befehl, der den ersten String durch den zweiten ersetzt. 

In meinem Fall füge ich den Befehl in doppelte Anführungszeichen ein, da ich ihn in einem Bash-Skript mit Variablen verwende.

2
Michael Cook

Das könnte für Sie funktionieren (GNU sed):

sed -si '/#include/{s//& "newfile.h\n&/;:a;$!{n;ba}}' file1 file2 file....

oder wenn der Speicher kein Problem ist:

sed -si ':a;$!{N;ba};s/#include/& "newfile.h\n&/' file1 file2 file...
2
potong

ich würde das mit einem awk-Skript machen:

BEGIN {i=0}
(i==0) && /#include/ {print "#include \"newfile.h\""; i=1}
{print $0}    
END {}

dann mit awk laufen lassen:

awk -f awkscript headerfile.h > headerfilenew.h

könnte schlampig sein, ich bin neu in diesem Bereich.

2
wakingrufus

Nichts Neues, aber vielleicht etwas konkretere Antwort: sed -rn '0,/foo(bar).*/ s%%\1%p'

Beispiel: xwininfo -name unity-launcher erzeugt eine Ausgabe wie folgt:

xwininfo: Window id: 0x2200003 "unity-launcher"

  Absolute upper-left X:  -2980
  Absolute upper-left Y:  -198
  Relative upper-left X:  0
  Relative upper-left Y:  0
  Width: 2880
  Height: 98
  Depth: 24
  Visual: 0x21
  Visual Class: TrueColor
  Border width: 0
  Class: InputOutput
  Colormap: 0x20 (installed)
  Bit Gravity State: ForgetGravity
  Window Gravity State: NorthWestGravity
  Backing Store State: NotUseful
  Save Under State: no
  Map State: IsViewable
  Override Redirect State: no
  Corners:  +-2980+-198  -2980+-198  -2980-1900  +-2980-1900
  -geometry 2880x98+-2980+-198

Das Extrahieren der Fenster-ID mit xwininfo -name unity-launcher|sed -rn '0,/^xwininfo: Window id: (0x[0-9a-fA-F]+).*/ s%%\1%p' erzeugt:

0x2200003
0

POSIXly (auch gültig in sed), Nur one regex wird verwendet, Speicher wird nur für eine Zeile benötigt (wie üblich):

sed '/\(#include\).*/!b;//{h;s//\1 "newfile.h"/;G};:1;n;b1'

Erklärt:

sed '
/\(#include\).*/!b          # Only one regex used. On lines not matching
                            # the text  `#include` **yet**,
                            # branch to end, cause the default print. Re-start.
//{                         # On first line matching previous regex.
    h                       # hold the line.
    s//\1 "newfile.h"/      # append ` "newfile.h"` to the `#include` matched.
    G                       # append a newline.
  }                         # end of replacement.
:1                          # Once **one** replacement got done (the first match)
n                           # Loop continually reading a line each time
b1                          # and printing it by default.
'                           # end of sed script.
0
Isaac

Mit der Option -z von GNU sed können Sie die gesamte Datei so verarbeiten, als wäre sie nur eine Zeile. Auf diese Weise ersetzt ein s/…/…/ nur die erste Übereinstimmung in der gesamten Datei. Denken Sie daran: s/…/…/ ersetzt nur die erste Übereinstimmung in jeder Zeile, aber mit der -z-Option sed wird die gesamte Datei als eine einzige Zeile behandelt.

sed -z 's/#include/#include "newfile.h"\n#include'

Im Allgemeinen müssen Sie Ihren sed-Ausdruck neu schreiben, da der Musterbereich jetzt die gesamte Datei enthält und nicht nur eine Zeile. Einige Beispiele:

  • s/text.*// kann als s/text[^\n]*// umgeschrieben werden. [^\n] stimmt mit allem überein außer dem Zeilenvorschubzeichen. [^\n]* stimmt mit allen Symbolen nach text überein, bis eine neue Zeile erreicht wird.
  • s/^text// kann als s/(^|\n)text// umgeschrieben werden.
  • s/text$// kann als s/text(\n|$)// umgeschrieben werden.
0
Socowi

Der folgende Befehl entfernt das erste Vorkommen einer Zeichenfolge in einer Datei. Es entfernt auch die leere Zeile. Es wird in einer XML-Datei dargestellt, funktioniert aber mit jeder Datei. 

Nützlich, wenn Sie mit XML-Dateien arbeiten und ein Tag entfernen möchten. In diesem Beispiel wird das erste Vorkommen des Tags "isTag" entfernt.

Befehl: 

sed -e 0,/'<isTag>false<\/isTag>'/{s/'<isTag>false<\/isTag>'//}  -e 's/ *$//' -e  '/^$/d'  source.txt > output.txt

Quelldatei (source.txt)

<xml>
    <testdata>
        <canUseUpdate>true</canUseUpdate>
        <isTag>false</isTag>
        <moduleLocations>
            <module>esa_jee6</module>
            <isTag>false</isTag>
        </moduleLocations>
        <node>
            <isTag>false</isTag>
        </node>
    </testdata>
</xml>

Ergebnisdatei (output.txt)

<xml>
    <testdata>
        <canUseUpdate>true</canUseUpdate>
        <moduleLocations>
            <module>esa_jee6</module>
            <isTag>false</isTag>
        </moduleLocations>
        <node>
            <isTag>false</isTag>
        </node>
    </testdata>
</xml>

ps: Unter Solaris SunOS 5.10 (ziemlich alt) funktionierte es nicht für mich, aber unter Linux 2.6, Sed-Version 4.1.5