Почему это регулярное выражение работает по-разному в sed, чем в Perl/Ruby?

У меня есть регулярное выражение, которое дает мне один результат в sed, но другой в Perl (и Ruby).

У меня есть строка one;two;;three, и я хочу выделить подстроки, разделенные символом ;. Поэтому в Perl я делаю следующее:

$a = "one;two;;three";
$a =~ s/([^;]*)/[\1]/g;
print $a;

(Или в Ruby: print "one;two;;three".gsub(/([^;]*)/, "[\\1]").)

Результат:

[one][];[two][];[];[three][]

(Я знаю причину ложных пустых подстрок.)

Любопытно, что когда я запускаю одно и то же регулярное выражение в sed, получается другой результат. Я запустил:

echo "one;two;;three" | sed -e 's/[^;]*/[\0]/g'

и я получаю:

[one];[two];[];[three]

В чем причина этого другого результата?

EDIT:

Кто-то ответил "потому что sed не perl". Я знаю это. Причина, по которой я задаю свой вопрос, заключается в том, что я не понимаю, насколько sed справляется с нулевыми совпадениями.

Ответ 1

Из исходного кода для sed-4.2 для функции-заменителя:

   /sed/execute.c
  /* If we're counting up to the Nth match, are we there yet?
     And even if we are there, there is another case we have to
 skip: are we matching an empty string immediately following
     another match?

     This latter case avoids that baaaac, when passed through
     s,a*,x,g, gives `xbxxcx' instead of xbxcx.  This behavior is
     unacceptable because it is not consistently applied (for
     example, `baaaa' gives `xbx', not `xbxx'). */

Это указывает на то, что поведение, которое мы наблюдаем в Ruby и Perl, сознательно избегалось в sed. Это не связано с какой-либо принципиальной разницей между языками, а результатом специальной обработки в sed

Ответ 2

Это интересный и удивительный краевой случай.

Ваш шаблон [^;]* может соответствовать пустой строке, поэтому он становится вопросом философии, а именно, сколько пустых строк находится между двумя символами: ноль, один или много?

СЕПГ

Символы sed четко следуют философии, описанной в разделе "Продвижение после нулевого размера регулярного выражения" "Маски регулярных выражений нулевой длины" .

Теперь двигатель регулярных выражений находится в сложной ситуации. Просили его пройти через всю строку, чтобы найти все совпадающие регулярные выражения. Первый матч закончился в начале строки, где началась первая попытка совпадения. Механизм регулярных выражений нуждается в способе избежать застревания в бесконечном цикле, который навсегда находит одинаковое совпадение нулевой длины в начале строки.

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

То есть нулевые пустые строки находятся между символами.

Вышеприведенный отрывок не является авторитетным стандартом, и цитирование такого документа вместо этого сделает это лучшим ответом.

Проверяя источник GNU sed, мы видим

/* Start after the match.  last_end is the real end of the matched
   substring, excluding characters that were skipped in case the RE
   matched the empty string.  */
start = offset + matched;
last_end = regs.end[0];

Perl и Ruby

Философия Perls с s///, которую Ruby, похоже, разделяет, поэтому в документации и примерах ниже Perl отображает оба параметра - есть ли ровно одна пустая строка после каждого символа.

"Regexp Quote-Like Operators" в документации perlop читает

Модификатор /g указывает глобальное сопоставление шаблонов, то есть сопоставление как можно больше в строке.

Выполнение трассировки s/([^;]*)/[\1]/g дает

  • Start. "Позиция соответствия", обозначенная символом ^, находится в начале целевой строки.

     o n e ; t w o ; ; t h r e e
    ^
    
  • Попытка сопоставить [^;]*.

     o n e ; t w o ; ; t h r e e
          ^
    

    Обратите внимание, что результат, полученный в $1, равен one.

  • Попытка сопоставить [^;]*.

     o n e ; t w o ; ; t h r e e
          ^
    

    Важный урок: Цендер regex * всегда преуспевает, потому что он означает "ноль или больше". В этом случае подстрока в $1 является пустой строкой.

Остальная часть матча продолжается, как указано выше.

Будучи проницательным читателем, вы теперь спрашиваете себя: "Я, если * всегда преуспевает, как совпадение заканчивается в конце целевой строки или, если на то пошло, как она проходит даже первый нуль -length match?"

Мы находим ответ на этот резкий вопрос в разделе "Повторяющиеся шаблоны, соответствующие подстроке нулевой длины" документации perlre.

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

@chars = split //, $string; # // is not magic in split
($whitewashed = $string) =~ s/()/ /g; # parens avoid magic s// /

Таким образом, Perl допускает такие конструкции, насильственно разбивая бесконечный цикл. Правила для этого отличаются для циклов нижнего уровня, заданных жадными кванторами *+{}, и для более высоких уровней, таких как модификатор /g или split.

& hellip;

Контуры более высокого уровня сохраняют дополнительное состояние между итерациями: было ли последнее совпадение нулевым. Чтобы разбить цикл, следующее совпадение после совпадения нулевой длины запрещается иметь длину нуля. Этот запрет взаимодействует с backtracking & hellip; и поэтому второе наилучшее совпадение выбирается, если наилучшее совпадение имеет нулевую длину.

Другие подходы Perl

С добавлением отрицательного утверждения lookbehind вы можете отфильтровать ложные пустые совпадения.

  $ perl -le '$a = "one;two;;three";
              $a =~ s/(?<![^;])([^;]*)/[\1]/g;
              print $a;'
  [one];[two];[];[three]

Примените то, что Mark Dominus назвал Randals Rule: "Используйте захват, когда вы знаете, что хотите сохранить. Используйте split, когда вы знаете, что вы хотите выбросить". Вы хотите выбросить точки с запятой, поэтому ваш код станет более прямым с

$ perl -le '$a = "one;two;;three";
            $a = join ";", map "[$_]", split /;/, $a;
            print $a;'
[one];[two];[];[three]

Ответ 3

В сценариях perl (и предположительно рубине) что-то происходит в этом выпуске, нет смысла просто обращаться с регулярным выражением как BRE или ERE.

awk (EREs) и sed (BREs) ведут себя так, как они должны для выполнения замены RE:

$ echo "one;two;;three" | sed -e 's/[^;]*/[&]/g'
[one];[two];[];[three]

$ echo "one;two;;three" | awk 'gsub(/[^;]*/,"[&]")'
[one];[two];[];[three]

Ты сказал I know the reason for the spurious empty substrings.. Упоминайте нас?