О заменах в Vim, использующих регулярные выражения

Привет, хабр! Ни для кого не секрет, что старина Vim очень хорош для решения разнообразнейшего круга проблем. Я бы хотел немного пописать об одной из составляющих, которые делают наш любимый редактор настолько мощным, насколько мощным он является — об инструментарии замен, использующем регулярные выражения. Свое повествование я планирую построить, рассказывая о том, как я решал пару специфических задач, и дополняя этот рассказ некоторыми базовыми справочными сведениями.


С одной стороны, про все это есть подробнейший хелп, доступный по адресу :help usr_27.txt — оттуда и было почерпнуто все, о чем будет идти речь. С другой стороны, когда мне понадобилось решить описываемые задачи, я потратил на это значительное время. Это дает мне право надеяться на то, что мой текст будет все же полезен. Сразу хочу оговориться, что я человек, далекий от программирования, поэтому моя терминология может показаться странной или нелепой — прошу меня за это простить.

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

<'фраза'> -> ' ' (ничто).

Поиск и замена в Vim осуществляется командой :substitute, однако куда удобнее использовать для нее аббревиатуру :s. Общий синтаксис этой команды примерно такой:

:{пределы}s/{что заменяем}/{на что заменяем}/{опции}

Элемент {пределы} должен содержать область, в которой мы бы хотели, чтобы совершалась замена. Если опустить этот элемент, то поиск и замена будет произведен только в той строке, где располагается курсор. Для замены во всем файле можно использовать символ '%'. Для поиска и замены в области, начинающейся со строки l1 и заканчивающейся строкой l2, {пределы} должны иметь вид 'l1,l2', например :14,17s/ будет осуществлять поиск и замену в строках с 14-й по 17-ю. Отдельного упоминания заслуживают строка с курсором, номер которой символически обозначается точкой, и последняя строка, номер которой обозначается знаком доллара. Таким образом, для того, чтобы осуществить поиск от текущей строки до конца файла, используют команду ':.,$s/'.

Вся эта команда осуществляет в указанных пределах поиск последовательности, удовлетворяющей критериям элемента «что заменяем», и заменяет эту последовательность на последовательность символов, построенную по правилам элемента «на что заменяем», учитывая опции, указанные после последнего слеша.

Первая команда, которую я попробовал для решения своей задачи, была следующая

:%s/<.*>//g

До первого слеша идет команда поиска и замены во всем файле. Между первым и вторым слешами идет последовательность, которую Vim будет искать. Подробнее о ней.

Сначала идет треугольная скобка, Vim будет искать буквальное совпадение с ней. Точка обозначает любой символ, а звездочка обозначает вхождение предыдущего символа произвольное число раз — начиная с нуля и до бесконечности. Таким образом, последовательность '.*' обозначает любую последовательность любых символов. Наконец, далее закрывающая треугольная скобка. Да, прошу прощения, если терминология «треугольных скобок» оскорбляет восприятие тех, кто помнит, что это знаки «меньше-больше» (:

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

Символ g, который завершает команду, обозначает поиск во всей строке. В противном случае Vim искал бы только первое совпадение в каждой из строк, входящих в {пределы}. Еще из полезных опций припоминаются опция 'n', которая осуществляет только поиск, но не заменяет (это помогает проверить, совпадают ли действительные критерии поиска с желаемыми), и 'с', которая спрашивает подтверждения перед каждым актом замены.

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

Напрашивается вывод — нужно искать между треугольными скобками любой символ, исключая закрывающую треугольную скобку. На этот случай у Vim есть соответствующая команда. Если при описании искомой последовательности заключить некоторый набор символов в квадратные скобки, то Vim будет искать что угодно из этих квадратных скобок. Например, шаблону '[a-z]' будет удовлетворять любая строчная латинская буква. Если же первый символ между квадратными скобками — шапочка '^', то Vim удовлетворится, найдя что угодно, кроме того, что внутри скобок. В нашем случае фраза

[^>]

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

:%s/<[^>]*>//g

Можно прикинуть, как решается такая задача в, скажем, блокноте, и в Vim. В блокноте я бы стал сначала массово заменять самые популярные теги на пустое место (например, первым бы запустил замену тега 'p' на пустое место), а потом стал бы искать треугольные скобки и удалять их и то, что внутри. На обработку действительно большого файла ушла бы у меня куча времени. А тут все получается одной командой — так просто.

Теперь еще об одной задаче — по долгу службы мне приходится пользоваться программой Wolfram Mathematica, которая на выходе дает много ASCII информации, которую, в свою очередь, требуется обработать для удобочитаемости. Например, отыскание абсолютной величины какого-то выражения эта программа обозначает словом 'Abs' и берет это выражение еще в квадратные скобки. Мне нравится читать математические тексты, пропущенные через Latex, и нахождение абсолютной величины совершенно естественно обозначать вертикальными палками (vertical bar). Так что мне нужно во всем файле совершить замену

Abs[ 'выражение' ] -> | 'выражение' |

Если бы надо было просто удалить все вхождения слова 'Abs', было бы совсем просто и аналогично предыдущей задаче, но в этом случае нам нужно еще и сохранить 'выражение', причем, каждый раз оно будет новым. Что же делать? На помощь приходит команда группировки. Если при описании разыскиваемой последовательности заключить какое-нибудь выражение в скобки \( \), то Vim поместит его в память под соответствующим номером (первое выражение под номером один, второе — два) и позволит в дальнейшем вызывать командой \x, где x — номер, под которым выражение было помещено в память.

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

:%s/Abs\[\([^\]]*\)\]/|\1|/g

Здесь стоит отметить, что для буквального совпадения квадратные скобки предваряются слешами, поскольку являются спецсимволами. Вообще любой спецсимвол, если должен участвовать в поиске, обозначая свое непосредственное значение, предваряется слешем: \^; \* и т.д. Сам слеш предваряется также слешем. Выглядит это так: для поиска последовательности '\cos' надо ввести '\\cos'.

Наконец, последняя задача, о которой я хотел бы написать. Та же Mathematica оперирует с множеством величин, которые обозначаются заглавной латинской буквой с числовым индексом, состоящим из одной цифры. В ASCII формате эти латинская буква и цифра просто идут подряд, например 'U1'. Для того, чтобы Latex обработал бы их как букву с индексом, индекс надо предварить символом нижнего подчеркивания '_'. Обрисовывается задача — совершить замену вида

'Заглавная латинская буква''цифра' -> 'Заглавная латинская буква'_'цифра'

Самое тривиальное решение, которое напрашивается — перебрать все комбинации, если их немного. То есть, запустить замену сначала 'U1' -> 'U_1', потом 'U2' -> 'U_2' и т.п. Понятно, что это не наш метод. Мы вспомним, что есть квадратные скобки. И для того, чтобы найти одну заглавную латинскую букву, достаточно ввести шаблон '[A-Z]'. Но и это не предел. Для такого шаблона у Vim есть специальная аббревиатура: '\u' (от 'uppercase'). Для цифр же есть '\d' (от 'digit'). Подробнее о таких конструкциях можно почитать по адресу :help pattern.txt. С использованием этих аббревиатур команда для поиска примет вид

:%s/\(\u\)\(\d\)/\1_\2/g

Тут опять встречается группировка круглыми скобками: она позволяет при поиске поместить найденную букву и цифру в память под соотвествующими номерами, и впоследствии их оттуда извлечь, вызывая командами с теми же номерами: '\1' вызовет букву, а '\2' — цифру.

Эти три несложные задачи, как мне кажется, отлично демонстрируют возможности Vim в поиске и замене. Полагаю, что если бы мне потребовалось бы решить одну из них, имея в руках текстовый редактор типа блокнота или, скажем, notepad++, время, которое я бы затратил на решение, существенно превзошло бы время, которое я бы потратил на то, чтобы обзавестись на той же машине копией Vim (:

Комментарии 22

    +3
    Рисовать [^>] было не обязательно, можно просто указать, что квантификатор нежадный.
      +1
      Те, для кого статья написана, не знают, как это сделать. Расскажете?
        0
        /<.*?>/
          +5
          /<.\{-}>/
          Регексп со знаком вопроса в vim не работает!
            0
            За решение с вопросом в выражение спасибо. Ни как не мог найти его решение.

            А можете немного расшифровать: почему у вас \ перед {. Как в RegExp / PCRE это разъяснить?
            /<.\{-}>/

              0
              снимаю свой вопрос. нашел :) Очень не обычное решение
                +3
                Скажите, почему на куче сайтов есть «я нашел решение», но нет самого решения? Может стоит всё же им поделиться?
                  +2
                  Вы правы, исправляюсь:

                  В справке по vim в файле pattern.txt.gz написано интересное поведение {пределов} в скобках:
                  */\{*
                  \{n,m}  Выбрать n до m предыдущего выражения, как можно больше
                  \{n}    Выбрать n предыдущего выражения
                  \{n,}   Выбрать не меньще, чем n предыдущего выражения, как можно больше
                  \{,m}   Выбрать 0 до m предыдущего выражения, как можно больше
                  \{}     Выбрать 0 или больше предыдущего выражения, как можно больше (как *)
                  


                  */\{-*
                  \{-n,m} Выбрать n до m предыдущего выражения, как можно меньще
                  \{-n}   Выбрать n предыдущего выражения
                  \{-n,}  Выбрать не меньще, чем n предыдущего выражения, как можно меньще
                  \{-,m}  Выбрать 0 до m предыдущего выражения, как можно меньще
                  \{-}    Выбрать 0 или больше предыдущего выражения, как можно меньще
                  


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

                  А зачем слеш пере скобкой — это зависит от того включена «магия» в VIM или нет. В этом я буду дальше разбираться.
                    0
                    Пример:

                    Example of <b>bold</b> and <i>italic</i> text
                    
                    pattern: /<.*>/ => выбирает => "<b>bold</b> and <i>italic</i>"
                    pattern: /<.{}>/ => выбирает => "<b>bold</b> and <i>italic</i>"
                    pattern: /<.{-}>/ => выбирает => "<b>bold</b>"
                    
        +4
        > …если бы мне потребовалось бы решить одну из них, имея в руках текстовый редактор типа блокнота или, скажем, notepad++…

        В notepad++ есть поддержка поиска и замены с использованием регулярных выражений. И в jEdit есть, и в Programmer's notepad — не стоит их обижать :) Регулярные выражения есть даже в стандартном диалоге поиска в OpenOffice, которые, кстати, бывают очень полезны при работе с безобразно отформатированными текстами и таблицами.

        С регулярными выражениями в Vim у меня была одна проблема: их использовать приходится всё же не так часто, а возможности Vim, не используемые постоянно, имеют свойство через некоторое время забываться. Но стикеры у монитора всегда выручают.
          +1
          Да, я погорячился с notepad++. Так что последний абзац следует воспринимать как тираду не о том, какой notepad++ слабый, а о том, какой необразованный я :)
          0
          что-то ни слова про PCRE, а они зачастую привычнее

          vim.wikia.com/wiki/Perl_compatible_regular_expressions
            0
            Да, вместо того, чтобы учить десяток edge-case'ов регулярных выражений в виме, надо делать :perldo
            0
            А подскажите, как удалить весь текст внутри тегов, а теги оставить?
              0
              Сам же и отвечу:)
              :%s/>.\{-\}</></g
              0
              Примного благодарен за развёрнутое описание, много раз нужно было заменить в коде что-то по маске, но на эксперементы небыло времени и боязнь просто запороть код побеждала, а тут всё расписанно :)
                0
                Поздравляю, вы открыли для себя регулярные выражения. Они есть практически везде, вим выделяется совсем не этим.
                P.S. Эти скобки обычно называют не треугольными, а угловыми.
                  +1
                  Спасибо, я бы сказал, Vim выделяется не только этим.
                  Угловыми скобками обычно называют несколько иные символы, об этом написано, в частности, в википедии.
                    0
                    Кстати, как пример чем действительно выделяется vim — это возможность использования выражений при замене (:h sub-replace-expression). Например такая команда
                    :%s/\(\d\+\)/\=submatch(0)+1
                    увеличит все числа в тексте на единицу.
                    А про скобки в википедии ведь то же самое написано.
                      0
                      Упс, что-то я не так написал. Должно быть так:
                      :%s/\d\+/\=submatch(0)+1/g
                  0
                  По-моему, если задача состоит только в том, чтобы заменить в файле один шаблон на другой, лучше использовать не вим, а сед с теми же регулярными выражениями. Сед работает намного быстрее вима, особенно когда файл такой большой, что не влезает в память.
                    0
                    Вот если бы вимовские регэкспы совпадали по синтаксису с перловыми — было б куда лучше. А так — приходится каждый раз вспоминать, куда надо поставить \

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое