Режимы однострочный (Single-line) и многострочный (Multiline)
Да, в индизайне эти сходные по написанию слова пишутся по-английски именно так: одно слово с дефисом, второе слитно.
При знакомстве с этой опцией может возникнуть непонимание. Первое предположение наличия двух режимов — однострочного и многострочного — используется или первый, или второй. Но на самом деле эти режимы не являются взаимоисключающими: они используются одновременно, и каждый из них или включён, или выключен, у них разные области действия.
Состояние режима «однострочный» влияет на обработку метасимвола . («точка» — совпадение с любым символом). Режим «многострочный» определяет обработку маркеров положения: ^ (начало абзаца) и $ (конец абзаца).
Когда однострочный режим выключён, поиск с использованием метасимвола . (точка) выполняется в пространстве абзаца, в котором стоит курсор. Аналогия — каждый абзац в статье является отдельной строкой. Когда однострочный режим активен, поиск с использованием метасимвола . (точка) выполняется в пространстве всей статьи. Статья превращается в одну строку.
Однострочный режим
(?-s) однострочный режим выключен, это стандартное состояние обработки греп-запросов: оператор .* выбирает все символы, начиная от текущей позиции до конца абзаца, но сам конец абзаца в выборку не попадает.
(?s) одиночный режим включён, в выборку попадает и знак перевода строки. Т. е. будет выбран весь текст от точки курсора до конца статьи. А если курсор не в тексте, а выбран фрейм, то будет выбрана вся статья.
Важно: если в тексте запроса нет метасимвола точка, то не имеет значения, какой вариант однострочного режима сейчас установлен.
Многострочный режим
(?m) многострочный режим включён, это стандартное состояние обработки греп-запросов, знаки ^ и $ определяют начало и конец абзаца.
(?-m) многострочный режим выключен, знак ^ интерпретируется как \A — маркер начала статьи, а знак $ как \Z — маркер конца статьи.
Важно: если в тексте запроса нет метасимволов ^ и $, то не имеет значения, какой вариант многострочного режима сейчас установлен.
Совместное использование режимов
Если в тексте запроса есть точка и хотя бы один из двух знаков ^ или $, то можно определить, как эти режимы будут взаимодействовать.
Очевидно, что возможны четыре их сочетания
Допустим, есть такие запросы поиска .{10}$ и ^.{12} и вот такой текст:
123456789ABCDEF
123456789ABCDEF
12345678
123456789ABCDEF
123456789ABCDEF
Поиск всегда начинается с текущей позиции курсора.
(?s)(?-m).{10}$ Будут выбраны последние десять знаков статьи. Поскольку точка работает как любой знак, то если последним символом статьи будет перевод строки, то он тоже попадёт в выборку.
(?-s)(?-m).{10}$ Если последний знак статьи не является переводом сроки, то будут выбраны последние десять знаков статьи. И т. к. точка работает как печатный знак, то если последним символом статьи будет перевод строки, то ничего выбрано не будет, поскольку среди последних десяти символов есть знак перевода строки, а он не входит в множество знаков, охватываемых метасимволом точка.
(?m)(?-s).{10}$ Будут выбираться последние десять знаков каждой строки. Перевод строки в выборку не входит.
(?sm).{10}$ Если поставить курсор в начало тестового текста, то будут выбираться последние десять знаков каждой строки. Но если, например, поставить курсор после буквы A во второй строке, то запрос выделит следующие десять знаков: буква F, перевод строки и восемь цифр третьей строки. Почему так происходит? Запрос ищет конец абзаца; находит его в конце строки, где он стоит; оценивает число знаков перед этим концом абзаца не только на предмет, есть ли нужное число, но и где находится текущая точка вставки. В нашем случае точка вставки попадает в эти десять знаков, значит, этот перевод строки не подходит. Он ищет следующий, так же проверяет десять символов перед ним, не попадает ли в них текущая позиция курсора. Если нет, то они выделяются.
В этом примере мы поставили курсор после буквы A. Такой же результат будет до буквы F. Но как только курсор после F, он попадает в пространство десяти знаков перед переводом строки, и запрос ищет в тексте следующий перевод строки.
(?s)(?-m)^.{12} Поиск в статье, ищутся все знаки в тексте. Знак ^ интерпретируется не только как начало абзаца , но и как \r, поэтому этот запрос последовательно выделяет сперва 12 знаков с начала статьи, а потом все последовательности из двенадцати знаков, начинающихся с \r.
(?-s)(?-m)^.{12} Поиск в статье, любой печатный знак. Поскольку «любой печатный знак», т. е. перевод строки в выборку не попадёт, то этот запрос выберет только первые двенадцать символов первой строки, если длина строки больше 12.
(?m)(?-s)^.{12} Поиск в строке, любой печатный знак.
Ищутся двенадцать текстовых символов в начале каждого абзаца. Если абзац короче, он пропускается.
(?sm)^.{12} Поиск в строке, любой знак.
Выбираются двенадцать символов в начале каждого абзаца. Если абзац короче, то в выборку попадёт перевод строки и несколько символов следующего абзаца.
Конечно, приведённые примеры вряд ли встретятся в реальной вёрстке. Но они полезны для того, чтобы понять особенности использования этих режимов.
Для понимания
Если есть два абзаца
Фраза
Слово
то любой из запросов .+ или (?-s).+ найдёт сперва абзац Фраза, а потом абзац Слово. В выборке будут только печатные знаки.
А если у нас такой запрос (?s).+, то он будет видеть этот текст так, в виде одной строки:
Фраза\rСлово
и этот запрос выберет все знаки в этой строке.
Строки ^фраза и (?m)^фраза обозначают поиск текста фраза в начале любой строки.
Строки слово$ и (?m)слово$ обозначают поиск текста слово в конце любой строки.
Строка (?-m)^фраза обозначает поиск текста фраза в начале статьи. Строка (?-m)слово$ обозначает поиск текста слово в конце статьи. Очевидно, что эти запросы можно переписать иначе, сделав их более понятными: \Aфраза и слово\Z.
Требуется уточнение
В начале было сказано, и это взято из документации, что состояние многострочный режим включено по умолчанию. Вся ли это информация об этом режиме?
Вот задача: найти каждый первый пробел в абзацах и запрос для её решения: ^(.+?)\K\h
Запрос эту задачу не решает, т. к. идёт через строку, и почему он так ведёт себя, я точно не знаю. Возможно, это особенности реализации нежадного поиска. Но если его изменить: (?m)^(.+?)\K\h, то он будет идти по каждой строке.
В чём же дело? Ведь если многострочный режим включён по умолчанию, то запросы ^(.+?)\K\h и (?m)^(.+?)\K\h должны выполняться одинаково. Однако этого не происходит. Моё предположение, что да, это так, но дополнительно при появлении команды (?m) изменяется трактовка знака ^, он становится на время работы запроса не только маркером начала абзаца, но и соответствует переводу строки \r.
Вот как это выглядит в реализации с другими запросами: запрос \r(.+?)\K\h идёт по всем строкам, начиная с второй, а запрос (^|\r)(.+?)\K\h работает так же, как (?m)^(.+?)\K\h. Точно так же будут работать (?s)^(.+?)\K\h и (?-s)^(.+?)\K\h, именно в силу того, что на время действия запроса операторы включения любого из этих режимов определят, что в запросе метасимвол ^ надо трактовать как \r.
Примеры
• Заменить шпации на табуляции.
В тексте позиции списка разделены концевыми шпациями.
A) text, numbers =КШ= D) text, numbers
B) text, numbers =КШ= E) text, numbers
C) text, numbers =КШ= F) text, numbers
Тут =КШ= — текстовое обозначение концевой шпации.
Надо заменить эти шпации на табуляции.
Запрос ^\u\).+\K~f будет искать через строку.
А явное указание, что многострочный режим включён (хотя он уже был включён по умолчанию), определит на время действия запроса, что символ начала статьи/начала абзаца ^ надо применять и к переводу строки. В результате запрос (?m)^\u\).+\K~f пройдёт по каждой строке.
• Выделить весь текст статьи, независимо от того, где стоит курсор.
Запрос .* выделит текст от текущей позиции до конца абзаца. Поскольку в запросе есть точка, то для расширения области действия запроса надо использовать однострочный режим. От текущей позиции до конца абзаца статьи текст выделит такой запрос (?s).*, а задачу выделения всего текста статьи решит запрос (?s)\A.+ В обоих случаях именно включение однострочного режима позволило включить в выборку знаки перевода строки.
• Выделить текст от позиции курсора до конца статьи.
Задачу выделения текста от позиции курсора до конца статьи решает любой из таких запросов:
(.|[\r\n])+ или (?s).*
Первый вариант для стандартного случая, когда однострочный режим выключен. Второй включает этот режим, и вся статья для поиска .* становится одной строкой.
Заключение
К сожалению, к тому диалекту GREP, что есть в индизайне, на русском языке нет толковой информации от разработчиков. Данная статья — результат моего исследования этих режимов. Когда с ними разобрался, многие задачи поиска стали решаться намного проще.
Попробуйте и вы сделать их своим инструментом, если пока не получалось использовать эти режимы.
Будут вопросы, пишите.
Михаил Иванюшин, независимый автор.