Загадка preg_replace: замена нуля или более символа в конце объекта

Скажем, $ d – путь к каталогу, и я хочу, чтобы он начинался и заканчивался ровно одной косой чертой (/). Первоначально он может иметь ноль, одну или несколько ведущих и / или конечных косых черт.

Я пытался:

preg_replace('%^/*|/*$', '/', $d); 

который работает для ведущей косой черты, но, к моему удивлению, дает две конечные слэши, если $ d имеет хотя бы одну конечную косую черту. Если субъект, например, 'foo///' то preg_replace () сначала соответствует и заменяет три конечных косая черта одной косой чертой, а затем соответствует нулевым сокращениям в конце и заменяет их косой чертой. (Вы можете проверить это, заменив второй аргумент на '[$0]' .) Я нахожу это довольно противоречивым.

Хотя есть много других способов решить основную проблему (и я ее реализовал), это стало загадкой PCRE для меня: какой (скалярный) шаблон в одном preg_replace выполняет эту работу?

ДОПОЛНИТЕЛЬНЫЙ ВОПРОС (изменить)

Может ли кто-нибудь объяснить, почему этот шаблон соответствует тому, как он работает в конце строки, но не ведет себя аналогично в начале?

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

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

Итак, почему ваше регулярное выражение не совпадало дважды в начале, как это происходит в конце? Из-за стартового якоря ( ^ ). Если объект начинается с одной или нескольких косой черты, он их потребляет, а затем пытается совместить нулевые слэши, но он терпит неудачу, потому что он больше не находится в начале строки. И если в начале нет косых черт, ручная подача имеет тот же эффект.

В конце темы это совсем другая история. Если там нет косых черт, оно ничего не соответствует, пытается наброситься вперед и провалиться; конец истории. Но если он соответствует одному или нескольким косым чертам, он их потребляет и пытается снова сопоставляться – и преуспевает, потому что привязка $ прежнему совпадает.

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

 preg_replace('%^/*|(?<!/)/*$%', '/', $d); 

… или убедитесь, что часть регулярного выражения должна потреблять по крайней мере один символ:

 preg_replace('%^/*|([^/])/*$%', '$1/', $d); 

Но в этом случае у вас есть гораздо более простой вариант, как продемонстрировал Джон Кугельман: просто захватите часть, которую вы хотите сохранить, и отбросьте все остальное.

 $path = '/' . trim($path, '/') . '/'; 

Сначала удаляются все слэши в начале или в конце, а затем снова добавляются одиночные.

 preg_replace('%^/*(.*?)/*$%', '/\1/', $d) 

это может быть сделано в одном preg_replace

 preg_replace('/^\/{2,}|\/{2,}$|^([^\/])|([^\/])$/', '\2/\1', $d); 

Небольшое изменение в вашем шаблоне будет заключаться в том, чтобы выделить две основные проблемы в конце строки:

  1. Заменить несколько слэшей одной косой чертой
  2. Заменить косые черты одной косой чертой

Шаблон для этого (и существующей части для совпадения в начале строки) будет выглядеть так:

 #^/*|/+$|$(?<!/)# 

Немного менее краткий, но более точный вариант должен быть очень явным только для соответствия нуля или двух или более слэшей; что бы заменить одну косую черту одной косой чертой?

 #^(?!/)|^/{2,}|/{2,}$|$(?<!/)# 

Кроме того: предложение nikic использовать trim (для удаления ведущих / конечных косых черт, а затем добавить свои собственные) является хорошим.