Sed - Ersetzt die ersten k Instanzen eines Wortes in der Datei

24

Ich möchte nur die ersten kInstanzen eines Wortes ersetzen .

Wie kann ich das machen?

Z.B. Angenommen, die Datei foo.txtenthält 100 Instanzen des Wortes "Linux".

Ich muss nur die ersten 50 Vorkommen ersetzen.

Narendra-Choudhary
quelle
1
Sie können sich darauf beziehen: unix.stackexchange.com/questions/21178/…
cuonglm
Benötigen Sie sed speziell oder sind andere Werkzeuge akzeptabel? Müssen Sie an der Befehlszeile arbeiten oder ist ein Texteditor akzeptabel?
Evilsoup
Alles, was auf der Kommandozeile funktioniert, ist akzeptabel.
Narendra-Choudhary

Antworten:

31

Der erste Abschnitt beschreibt, sedwie Sie die ersten k-Vorkommen in einer Zeile ändern. Der zweite Abschnitt erweitert diesen Ansatz, um nur die ersten k-Vorkommen in einer Datei zu ändern, unabhängig davon, in welcher Zeile sie erscheinen.

Linienorientierte Lösung

Mit standard sed gibt es einen Befehl, um das k-te Vorkommen eines Wortes in einer Zeile zu ersetzen. Wenn k3 ist, zum Beispiel:

sed 's/old/new/3'

Oder man kann alle Vorkommen ersetzen durch:

sed 's/old/new/g'

Beides ist nicht das, was Sie wollen.

GNU sedbietet eine Erweiterung, die das k-te Vorkommen und alles danach ändert. Wenn k 3 ist, zum Beispiel:

sed 's/old/new/g3'

Diese können kombiniert werden, um das zu tun, was Sie wollen. So ändern Sie die ersten 3 Vorkommen:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old

Wo \nist hier nützlich, weil wir sicher sein können, dass es nie in einer Zeile auftritt.

Erläuterung:

Wir verwenden drei sedSubstitutionsbefehle:

  • s/\<old\>/\n/g4

    Dies ist die GNU-Erweiterung, um das vierte und alle nachfolgenden Vorkommen von oldmit zu ersetzen \n.

    Die erweiterte Regex-Funktion \<wird verwendet, um den Wortanfang und \>das Wortende abzugleichen. Dies stellt sicher, dass nur vollständige Wörter gefunden werden. Erweiterte reguläre Ausdrücke erfordern die -EOption zu sed.

  • s/\<old\>/new/g

    Es oldbleiben nur die ersten drei Vorkommen von übrig, und dies ersetzt sie alle durch new.

  • s/\n/old/g

    Das vierte und alle übrigen Vorkommen von oldwurden \nim ersten Schritt durch ersetzt. Dies bringt sie in ihren ursprünglichen Zustand zurück.

Nicht-GNU-Lösung

Wenn GNU sed nicht verfügbar ist und Sie die ersten drei Vorkommen von oldto ändern möchten new, verwenden Sie drei sBefehle:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old

Dies funktioniert gut, wenn kes sich um eine kleine Zahl handelt, die jedoch schlecht bis groß skaliert k.

Da einige Nicht-GNU-Seds das Kombinieren von Befehlen mit Semikolons nicht unterstützen, wird hier jeder Befehl mit einer eigenen -eOption eingeführt. Es kann auch erforderlich sein, zu überprüfen, ob Ihr seddie Wortbegrenzungssymbole \<und unterstützt \>.

Dateiorientierte Lösung

Wir können sed anweisen, die gesamte Datei einzulesen und dann die Ersetzungen vorzunehmen. Um zum Beispiel die ersten drei Vorkommen oldeiner BSD-artigen sed zu ersetzen :

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'

Die sed-Befehle H;1h;$!d;xlesen die gesamte Datei ein.

Da die oben genannten keine GNU-Erweiterung verwenden, sollte es auf BSD (OSX) sed funktionieren. Beachten Sie, dass dieser Ansatz ein erfordert sed, das lange Zeilen verarbeiten kann. GNU sedsollte in Ordnung sein. Wer eine Nicht-GNU-Version von verwendet, sedsollte seine Fähigkeit testen, lange Leitungen zu handhaben.

Mit einem GNU-Sed können wir den goben beschriebenen Trick weiter verwenden , aber durch \nersetzt \x00, um die ersten drei Vorkommen zu ersetzen:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'

Dieser Ansatz skaliert gut und kwird groß. Dies setzt jedoch voraus, dass dies \x00nicht in Ihrer ursprünglichen Zeichenfolge enthalten ist. Da es unmöglich ist, das Zeichen \x00in eine Bash-Zeichenfolge einzufügen, ist dies normalerweise eine sichere Annahme.

John1024
quelle
5
Dies funktioniert nur für Zeilen und ändert die ersten 4 Vorkommen in jeder Zeile
1
@mikeserv Ausgezeichnete Idee! Antwort aktualisiert.
John1024
(1) Sie erwähnen GNU und Nicht-GNU sed und schlagen vor tr '\n' '|' < input_file | sed …. Aber das wandelt natürlich die gesamte Eingabe in eine Zeile um, und einige Nicht-GNU-Seds können nicht mit beliebig langen Zeilen umgehen. (2) Sie sagen: „… oben sollte die in Anführungszeichen stehende Zeichenfolge '|'durch ein beliebiges Zeichen oder eine Zeichenfolge ersetzt werden.“ Sie können jedoch kein trZeichen durch eine Zeichenfolge (mit einer Länge> 1) ersetzen. (3) In Ihrem letzten Beispiel sagen Sie -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Dies scheint ein Tippfehler für zu sein -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man sagt, dass Monica
@ G-Man Vielen Dank! Ich habe die Antwort aktualisiert.
John1024
das ist so hässlich
Louis Maddox
8

Awk verwenden

Die awk-Befehle können verwendet werden, um die ersten N Vorkommen des Wortes durch die Ersetzung zu ersetzen.
Die Befehle werden nur ersetzt, wenn das Wort vollständig übereinstimmt.

In den folgenden Beispielen ersetze ich die ersten 27Vorkommen von olddurchnew

Unter Verwendung

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file

Dieser Befehl durchläuft jedes Feld, bis es übereinstimmt old, überprüft, ob der Zähler unter 27 liegt, inkrementiert und ersetzt die erste Übereinstimmung in der Zeile. Geht dann auf das nächste Feld / die nächste Zeile und wiederholt.

Ersetzen Sie das Feld manuell

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Ähnlich wie beim vorherigen Befehl, aber da es bereits einen Marker für das Feld gibt ($i), ändert es einfach den Wert des Felds von oldnach new.

Führen Sie vorher eine Überprüfung durch

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Wenn Sie überprüfen, ob die Zeile alt ist und der Zähler unter 27 liegt SHOULD, erhöhen Sie die Geschwindigkeit geringfügig, da keine Zeilen verarbeitet werden, wenn diese falsch sind.

ERGEBNISSE

Z.B

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old

zu

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old
Jeff Schaller
quelle
Der erste (mit sub) macht das Falsche, wenn der String "alt" vor dem * Wort alt steht; zB : „Geben Sie etwas Gold auf den alten Mann.“ → „Geben Sie einige GNEW dem alten Mann.“
G-Man sagt "wieder einzusetzen Monica
@ G-Man Ja, ich habe das $ibisschen vergessen , es wurde bearbeitet, danke :)
7

Angenommen, Sie möchten nur die ersten drei Instanzen einer Zeichenfolge ersetzen ...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters

Hinweis: Das oben Genannte funktioniert wahrscheinlich nicht mit eingebetteten Kommentaren
... oder in meinem Beispiel mit einer '1' ...

AUSGABE:

22
211
211
311

Dort verwende ich zwei bemerkenswerte Techniken. Zunächst wird jedes Vorkommen 1einer Zeile durch ersetzt \n1. Auf diese Weise kann ich beim nächsten rekursiven Ersetzen sicher sein, dass das Vorkommen nicht zweimal ersetzt wird, wenn meine Ersetzungszeichenfolge meine Ersetzungszeichenfolge enthält. Zum Beispiel, wenn ich ersetzen hemit heyihm wird immer noch funktionieren.

Ich mache das wie:

s/1/\
&/g

Zweitens zähle ich die Ersetzungen, indem ich hbei jedem Auftreten ein Zeichen in das alte Feld einfüge. Sobald ich drei bin, tritt nichts mehr auf. Wenn Sie dies auf Ihre Daten anwenden und \{3\}die Anzahl der von Ihnen gewünschten Ersetzungen und die /\n1/Adressen der zu ersetzenden Adressen ändern , sollten Sie nur so viele Ersetzungen vornehmen, wie Sie möchten.

Ich habe das ganze -eZeug nur zur besseren Lesbarkeit gemacht. POSIXly Es könnte so geschrieben werden:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"

Und w / GNU sed:

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

Denken Sie auch daran, dass dies sedzeilenorientiert ist - es liest nicht die gesamte Datei ein und versucht dann, eine Schleife darüber zu erstellen, wie dies in anderen Editoren häufig der Fall ist. sedist einfach und effizient. Trotzdem ist es oft praktisch, Folgendes zu tun:

Hier ist eine kleine Shell-Funktion, die sie zu einem einfach ausgeführten Befehl zusammenfasst:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }

Also damit kann ich machen:

seq 11 100 311 | firstn 7 1 5

...und bekomme...

55
555
255
311

...oder...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

...bekommen...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

... oder, um Ihrem Beispiel zu entsprechen (in einer kleineren Größenordnung) :

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux
mikeserv
quelle
4

Eine kurze Alternative in Perl:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

Ändern Sie den Wert von `$ n $ nach Ihren Wünschen.

Wie es funktioniert:

  • Für jede Linie, hält es versucht , ersetzen newfür old( s/old/new/) und wann immer sie kann, erhöht er die Variable $i( ++$i).
  • Es arbeitet 1 while ...so lange an der Zeile ( ), wie es $ninsgesamt weniger als Ersetzungen vorgenommen hat, und es kann in dieser Zeile mindestens eine Ersetzung vornehmen.
Joseph R.
quelle
4

Verwenden Sie eine Muschelschlaufe und ex!

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

Ja, es ist ein bisschen doof.

;)

Hinweis: Dies kann fehlschlagen, wenn olddie Datei weniger als 50 Instanzen von enthält. (Ich habe es nicht getestet.) In diesem Fall würde die Datei unverändert bleiben.


Besser noch, benutze Vim.

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

Erläuterung:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit
Platzhalter
quelle
: s // new <CR> sollte auch funktionieren, da ein leerer regulärer Ausdruck die zuletzt verwendete Suche
wiederverwendet
3

Eine einfache, aber nicht sehr schnelle Lösung besteht darin, die in /programming/148451/how-to-use-sed-to-replace-only-the-irst-occurrence-in-a beschriebenen Befehle zu durchlaufen -Datei

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

Dieser spezielle sed Befehl funktioniert wahrscheinlich nur für GNU sed und wenn newword nicht Teil von oldword ist . Für Nicht-GNU-Benutzer siehe hier, wie nur das erste Muster in einer Datei ersetzt wird.

Jofel
quelle
+1 für das Erkennen, dass das Ersetzen von "alt" durch "fett" Probleme verursachen kann.
G-Man sagt, dass Monica
2

Mit GNU können awkSie das Datensatztrennzeichen RSauf das Wort setzen, das durch Wortgrenzen getrennt werden soll. In diesem Fall wird das Datensatztrennzeichen in der Ausgabe auf das Ersatzwort für die ersten kDatensätze gesetzt, während das ursprüngliche Datensatztrennzeichen für den Rest beibehalten wird

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

ODER

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
iruvar
quelle