Wie verwende ich Regex mit AWK zum Ersetzen von Strings?

13

Angenommen, es gibt Text aus einer Datei:

(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)

Ich möchte zu jeder Zahl 11 addieren, gefolgt von einem "in jeder Zeile, wenn es eine gibt, dh

(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)

Hier ist meine Lösung mit GNU AWK und Regex:

awk -F'#' 'NF>1{gsub(/"(\d+)\""/, "\1+11\"")}'

dh ich möchte ersetzen (\d+)\"durch \1+10\", wo \1die Gruppe darstellt (\d+). Aber es geht nicht. Wie kann ich es zum Laufen bringen?

Wenn Gawk nicht die beste Lösung ist, was kann sonst noch verwendet werden?

StackExchange für alle
quelle
Entschuldigung für die Vervielfältigung. Aber ich habe zuerst nach Stackoverflow gefragt und keine zufriedenstellende Antwort erhalten, also habe ich für die Migration markiert. Aber es ist eine Weile nicht passiert, also habe ich nicht damit gerechnet und dann auf Unix.SE gefragt.
StackExchange für alle

Antworten:

12

Versuchen Sie dies (Gawk wird benötigt).

awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}' YourFile

Testen Sie mit Ihrem Beispiel:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 2" "#2")
("Exercises 30" "#30")
("Notes and References 34" "#34"))
)
'|awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}'   
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 13" "#13")
("Exercises 41" "#41")
("Notes and References 45" "#45"))
)

Beachten Sie, dass dieser Befehl nicht funktioniert, wenn sich die beiden Zahlen (z. B. 1 "und" # 1 ") unterscheiden oder wenn sich mehrere Zahlen in derselben Zeile mit diesem Muster befinden (z. B. 23" ... 32 "..." # 123 ") in einer Zeile.


AKTUALISIEREN

Da @Tim (OP) angibt, dass die Zahl, die "in derselben Zeile steht, unterschiedlich sein kann, habe ich einige Änderungen an meiner vorherigen Lösung vorgenommen und sie für Ihr neues Beispiel funktionsfähig gemacht.

Übrigens, aufgrund des Beispiels habe ich das Gefühl, dass es sich um ein Inhaltsverzeichnis handeln könnte, sodass ich nicht sehe, wie sich die beiden Zahlen unterscheiden könnten. Das erste wäre die gedruckte Seitennummer und das zweite mit # wäre der Seitenindex. Habe ich recht?

Wie auch immer, Sie kennen Ihre Anforderungen am besten. Jetzt die neue Lösung, immer noch mit Gawk (ich teile den Befehl in Zeilen auf, um das Lesen zu erleichtern):

awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}' yourFile

teste mit deinem neuen Beispiel:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)
'|awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}'                        
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)


EDIT2 basierend auf @Tims Kommentar

(1) Bedeutet FS = OFS = "\" \ "#", dass das Feldtrennzeichen in Eingabe und Ausgabe doppelte Anführungszeichen, Leerzeichen, doppelte Anführungszeichen und # ist? Warum doppeltes Anführungszeichen zweimal angeben?

Sie sind sowohl im Eingabe- als auch im Ausgabeteil für das Trennzeichen richtig. Es definierte Trennzeichen als:

" "#

Es gibt zwei doppelte Anführungszeichen, da es einfacher ist, die beiden gewünschten Zahlen zu erfassen (basierend auf Ihrer Beispieleingabe).

(2) Bedeutet $ in /.* ([0-9] +) $ / das Ende der Zeichenfolge?

Genau!

(3) Was ist der Unterschied zwischen "g" und "G" im dritten Argument von gensub ()? Es gibt keinen Unterschied zwischen G und g. Überprüfen Sie dies heraus:

gensub(regexp, replacement, how [, target]) #
    Search the target string target for matches of the regular expression regexp. 
    If "how" is a string beginning with g or G (short for global”), then 
        replace all matches of regexp with replacement.

Dies ist von http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . Sie können lesen, um eine detaillierte Verwendung von gensub zu erhalten.

Kent
quelle
Vielen Dank! Ich frage mich, wie es funktioniert, wenn die beiden Zahlen 1 "und" # 1 "unterschiedlich sind?
StackExchange for All
Diese Antwort funktioniert für Ihre aktuelle Anforderung / Ihr aktuelles Beispiel. Wenn sich die Anforderung ändert, können Sie die Frage möglicherweise bearbeiten und ein besseres Beispiel geben. und aus Ihrem Code awk -F'#'scheint, dass Sie die Änderung nur für den Teil nach dem '#' vornehmen möchten?
Kent
Danke für Ihren Vorschlag. Ich habe mein Beispiel so geändert, dass die beiden Zahlen nicht gleich sind.
StackExchange for All
@ Tim siehe meine aktualisierte Antwort, für Ihr neues Beispiel.
Kent
Vielen Dank! Einige Fragen: (1) Bedeutet, FS=OFS="\" \"#"dass das Feldtrennzeichen in Eingabe und Ausgabe doppelte Anführungszeichen, Leerzeichen, doppelte Anführungszeichen und # ist? warum doppeltes Anführungszeichen zweimal angeben? (2) in /.* ([0-9]+)$/, $bedeutet das Ende der Zeichenkette? (3) Was ist der Unterschied zwischen "g"und im dritten Argument von gensub () "G"?
StackExchange for All
7

Im Gegensatz zu fast jedem Tool, das reguläre Ausdrücke ersetzt, lässt awk keine Rückverweise wie \1im Ersatztext zu. GNU Awk ermöglicht den Zugriff auf übereinstimmende Gruppen, wenn Sie die matchFunktion verwenden , jedoch nicht mit ~oder suboder gsub.

Beachten Sie auch, dass \1Ihr Snippet , selbst wenn es unterstützt wird, die Zeichenfolge anhängt +11und keine numerische Berechnung durchführt. Außerdem stimmt Ihr regulärer Ausdruck nicht ganz, Sie passen zu Dingen wie "42""und nicht "#42".

Hier ist eine awk-Lösung (Warnung, ungetestet). Es wird nur ein einziger Austausch pro Zeile durchgeführt.

awk '
  match($0, /"#[0-9]+"/) {
    n = substr($0, RSTART+2, RLENGTH-3) + 11;
    $0 = substr($0, 1, RSTART+1) n substr($0, RSTART+RLENGTH-1)
  }
  1 {print}'

In Perl wäre es einfacher.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'
Gilles 'SO - hör auf böse zu sein'
quelle
Der erste Satz Ihrer Antwort ist genau das, wonach ich gesucht habe. Die Tatsache, dass Sie "... im Ersetzungstext" gesagt haben, wirft jedoch die folgende Frage auf: Erlaubt awk Rückverweise im Regex-Muster selbst?
Wildcard
1
@Wildcard Nein, awk verfolgt einfach keine Gruppen (mit Ausnahme der GNU-Erweiterung, die ich erwähne).
Gilles 'SO- hör auf böse zu sein'
5

awkkann es, aber es ist nicht direkt, auch mit Rückverweisen.
GNU awk hat eine (teilweise) Rückreferenzierung in Form von gensub .

Instanzen von 123"werden vorübergehend eingepackt \x01und \x02als unmodifiziert markiert (z sub(). B. co

Oder Sie können einfach durch die Schleife gehen und dabei die Kandidaten wechseln. In diesem Fall sind die Rückverweise und "Klammern" nicht erforderlich. Es ist jedoch erforderlich, den Zeichenindex im Auge zu behalten.

awk '{$0=gensub(/([0-9]+)\"/, "\x01\\1\"\x02", "g", $0 )
      while ( match($0, /\x01[0-9]+\"\x02/) ) {
        temp=substr( $0, RSTART, RLENGTH )
        numb=substr( temp, 2, RLENGTH-3 ) + 11
        sub( /\x01[0-9]+\"\x02/, numb "\"" ) 
      } print }'

Hier ist eine andere Art und Weise, unter Verwendung gensubund Anordnung splitund \x01als ein Feldtrennzeichen (für Split ) .. \ x02 Markierungen ein Array - Element als ein Kandidat für die arithmetische Addition.

awk 'BEGIN{ ORS="" } {
     $0=gensub(/([0-9]+)\"/, "\x01\x02\\1\x01\"", "g", $0 )
     split( $0, a, "\x01" )
     for (i=0; i<length(a); i++) { 
       if( substr(a[i],1,1)=="\x02" ) { a[i]=substr(a[i],2) + 11 }
       print a[i]
     } print "\n" }'
Peter.O
quelle
Vielen Dank! Was bedeutet in Ihrem ersten Code (1) "\x01\\1\"\x02"? Ich verstehe immer noch nicht \x01und \x02. (2) Wie unterschiedlich ist die Rückkehr $0von gensubund die $0als letztes Argument gensub?
StackExchange for All
@Tim. Die Hex-Werte \x01und \x02werden als Substitutionsmarker verwendet. Es ist sehr unwahrscheinlich, dass diese Werte in einer normalen Textdatei enthalten sind, daher sind sie auch "sehr" sicher zu verwenden (dh sie stoßen nicht auf einen Konflikt mit bereits vorhandenen). Es handelt sich lediglich um temporäre Bezeichnungen $0=gensub(... $0). Siehe hierzu verknüpfe String-Manipulationsfunktionen , aber zusammenfassend: Es (gensub) gibt den modifizierten String als Ergebnis der Funktion zurück und der ursprüngliche Ziel-String wird nicht geändert. ... Das $0=ändert einfach das ursprüngliche Ziel ..
Peter.O
2

Da die Lösungen in (g) awk recht komplex zu sein scheinen, wollte ich in Perl eine alternative Lösung hinzufügen:

perl -wpe 's/\d+(?=")/$&+11/eg' < in.txt > out.txt

Erläuterung:

  • Option -waktiviert Warnungen (die Sie vor möglichen unerwünschten Auswirkungen warnen).
  • Option -pimpliziert eine Schleife um den Code, die sed oder awk ähnelt und jede Eingabezeile automatisch in der Standardvariablen speichert $_.
  • Die Option -eteilt Perl mit, dass der Programmcode in der Befehlszeile und nicht in einer Skriptdatei folgt.
  • Bei dem Code handelt es sich um eine Regex-Ersetzung ( s/.../.../) an $_, bei der eine Ziffernfolge, wenn sie von einem gefolgt "wird, durch die Folge ersetzt wird, die als Zahl in der Addition plus 11 interpretiert wird.
  • Die positive Look-Ahead-Behauptung mit (?=pattern) der Breite Null sucht die, "ohne sie in das Match aufzunehmen, sodass wir sie bei der Ersetzung nicht wiederholen müssen. Die MATCH-Variable $&in der Ersetzung enthält dann nur die Nummer.
  • Der /eRegex-Modifikator weist perlan, die Ersetzung als Code "auszuführen", anstatt sie als Zeichenfolge zu verwenden.
  • Der /gModifikator macht die Ersetzung "global" und wiederholt sie bei jeder Übereinstimmung in der Zeile.

Die MATCH-Variable $&wird die Code-Performance in Perl-Versionen vor 5.20 leider beeinträchtigen. Eine schnellere (und nicht viel komplexere) Lösung würde $1stattdessen die Gruppierung und den Rückverweis verwenden:

perl -wpe 's/(\d+)?="/$1+11/eg' < in.txt > out.txt

Und wenn die Vorausschau-Behauptung zu verwirrend aussieht, können Sie das Anführungszeichen auch explizit ersetzen:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Dubu
quelle