Нежадное совпадение с регулярным выражением SED (эмулирует perl. *?)

22

Я хочу использовать sedдля замены чего-либо в строке между первым ABи первым появлением AC(включительно) на XXX.

К примеру , у меня есть эта строка (эта строка только для теста):

ssABteAstACABnnACss

и я хотел бы вывод , подобный этому: ssXXXABnnACss.


Я сделал это с perl:

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

но я хочу реализовать это с sed. Следующее (с использованием Perl-совместимого регулярного выражения) не работает:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
بارپابابا
источник
2
Это не имеет смысла. У вас есть рабочее решение на Perl, но вы хотите использовать Sed, почему?
Кусалананда

Ответы:

16

Регулярные выражения Sed соответствуют самому длинному совпадению. Сед не имеет эквивалента не жадных.

Очевидно, что мы хотим сделать, это матч

  1. ABс
    последующим
  2. любое количество чего-либо кроме AC,
    сопровождаемого
  3. AC

К сожалению, sedне могу сделать # 2 - по крайней мере, не для многосимвольного регулярного выражения. Конечно, для односимвольного регулярного выражения, такого как @(или даже [123]), мы можем сделать [^@]*или [^123]*. И поэтому мы можем обойти ограничения СЕПГА путем изменений всех вхождений ACв , @а затем в поисках

  1. ABс
    последующим
  2. любое количество чего-либо кроме @,
    сопровождаемого
  3. @

как это:

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

Последняя часть изменяет непревзойденные экземпляры @обратно на AC.

Но, конечно, это безрассудный подход, потому что входные данные уже могут содержать @символы, поэтому, сопоставляя их, мы можем получить ложные срабатывания. Однако, поскольку ни одна переменная оболочки никогда не будет содержать символ NUL ( \x00), NUL, вероятно, является хорошим символом для использования в вышеуказанном обходном пути вместо @:

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

Использование NUL требует GNU sed. (Чтобы убедиться, что функции GNU включены, пользователь не должен устанавливать переменную оболочки POSIXLY_CORRECT.)

Если вы используете sed с -zфлагом GNU для обработки входных данных, разделенных NUL, таких как выходные данные find ... -print0, то NUL не будет в пространстве шаблонов, и NUL является хорошим выбором для подстановки здесь.

Хотя NUL не может быть в переменной bash, его можно включить в printfкоманду. Если ваша входная строка может содержать какой-либо символ, включая NUL, см. Ответ Стефана Шазеласа, в котором добавлен умный метод экранирования.

John1024
источник
Я только отредактировал ваш ответ, чтобы добавить длинное объяснение; не стесняйтесь обрезать его или откатить назад.
G-Man говорит: «Восстанови Монику»
@ G-Man Это отличное объяснение! Очень красиво сделано. Спасибо.
John1024
Вы можете echoили printf`\ 000 'просто отлично в bash (или ввод может происходить из файла). Но в целом строка текста, конечно, вряд ли имеет NUL.
ilkkachu
@ilkkachu Вы правы в этом. Я должен был написать, что ни одна переменная или параметр оболочки не может содержать NUL. Ответ обновлен.
John1024
Разве это не было бы намного безопаснее, если бы вы перешли ACна AC@и обратно?
Майкл Vehrs
7

Некоторые sedреализации поддерживают это. ssedимеет режим PCRE:

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

AT & T ast sed имеет конъюнкцию и отрицание при использовании расширенных регулярных выражений :

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

В частности, вы можете использовать эту технику: заменить конечную строку (здесь AC) одним символом, который не встречается ни в начальной, ни в конечной строке (как :здесь), чтобы вы могли это сделать s/AB[^:]*://, и в случае, если этот символ может появиться на входе используйте механизм экранирования, который не конфликтует со строками начала и конца.

Пример:

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'

В GNU sedподход заключается в использовании символа новой строки в качестве символа замены. Поскольку sedобрабатывает одну строку за раз, новая строка никогда не встречается в пространстве шаблона, поэтому можно сделать следующее:

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

Это обычно не работает с другими sedреализациями, потому что они не поддерживают [^\n]. С GNU sedвы должны убедиться, что POSIX-совместимость не включена (как с переменной окружения POSIXLY_CORRECT).

Стефан Шазелас
источник
6

Нет, регулярные выражения sed не имеют жадного соответствия.

Вы можете сопоставить весь текст вплоть до первого вхождения AC, используя «что-либо не содержащее AC», после ACкоторого следует то же самое, что и в Perl .*?AC. Дело в том, что «все, что не содержит AC» не может быть легко выражено как регулярное выражение: всегда есть регулярное выражение, которое распознает отрицание регулярного выражения, но регулярное выражение отрицания быстро усложняется. А в переносимом sed это вообще невозможно, поскольку регулярное выражение отрицания требует группировки чередования, которое присутствует в расширенных регулярных выражениях (например, в awk), но не в переносимых базовых регулярных выражениях. Некоторые версии sed, такие как GNU sed, имеют расширения для BRE, которые позволяют ему выражать все возможные регулярные выражения.

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

Из-за сложности отрицания регулярного выражения это не обобщает хорошо. Вместо этого вы можете временно преобразовать линию. В некоторых реализациях sed вы можете использовать новые строки в качестве маркера, так как они не могут появляться в строке ввода (и если вам нужно несколько маркеров, используйте новую строку, за которой следует переменный символ).

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

Однако следует помнить, что обратная косая черта не работает в наборе символов с некоторыми версиями sed. В частности, это не работает в GNU sed, который является реализацией sed для не встроенного Linux; в GNU sed вы можете использовать \nвместо:

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

В этом конкретном случае достаточно заменить первый ACсимвол новой строкой. Подход, который я представил выше, носит более общий характер.

Более мощный подход в sed - это сохранить линию в пространстве удержания, удалить все, кроме первой «интересной» части строки, заменить пространство удержания и пространство образца или добавить пространство образца в пространство удержания и повторить. Однако, если вы начнете делать такие сложные вещи, вам стоит подумать о переходе на awk. В Awk также нет не жадного соответствия, но вы можете разбить строку и сохранить части в переменные.

Жиль "ТАК - прекрати быть злым"
источник
@ ilkkachu Нет, это не так. s/\n//gудаляет все новые строки
Жиль "ТАК - перестань быть злым"
ASDF. Хорошо, мой плохой.
ilkkachu
3

sed - не жадное совпадение от Christoph Sieghart

Уловка, чтобы получить не жадное соответствие в sed, состоит в том, чтобы сопоставить все символы, кроме того, который завершает совпадение. Я знаю, нетрудно, но я потратил драгоценные минуты на это, и сценарии оболочки должны быть, в конце концов, быстрыми и легкими. Так что в случае, если это может понадобиться кому-то другому:

Жадный подход

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

Не жадное совпадение

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

gresolio
источник
3
Термин «ежу понятно» неоднозначен. В этом случае не ясно, что вы (или Кристоф Зигхарт) продумали это до конца. В частности, было бы хорошо , если бы вы показали , как решить эту проблему конкретной в вопросе (где выражение нулевой из-более-of следуют по более чем один символ ) . Вы можете обнаружить, что этот ответ не работает в этом случае.
Скотт
Кроличья нора гораздо глубже, чем мне показалось на первый взгляд. Вы правы, этот обходной путь не подходит для многосимвольного регулярного выражения.
gresolio
0

В вашем случае вы можете просто отменить закрытие char следующим образом:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
Midori
источник
2
Вопрос говорит: «Я хочу что - нибудь между первым заменить ABи первое появление ACс XXX...» и дает в ssABteAstACABnnACssкачестве примера входных данных. Этот ответ работает для этого примера , но не отвечает на вопрос в целом. Например, ssABteCstACABnnACssтакже должен выдавать вывод aaXXXABnnACss, но ваша команда пропускает эту строку без изменений.
G-Man говорит: «Восстанови Монику»
0

Решение довольно простое. .*жадный, но не совсем жадный. Рассмотрим сопоставление ssABteAstACABnnACssс регулярным выражением AB.*AC. То, ACчто следует, .*должно иметь совпадение. Проблема в том, что, поскольку .*он жадный, последующий ACбудет соответствовать последнему, AC а не первому. .*сгорает первым, в ACто время как литерал ACв регулярном выражении совпадает с последним в ssABteAstACABnn AC ss. Чтобы этого не случилось, просто замените первое ACна что-то смешное, чтобы отличить его от второго и от всего остального.

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

Жадный .*теперь остановиться у подножия -foobar-в ssABteAst-foobar-ABnnACssпотому , что нет другого , -foobar-чем это -foobar-, и регулярное выражение -foobar- должны иметь спичку. Предыдущая проблема заключалась в том, что регулярное выражение ACимело два совпадения, но из-за .*жадности ACбыло выбрано последнее совпадение для . Однако, с -foobar-, возможно только одно совпадение, и это совпадение доказывает, что .*оно не является абсолютно жадным. Остановка автобуса .*происходит, когда остается только одно совпадение для остальной части следующего регулярного выражения .*.

Обратите внимание, что это решение не будет выполнено, если ACперед первым появится значок, ABпотому что неправильный ACбудет заменен на -foobar-. Например, после первой sedзамены ACssABteAstACABnnACssстановится -foobar-ssABteAstACABnnACss; следовательно, совпадение не может быть найдено против AB.*-foobar-. Однако, если последовательность всегда ... AB ... AC ... AB ... AC ..., тогда это решение будет успешным.

Дж. Д. Грэм
источник
0

Один из вариантов - изменить строку так, чтобы вы хотели жадного совпадения.

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

Используйте, revчтобы перевернуть строку, отменить критерии соответствия, использовать sedобычным способом, а затем отменить результат ....

ssAB-+-+-+-+ACABnnACss
bu5hman
источник