Jak używać wyrażenia regularnego z AWK do zamiany ciągów?

13

Załóżmy, że jest jakiś tekst z pliku:

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

Chcę dodać 11 do każdego numeru, a następnie "w każdym wierszu, jeśli taki jest, tj

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

Oto moje rozwiązanie, używając GNU AWK i regex:

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

to znaczy, chcę wymienić (\d+)\"z \1+10\", gdzie \1jest grupa reprezentująca (\d+). Ale to nie działa. Jak mogę to zrobić?

Jeśli gawk nie jest najlepszym rozwiązaniem, czego jeszcze można użyć?

Tim
źródło
Przepraszam za powielanie. Ale najpierw zapytałem o przepełnienie stosu i nie otrzymałem zadowalającej odpowiedzi, więc zgłosiłem się do migracji. Ale tak się nie stało, więc nie spodziewałem się, że tak się stanie, a potem zapytałem na Unix.SE.
Tim

Odpowiedzi:

12

Spróbuj tego (gawk jest potrzebny).

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

Przetestuj na swoim przykładzie:

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"))
)

Zauważ, że to polecenie nie będzie działać, jeśli dwie liczby (np. 1 ”i„ # 1 ”) są różne. Lub w tym samym wierszu jest więcej liczb z tym wzorem (np. 23„ ... 32 ”...” # 123 ") w jednym wierszu.


AKTUALIZACJA

Ponieważ @Tim (OP) powiedział, że liczba, po której następuje ta "sama linia, może być inna, wprowadziłem pewne zmiany w moim poprzednim rozwiązaniu i sprawiłem , że działało w twoim nowym przykładzie.

BTW, z przykładu uważam, że może to być tabela struktury treści, więc nie rozumiem, jak te dwie liczby mogą się różnić. Pierwszy to wydrukowany numer strony, a drugi z # to indeks strony. Czy mam rację?

W każdym razie najlepiej znasz swoje wymagania. Teraz nowe rozwiązanie, wciąż z gawk (dzielę polecenie na linie, aby ułatwić czytanie):

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

przetestuj na nowym przykładzie:

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 na podstawie komentarza @Tim

(1) Czy FS = OFS = "\" \ "#" oznacza, że ​​separatorem pola na wejściu i wyjściu jest podwójny cudzysłów, spacja, podwójny cudzysłów i #? Po co podawać podwójną wycenę dwa razy?

Masz rację dla separatora zarówno w części wejściowej, jak i wyjściowej. Zdefiniował separator jako:

" "#

Istnieją dwa podwójne cudzysłowy, ponieważ łatwiej jest złapać dwie pożądane liczby (na podstawie przykładowego wejścia).

(2) W /.* ([0-9] +) $ /, czy $ oznacza koniec ciągu?

Dokładnie!

(3) Jaka jest różnica między „g” a „G” w trzecim argumencie gensub ()? nie ma różnicy między G i g. Sprawdź to:

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.

To jest z http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . możesz przeczytać, aby uzyskać szczegółowe informacje na temat korzystania z gensub.

Kent
źródło
Dzięki! Zastanawiam się, jak zrobić to działać, jeśli dwie liczby np 1" i «1» są różne?
Tym
ta odpowiedź działa dla twojego aktualnego wymagania / przykładu. jeśli wymaganie zostanie zmienione, być może mógłbyś edytować pytanie i podać lepszy przykład. a ze swojego kodu awk -F'#'wydaje się, że chcesz wprowadzić zmiany tylko po „#”?
Kent
Dzięki za Twoją sugestię. Właśnie zmodyfikowałem mój przykład, aby dwie liczby nie były takie same.
Tim
@ Tym razem zobacz moją zaktualizowaną odpowiedź na nowy przykład.
Kent
Dzięki! Niektóre pytania: (1) FS=OFS="\" \"#"oznacza, że ​​separatorem pola zarówno na wejściu, jak i na wyjściu jest podwójny cudzysłów, spacja, podwójny cudzysłów i #? po co podawać podwójną wycenę dwa razy? (2) w /.* ([0-9]+)$/, $oznacza koniec łańcucha? (3) w trzecim argumencie gensub () jaka jest różnica między "g"i "G"?
Tim
7

W przeciwieństwie do niemal każdego narzędzia, które zapewnia podstawienia wyrażeń regularnych, awk nie zezwala na odwołania wsteczne, takie jak \1w tekście zastępczym. GNU awk daje dostęp do grup dobranych w przypadku korzystania z matchfunkcji , ale nie z ~lub sublub gsub.

Zauważ też, że nawet jeśli \1byłby obsługiwany, twój fragment kodu dołączałby ciąg +11, nie wykonując obliczeń numerycznych. Ponadto wyrażenie regularne nie jest w porządku, dopasowujesz rzeczy takie jak "42""i nie "#42".

Oto rozwiązanie awk (ostrzeżenie, niesprawdzone). Wykonuje tylko jedną zamianę na linię.

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}'

W Perlu byłoby łatwiej.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'
Gilles „SO- przestań być zły”
źródło
Pierwsze zdanie twojej odpowiedzi jest dokładnie tym, czego szukałem. Jednak fakt, że powiedziałeś „... w tekście zastępczym” nasuwa kolejne pytanie: czy awk dopuszcza odwołania wsteczne w samym wzorcu wyrażeń regularnych?
Wildcard,
1
@Wildcard Nie, awk po prostu nie śledzi grup (oprócz wspomnianego rozszerzenia GNU).
Gilles „SO- przestań być zły”
5

awkmoże to zrobić, ale nie jest to bezpośrednie, nawet przy użyciu odsyłania wstecznego.
GNU awk ma (częściowe) odsyłanie zwrotne, w postaci gensub .

Instancje 123"są tymczasowo pakowane \x01i \x02oznaczane jako niezmodyfikowane (np sub(). Co

Lub możesz po prostu przejść przez pętlę zmieniając kandydatów w trakcie podróży, w którym to przypadku odsyłanie zwrotne i „nawiasy” nie są potrzebne; ale konieczne jest śledzenie indeksu postaci.

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 }'

Oto inny sposób: użycie gensubi tablica splitoraz \x01jako separator pola (do podziału ) .. \ x02 oznacza element tablicy jako kandydata do dodania arytmetycznego.

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
źródło
Dzięki! W twoim pierwszym kodzie (1) co "\x01\\1\"\x02"oznacza? Nadal nie rozumiem \x01i \x02. (2) na ile różni się zwrot $0przez gensubi $0od ostatniego argumentu gensub?
Tim
@Tim. Wartości szesnastkowe \x01i \x02są używane jako znaczniki podstawienia. Wartości te są bardzo mało prawdopodobne, aby być w każdym normalnym pliku tekstowym, więc są one w równym stopniu „bardzo” bezpieczne w użyciu (tzn. Nie wystąpi konflikt z tych istniejących wcześniej) .. Są etykiety tylko tymczasowe .. Re $0=gensub(... $0).. zobaczyć link Funkcje manipulacji ciągami , ale w skrócie: To (gensub) zwraca zmodyfikowany ciąg jako wynik funkcji, a pierwotny ciąg docelowy nie jest zmieniany. ... To $0=po prostu modyfikuje pierwotny cel.
Peter.O,
3

Ponieważ rozwiązania w (g) awk wydają się dość skomplikowane, chciałem dodać alternatywne rozwiązanie w Perlu:

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

Wyjaśnienie:

  • Opcja -wwłącza ostrzeżenia (które ostrzegają o możliwych niepożądanych efektach).
  • Opcja -pzakłada pętlę wokół kodu, który działa podobnie do sed lub awk, oszczędzając każdy wiersz wejścia automatycznie w zmiennej domyślnej $_.
  • Opcja -einformuje perla, że ​​kod programu podąża za wierszem poleceń, a nie w pliku skryptu.
  • Kod jest podstawieniem wyrażenia regularnego ( s/.../.../) $_, w którym ciąg cyfr, po którym następuje a ", zostanie zastąpiony przez ciąg interpretowany jako liczba w dodatku, plus 11.
  • W zerowej szerokości pozytywne antycypowana Twierdzenie (?=pattern) wyszukuje "bez zabraniem go na mecz, więc nie trzeba powtarzać go w wymianie. Zmienna MATCH $&w zastępstwie będzie wówczas zawierać tylko liczbę.
  • /eModyfikator do regex mówi perldo „execute” zastąpienia jako kod zamiast brać go jako ciąg znaków.
  • /gModyfikator powoduje zastąpienie „globalny”, powtarzając go na każdym meczu w wierszu.

Zmienna MATCH $&będzie niestety szkodliwa dla wydajności kodu w wersjach Perla przed 5.20. Szybsze (i niewiele bardziej skomplikowane) rozwiązanie użyłoby $1zamiast tego grupowania i odwołań wstecznych :

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

A jeśli stwierdzenie dotyczące przyszłości wydaje się zbyt mylące, można również wyraźnie zastąpić znak cudzysłowu:

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