Nicht gieriges Match mit SED-Regex (Perl emulieren. *?)

17

Ich möchte verwenden sed, um irgendetwas in einer Zeichenfolge zwischen dem ersten ABund dem ersten Vorkommen von AC(einschließlich) durch zu ersetzen XXX.

Zum Beispiel habe ich diese Zeichenfolge (diese Zeichenfolge ist nur für einen Test):

ssABteAstACABnnACss

und ich möchte eine Ausgabe wie folgt aus : ssXXXABnnACss.


Ich habe das gemacht mit perl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

aber ich möchte es mit implementieren sed. Folgendes (unter Verwendung des Perl-kompatiblen regulären Ausdrucks) funktioniert nicht:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
بارپابابا
quelle
2
Das ergibt keinen Sinn. Sie haben eine funktionierende Lösung in Perl, möchten aber Sed verwenden. Warum?
Kusalananda

Antworten:

12

Sed Regexes entsprechen der längsten Übereinstimmung. Sed hat kein Äquivalent zu nicht gierig.

Offensichtlich wollen wir Match machen

  1. AB,
    Gefolgt von
  2. eine beliebige Menge von etwas anderem als AC,
    gefolgt von
  3. AC

Leider sedkann # 2 nicht ausgeführt werden - zumindest nicht für einen regulären Ausdruck mit mehreren Zeichen. Natürlich können wir für einen einzelnen regulären Ausdruck wie @(oder sogar [123]) [^@]*oder tun [^123]*. Und so können wir die Einschränkungen von sed umgehen, indem wir alle Vorkommen von ACto ändern @und danach suchen

  1. AB,
    Gefolgt von
  2. eine beliebige Anzahl von etwas anderem als @,
    gefolgt von
  3. @

so was:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

Der letzte Teil ändert nicht übereinstimmende Instanzen von @back zu AC.

Dies ist natürlich ein rücksichtsloser Ansatz, da die Eingabe bereits @Zeichen enthalten kann. Wenn Sie also übereinstimmen, erhalten Sie möglicherweise falsch positive Ergebnisse. Da jedoch keine Shell-Variable jemals ein NUL ( \x00) -Zeichen enthalten wird, ist NUL wahrscheinlich ein gutes Zeichen für die oben beschriebene Problemumgehung anstelle von @:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

Die Verwendung von NUL erfordert GNU sed. (Um sicherzustellen, dass GNU-Funktionen aktiviert sind, darf der Benutzer die Shell-Variable POSIXLY_CORRECT nicht gesetzt haben.)

Wenn Sie sed mit dem GNU- -zFlag verwenden, um durch NUL getrennte Eingaben wie die Ausgabe von zu verarbeiten find ... -print0, befindet sich NUL nicht im Musterbereich, und NUL ist hier eine gute Wahl für die Substitution.

Obwohl NUL nicht in einer Bash-Variablen enthalten sein kann, ist es möglich, es in einen printfBefehl aufzunehmen. Wenn Ihre Eingabezeichenfolge überhaupt ein Zeichen enthalten kann, einschließlich NUL, lesen Sie die Antwort von Stéphane Chazelas, die eine clevere Escape-Methode hinzufügt.

John1024
quelle
Ich habe gerade Ihre Antwort bearbeitet, um eine ausführliche Erklärung hinzuzufügen. Sie können es zuschneiden oder zurückrollen.
G-Man sagt, dass Monica
@ G-Man Das ist eine hervorragende Erklärung! Sehr schön gemacht. Vielen Dank.
John1024
Sie können echooder printfein `\ 000 'ganz gut in Bash (oder die Eingabe könnte aus einer Datei stammen). Aber im Allgemeinen ist es natürlich nicht wahrscheinlich, dass eine Textfolge NULs enthält.
Ilkkachu
@ilkkachu Da hast du recht. Was ich hätte schreiben sollen, ist, dass keine Shell- Variable oder ein Parameter NULs enthalten kann. Antwort aktualisiert.
John1024
Wäre das nicht viel sicherer, wenn Sie ACzu AC@und zurück wechseln würden?
Michael Vehrs
7

Einige sedImplementierungen unterstützen dies. ssedhat einen PCRE-Modus:

ssed -R 's/AB.*?AC/XXX/g'

AT & T ast sed hat Konjunktion und Negation, wenn erweiterte reguläre Ausdrücke verwendet werden :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

Portabel können Sie diese Technik anwenden: Ersetzen Sie die Endzeichenfolge (hier AC) durch ein einzelnes Zeichen, das weder in der Anfangs- noch in der Endzeichenfolge (wie :hier) vorkommt s/AB[^:]*://, und falls dieses Zeichen in der Eingabe vorkommt Verwenden Sie einen Escape-Mechanismus, der nicht mit den Anfangs- und Endzeichenfolgen kollidiert.

Ein Beispiel:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

Bei GNU sedbesteht ein Ansatz darin, Newline als Ersatzzeichen zu verwenden. Da sedeine Zeile nach der anderen verarbeitet wird, wird im Musterbereich niemals eine neue Zeile eingefügt, sodass Folgendes möglich ist:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

Bei anderen sedImplementierungen funktioniert das im Allgemeinen nicht , da sie nicht unterstützt werden [^\n]. Bei GNU müssen sedSie sicherstellen, dass die POSIX-Kompatibilität nicht aktiviert ist (wie bei der Umgebungsvariablen POSIXLY_CORRECT).

Stéphane Chazelas
quelle
5

Nein, sed reguläre Ausdrücke haben keine nicht gierigen Übereinstimmungen.

Sie können den gesamten Text bis zum ersten Auftreten von ACabgleichen, indem Sie "alles , was nichts enthält AC" gefolgt von verwenden AC. Dies entspricht dem von Perl .*?AC. Die Sache ist, dass „alles, was nichts enthält AC“ nicht einfach als regulärer Ausdruck ausgedrückt werden kann: Es gibt immer einen regulären Ausdruck, der die Negation eines regulären Ausdrucks erkennt, aber der Negations-Regex wird schnell kompliziert. Und in portablem sed ist dies überhaupt nicht möglich, da der Negations-Regex die Gruppierung einer Alternative erfordert, die in erweiterten regulären Ausdrücken (z. B. in awk), aber nicht in portablen regulären Grundausdrücken vorhanden ist. Einige Versionen von sed, wie GNU sed, haben Erweiterungen für BRE, mit denen alle möglichen regulären Ausdrücke ausgedrückt werden können.

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

Aufgrund der Schwierigkeit, einen regulären Ausdruck zu negieren, lässt sich dies nicht gut verallgemeinern. Stattdessen können Sie die Linie vorübergehend transformieren. In einigen sed-Implementierungen können Sie Zeilenumbrüche als Marker verwenden, da diese nicht in einer Eingabezeile angezeigt werden können (und wenn Sie mehrere Marker benötigen, verwenden Sie Zeilenumbrüche gefolgt von einem unterschiedlichen Zeichen).

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

Beachten Sie jedoch, dass Backslash-Newline in einigen sed-Versionen in einem Zeichensatz nicht funktioniert. Dies funktioniert insbesondere nicht in GNU sed, der sed-Implementierung unter nicht eingebettetem Linux. in GNU sed können Sie \nstattdessen verwenden:

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

In diesem speziellen Fall reicht es aus, die erste ACZeile durch eine neue zu ersetzen . Der Ansatz, den ich oben vorgestellt habe, ist allgemeiner.

Ein leistungsfähigerer Ansatz in sed besteht darin, die Linie im Haltebereich zu speichern, alle bis auf den ersten „interessanten“ Teil der Linie zu entfernen, den Haltebereich und den Musterbereich auszutauschen oder den Musterbereich an den Haltebereich anzuhängen und zu wiederholen. Wenn Sie jedoch anfangen, so komplizierte Dinge zu tun, sollten Sie sich wirklich überlegen, auf awk umzusteigen. Awk hat auch keine nicht gierigen Übereinstimmungen, aber Sie können eine Zeichenfolge teilen und die Teile in Variablen speichern.

Gilles 'SO - hör auf böse zu sein'
quelle
@ilkkachu Nein, das tut es nicht. s/\n//gEntfernt alle Zeilenumbrüche.
Gilles 'SO- hör auf böse zu sein'
asdf. Richtig, mein schlechtes.
Ilkkachu
3

sed - nicht gieriges Matching von Christoph Sieghart

Der Trick, um nicht gierige Übereinstimmungen in sed zu erhalten, besteht darin, alle Zeichen mit Ausnahme desjenigen, der die Übereinstimmung beendet, abzugleichen. Ich weiß, ein Kinderspiel, aber ich habe wertvolle Minuten damit verschwendet und Shell-Skripte sollten schließlich schnell und einfach sein. Für den Fall, dass jemand anderes es braucht:

Gieriges Matching

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Nicht gieriges Matching

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

gresolio
quelle
2
Der Begriff "no-brainer" ist mehrdeutig. In diesem Fall ist nicht klar, dass Sie (oder Christoph Sieghart) dies durchdacht haben. Insbesondere wäre es schön gewesen , wenn man gezeigt hatte , wie das spezifische Problem in der Frage zu lösen (wenn der Null-of-mehr-of - Ausdruck gefolgt von mehr als ein Zeichen ) . Sie können feststellen, dass diese Antwort in diesem Fall nicht gut funktioniert.
Scott
Das Kaninchenloch ist viel tiefer als es mir auf den ersten Blick schien. Sie haben Recht, diese Problemumgehung eignet sich nicht für reguläre Ausdrücke mit mehreren Zeichen.
Gresolio
0

In Ihrem Fall können Sie das schließende Zeichen einfach auf folgende Weise negieren:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
midori
quelle
2
Die Frage lautet: "Ich möchte alles zwischen dem ersten ABund dem ersten Auftreten von ACdurch XXX... ersetzen " und gibt ssABteAstACABnnACssals Beispiel eine Eingabe. Diese Antwort funktioniert für dieses Beispiel , beantwortet aber die Frage im Allgemeinen nicht. Zum Beispiel ssABteCstACABnnACsssollte auch die Ausgabe ergeben aaXXXABnnACss, aber Ihr Befehl durchläuft diese Zeile unverändert.
G-Man sagt, dass Monica