Golang regexp: как заматчить перенос строки

31.03.2020 регулярные выражения исходники


Golang regexp: как заматчить перенос строки

Для форматирования вставок кода на этом сайте используется логика на основе регулярных выражений. Логика проста — вынимаем содержимое определенного тега, процессим его с помощью библиотеки подсветки синтаксиса и заменяем исходный тег на новый.

Много времени ушло именно на отладку данного регулярного выражения, и даже дебаггер не помог, т.к. проблема была с самим регулярным выражением.
Например, как вы думаете, заматчит ли регулярное выражение (ниже) текст (еще ниже)?

Регулярка:

<tag>(.*)</tag>

Текст:

<tag>1
2
3</tag>

Если вы пришли из PHP (как и я), то скажете, что да.

Однако, простой пример с go playground опровергает данное предположение:

match, _ := regexp.MatchString("<tag>(.*)</tag>", "<tag>1\n2\n3</tag>")
fmt.Println(match)

// false

Исследование исходников пакета regexp навело на список флагов:

const (
	FoldCase      Flags = 1 << iota // case-insensitive match
	Literal                         // treat pattern as literal string
	ClassNL                         // allow character classes like [^a-z] and [[:space:]] to match newline
	DotNL                           // allow . to match newline
	OneLine                         // treat ^ and $ as only matching at beginning and end of text
	NonGreedy                       // make repetition operators default to non-greedy
	PerlX                           // allow Perl extensions
	UnicodeGroups                   // allow \p{Han}, \P{Han} for Unicode group and negation
	WasDollar                       // regexp OpEndText was $, not \z
	Simple                          // regexp contains no counted repetition

	MatchNL = ClassNL | DotNL

	Perl        = ClassNL | OneLine | PerlX | UnicodeGroups // as close to Perl as possible
	POSIX Flags = 0                                         // POSIX syntax
)

Согласно исходникам, GO работает с регуляркой следующим образом:

  • Компилирует функцией syntax.Parse
  • Метод syntax.Parse затем использует Flags, чтобы "распланировать" выполнение регулярного выражения (определить, какие конкретно операции будут выполнены в зависимости от символов в регулярке)
  • regexp.Regexp (тот тип, с которым пользователь в GO работает с регулярным выражением) создается на основе результатов выполнения syntax.Parse

Итак, нам нужно найти как скомпилировать регулярное выражение с флагом DotNL.

Поискав все вызовы функции compile из пакета Regex, прихожу к выводу, что на данный момент я могу скомпилировать регулярное выражение только с флагами Posix или Perl, те варианта с флагом DotNL или включающим его не существует.

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

<tag>([[:graph:]\\s]*?)</tag>

В GO есть классы символов, которые включают в себя много отдельных символов, это задокументировано здесь. Я использовал 2 из них, чтобы покрыть действительно все символы в группе [ ].

Другие статьи