Nie-chciwy mecz z wyrażeniem regularnym SED (emulować perla. *?)

22

Chcę użyć, sedaby zastąpić cokolwiek w ciągu między pierwszym ABi pierwszym wystąpieniem AC(włącznie) z XXX.

Na przykład mam ten ciąg (ten ciąg jest tylko do testu):

ssABteAstACABnnACss

i chciałbym wynik podobny do tego: ssXXXABnnACss.


Zrobiłem to z perl:

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

ale chcę to zaimplementować sed. Następujące (przy użyciu wyrażenia regularnego zgodnego z Perl) nie działa:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
بارپابابا
źródło
2
To nie ma sensu. Masz działające rozwiązanie w Perlu, ale chcesz użyć Sed, dlaczego?
Kusalananda

Odpowiedzi:

16

Wyrażenia regularne Sed pasują do najdłuższego dopasowania. Sed nie ma odpowiednika niechcianego.

Oczywiście chcemy dopasować

  1. AB,
    a następnie
  2. dowolna ilość czegokolwiek poza AC,
    po której następuje
  3. AC

Niestety sednie można zrobić nr 2 - przynajmniej nie dla wyrażenia regularnego składającego się z wielu znaków. Oczywiście, dla wyrażenia regularnego zawierającego jeden znak, takiego jak @(lub nawet [123]), możemy zrobić [^@]*lub [^123]*. Możemy więc obejść ograniczenia sed, zmieniając wszystkie wystąpienia ACna, @a następnie szukając

  1. AB,
    a następnie
  2. dowolna liczba innych niż @,
    po których następuje
  3. @

lubię to:

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

Ostatnia część zmienia niedopasowane przypadki @powrotu do AC.

Ale, oczywiście, jest to lekkomyślne podejście, ponieważ dane wejściowe mogą już zawierać @znaki, więc dopasowując je, możemy uzyskać fałszywe alarmy. Ponieważ jednak żadna zmienna powłoki nigdy nie będzie zawierała znaku NUL ( \x00), NUL jest prawdopodobnie dobrym znakiem do użycia w powyższym obejściu zamiast @:

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

Korzystanie z NUL wymaga GNU sed. (Aby upewnić się, że funkcje GNU są włączone, użytkownik nie może ustawiać zmiennej powłoki POSIXLY_CORRECT.)

Jeśli używasz sed z -zflagą GNU do obsługi danych wejściowych oddzielonych przez NUL, takich jak dane wyjściowe find ... -print0, to NUL nie będzie w przestrzeni wzorców, a NUL jest dobrym wyborem do podstawienia tutaj.

Chociaż NUL nie może znajdować się w zmiennej bash, możliwe jest włączenie jej do printfpolecenia. Jeśli Twój ciąg wejściowy może w ogóle zawierać dowolny znak, w tym NUL, zobacz odpowiedź Stéphane Chazelas, która dodaje sprytną metodę ucieczki.

John1024
źródło
Właśnie zredagowałem twoją odpowiedź, aby dodać długie wyjaśnienie; możesz go przyciąć lub przywrócić.
G-Man mówi „Przywróć Monikę”
@ G-Man To doskonałe wyjaśnienie! Bardzo ładnie wykonane. Dziękuję Ci.
John1024,
Możesz echolub printf`\ 000 'w porządku w bash (lub dane wejściowe mogą pochodzić z pliku). Ale generalnie ciąg tekstu prawdopodobnie nie ma wartości NUL.
ilkkachu
@ilkkachu Masz rację. Powinienem napisać, że żadna zmienna powłoki lub parametr nie może zawierać wartości NUL. Odpowiedź zaktualizowana.
John1024,
Czy nie byłoby o wiele bezpieczniej, gdybyś zmienił ACsię AC@i wrócił?
Michael Vehrs,
7

Niektóre sedimplementacje mają na to wsparcie. ssedma tryb PCRE:

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

AT&T ast sed ma koniunkcję i negację podczas używania rozszerzonych wyrażeń regularnych :

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

Przenośnie możesz użyć tej techniki: zastąp ciąg końcowy (tutaj AC) pojedynczym znakiem, który nie występuje ani na początku, ani na końcu (jak :tutaj), abyś mógł to zrobić s/AB[^:]*://, a na wypadek, gdyby znak ten pojawił się na wejściu , użyj mechanizmu zmiany znaczenia, który nie koliduje z ciągami początkowym i końcowym.

Przykład:

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'

W GNU sed, podejście polega na użyciu znaku nowej linii jako znaku zastępującego. Ponieważ sedprzetwarza jedną linię na raz, nowa linia nigdy nie występuje w obszarze wzorców, więc można wykonać:

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

To na ogół nie działa z innymi sedimplementacjami, ponieważ nie obsługują [^\n]. W GNU sedmusisz się upewnić, że kompatybilność z POSIX nie jest włączona (jak w przypadku zmiennej środowiskowej POSIXLY_CORRECT).

Stéphane Chazelas
źródło
6

Nie, wyrażenia regularne sed nie mają nieprzystosowanego dopasowania.

Możesz dopasować cały tekst do pierwszego wystąpienia AC, używając „niczego niezawierającego AC”, po ACktórym następuje , co robi to samo co Perla .*?AC. Chodzi o to, że „niczego nie zawierającego AC” nie można łatwo wyrazić jako wyrażenie regularne: zawsze istnieje wyrażenie regularne, które rozpoznaje negację wyrażenia regularnego, ale wyrażenie regularne negacji szybko się komplikuje. A w przenośnym sed nie jest to w ogóle możliwe, ponieważ regex negacji wymaga zgrupowania alternacji występującej w rozszerzonych wyrażeniach regularnych (np. W awk), ale nie w przenośnych podstawowych wyrażeniach regularnych. Niektóre wersje sed, takie jak GNU sed, mają rozszerzenia BRE, które umożliwiają wyrażanie wszystkich możliwych wyrażeń regularnych.

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

Ze względu na trudność negowania wyrażenia regularnego nie uogólnia to dobrze. Zamiast tego możesz tymczasowo przekształcić linię. W niektórych implementacjach sed możesz używać znaków nowej linii jako znacznika, ponieważ nie mogą one pojawiać się w linii wejściowej (a jeśli potrzebujesz wielu znaczników, użyj nowej linii, po której następuje zmienny znak).

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

Uważaj jednak na to, że backslash-newline nie działa w zestawie znaków w niektórych wersjach sed. W szczególności nie działa to w GNU sed, który jest implementacją sed w niewbudowanym systemie Linux; w GNU sed możesz \nzamiast tego użyć :

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

W tym konkretnym przypadku wystarczy zastąpić pierwszy ACnowym znakiem. Podejście, które przedstawiłem powyżej, jest bardziej ogólne.

Bardziej potężnym podejściem w sed jest zapisanie linii w przestrzeni wstrzymania, usunięcie wszystkich oprócz pierwszej „interesującej” części linii, zamiana przestrzeni wstrzymania i przestrzeni wzorców lub dołączenie przestrzeni wzorców do przestrzeni wstrzymania i powtórzenie. Jeśli jednak zaczniesz robić rzeczy, które są tak skomplikowane, powinieneś naprawdę pomyśleć o przejściu na awk. Awk nie ma również chciwego dopasowania, ale możesz podzielić ciąg i zapisać części na zmienne.

Gilles „SO- przestań być zły”
źródło
@ilkkachu Nie, nie ma. s/\n//gusuwa wszystkie nowe wiersze.
Gilles 'SO - przestań być zły'
asdf. Racja, mój zły.
ilkkachu
3

sed - nie chciwe dopasowanie przez Christopha Siegharta

Sztuką, aby uzyskać nie chciwe dopasowanie w sed, jest dopasowanie wszystkich znaków z wyjątkiem tego, który kończy dopasowanie. Wiem, że to oczywiste, ale zmarnowałem na to cenne minuty, a skrypty powłoki powinny być w końcu szybkie i łatwe. Na wypadek, gdyby ktoś inny mógł go potrzebować:

Chciwe dopasowanie

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

Nie chciwe dopasowanie

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

gresolio
źródło
3
Termin „bezmyślny” jest niejednoznaczny. W tym przypadku nie jest jasne, że ty (lub Christoph Sieghart) to przemyślałeś. W szczególności, byłoby miło, gdybyś pokazał, jak rozwiązać ten problem specyficzny w pytaniu (gdzie wyrażenie zero-of-bardziej-of następuje przez więcej niż jeden znak ) . Może się okazać, że w takim przypadku ta odpowiedź nie działa dobrze.
Scott,
Królicza nora jest znacznie głębsza niż mi się wydawało na pierwszy rzut oka. Masz rację, to obejście nie działa dobrze w przypadku wyrażeń regularnych zawierających wiele znaków.
gresolio
0

W twoim przypadku możesz po prostu zanegować znak zamknięcia w ten sposób:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
midori
źródło
2
Pytanie brzmi: „Chcę zamienić cokolwiek między pierwszym ABa pierwszym wystąpieniem ACz XXX…” i podaje ssABteAstACABnnACssjako przykładowy wkład. Ta odpowiedź działa w tym przykładzie , ale ogólnie nie odpowiada na pytanie. Na przykład ssABteCstACABnnACsspowinien również dać wynik aaXXXABnnACss, ale twoje polecenie przechodzi przez ten wiersz bez zmian.
G-Man mówi „Przywróć Monikę”
0

Rozwiązanie jest dość proste. .*jest chciwy, ale nie jest absolutnie chciwy. Rozważ dopasowanie ssABteAstACABnnACssz wyrażeniem regularnym AB.*AC. ACŻe następuje .*powinno być rzeczywiście mecz. Problem polega na tym, że ponieważ .*jest chciwy, kolejne ACbędą pasować do ostatniego, AC a nie pierwszego. .*zjada pierwszy, ACpodczas gdy literał ACw wyrażeniu regularnym pasuje do ostatniego w ssABteAstACABnn AC ss. Aby temu zapobiec, po prostu zastąp pierwszy ACz czymś niedorzecznym, aby odróżnić go od drugiego i od wszystkiego innego.

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

Chciwy .*będzie teraz zatrzymać u stóp -foobar-w ssABteAst-foobar-ABnnACssbo nie ma innego -foobar-niż ten -foobar-, a regexp -foobar- MUSI mieć mecz. Poprzedni problem polegał na tym, że wyrażenie regularne ACmiało dwa dopasowania, ale ponieważ .*był zachłanny, ACwybrano ostatnie dopasowanie . Jednak z -foobar-, tylko jeden mecz jest możliwy, a ten mecz dowodzi, że .*nie jest absolutnie chciwy. Przystanek autobusowy dla .*występuje, gdy pozostała tylko jedna pasująca reszta wyrażeń regularnych następuje .*.

Zauważ, że to rozwiązanie zawiedzie, jeśli ACpojawi się przed pierwszym, ABponieważ niewłaściwe ACzostanie zastąpione przez -foobar-. Na przykład po pierwszej sedzamianie ACssABteAstACABnnACssstaje się -foobar-ssABteAstACABnnACss; dlatego nie można znaleźć dopasowania przeciwko AB.*-foobar-. Jednak jeśli sekwencją jest zawsze ... AB ... AC ... AB ... AC ..., to rozwiązanie się powiedzie.

JD Graham
źródło
0

Jedną z możliwości jest zmiana łańcucha, aby uzyskać pożądane dopasowanie

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

Użyj, revaby odwrócić ciąg, odwróć kryteria dopasowania, użyj sedw zwykły sposób, a następnie odwróć wynik ....

ssAB-+-+-+-+ACABnnACss
bu5hman
źródło