Александър Иванов
cream в режим „expert“
23. Пак търсене и заместване — регулярни изрази
Апетитът идва с яденето. Колкото повече използвате търсене и заместване, толкова повече неща ще ви се иска да постигнете. Можем ли да намерим кой, само когато е отделна дума? Можем ли едновременно да търсим и да преброим различни форми на въпросителното местоимение: кой, коя, кое, кои? Може ли въпросително местоимение да стои в края на изречението (тоест пред точка, въпросителен или удивителен знак)? Можем ли да извадим от текста всички въпросителни местоимения с по четири думи ляв и десен контекст (тоест да си направим конкорданс)?
Отговорът на всички тези въпроси (и на много други, които тепърва ще ви хрумнат) е „да“! При това може да го направите, без да излизате от текстовия редактор cream.
Всички тези чудеса са възможни поради един почти универсален механизъм за търсене (и за заместване), който е вграден и във vim и който е известен като механизъм на регулярните изрази. Всъщност регулярните изрази представляват цял формален език и първата среща с него е стряскаща. Ето един пример за съвсем нормална и работоспособна команда, изградена върху този език:
:%s:<[^>]\+>::g
Все едно, че котката се е разходила по клавиатурата!
Но целта си струва усилията. Граматиката на регулярните изрази не е особено сложна, при това няма изключения. Тоест, за филолога усвояването й не е кой знае каква трудност, той само трябва да свикне с непривичните значения, които придобиват привични символи в този контекст.
По-лошо е това, че езикът на регулярните изрази има и „диалекти“, които се различават повече или по-малко един от друг. Но за сметка на това той се вгражда във все повече програми и програмни езици, та изучаването му става все по-полезно.
Кратка филологическа забележка: Нормално е филологът да си задава тук някои въпроси. Той се досеща, че формата регулярен сме заели от руски. Ако я бяхме заели от другаде, щеше да бъде „регуларен“. Ама какво значи?
Филологът знае още, че и англ. regular, и нем. regulär, и фр. régulier произлизат от лат. regularis от regula ’правило’, тоест регулярен трябва да значи ’правилен’. И какво правилно има в израза, дето ви го показах преди малко?
Работата е там, че още в средновековния научен жаргон regularis започва да се осмисля и като ’обобщен’ (сема, която дава възможност за такова преосмисляне, има още в класическия латински). Така че ако мислите за регулярните изрази като за ’обобщаващи/обобщени изрази’, никак няма да сбъркате.
Тук аз ще посоча само основни неща от регулярните изрази, при това на диалекта, вграден във vim. Ще наблягам най-вече на примерите и експериментите. Включвайте оцветяване на търсеното и да започваме.
Видяхме, че
/кой
намира поредицата от букви к, о и й в каквато и позиция да са в думата. Но
/\<кой\>
ще намира вече само кой като отделна дума. При това няма никакво значение дали след кой има някакъв пунктуационен знак, или дали Кой е в началото на изречението и е с главна буква. (Използвайте предварително командата :set noignorecase, ако искате да намирате кой само с малки букви.)
Поредицата символи \< фиксира началото на дума; поредицата \> фиксира края на думата. В документацията наричат това „котва“ (англ. anchor). Има още две удобни за работа „котви“ — знакът ^ фиксира началото на реда, знакът $ — края на реда. Така
/^кой\>
ще открие думата кой само ако е в началото на реда, а
/^кой$
само ако думата е сама на реда.
Да разширим още малко задачата си — нека потърсим като отделни думи кой, коя, кое и кои:
/<\ко[йяеи]\>
Пробвайте — това работи. Изводът е ясен — в квадратни скоби посочваме множество от символи, които могат да заемат дадена позиция в търсената поредица от символи. Може да се задава и интервал от символи, например изразът:
/[A-Za-z]
съдържа два интервала — за главните и за малките букви в латинската азбука. Това е много полезна команда за нас, които пишем на кирилица — по този начин можем да видим в текста случайно попаднали латински букви, омографни с българските. Ако има такива случайно попаднали латински букви, ако например о в кой е латинско, това ще ни създава проблеми и при търсенето, и при подреждането по азбучен ред на думите. За да откриете кирилските букви в един текст, командата е даже по-проста:
/[А-я]
Защо кирилските букви можем да ги задаваме с един интервал? Отговорът е в кодовата таблица — там се вижда, че между латинско главно Z и латинско малко a има други символи, докато кирилицата е плътно наредена, няма такива „дупки“ и можем да я зададем с първата главна буква и с последната малка буква от азбуката.
Това — добре, но читателят веднага ще попита, а как да намерим квадратна скоба в текста? Или тиренце (дефис)? Ето как става това — за квадратните скоби: /[ или /\], или едновременно и двете скоби /[[\]]; за дефиса: /- или /\-, или /[-].
Ще започна обясненията с последния пример — ако тирето (дефисът) е непосредствено след отварящата квадратна скоба или непосредствено преди затварящата, той ще се интерпретира като символ от множеството, а не като символ за интервал. Така
/[-А-я]
ще „хваща“ не само кирилските букви, но и тиренцето (дефиса).
В случая с двете квадратни скоби — /[[/]] — обяснението е малко по-дълго. Както с дефиса (малкото тире) можем да зададем лявата квадратна скоба в скоби: /[[] — това наистина ще намери всички леви квадратни скоби. Но със затварящата скоба нещата са малко по-сложни: трябва да я включим в множеството символи, предхождана от обратна (падаща наляво) наклонена черта — \]. И така се получи цялата команда — /[[\]].
Функцията на обратната наклонена черта (тази, която пада наляво, англ. backslash) е да „освобождава“ служебните символи от служебната им функция и да ги превръща просто в символи; или обратното — да „освобождава“ символа от символното му значение и да му придава служебно значение. В примера „освобождаваме“ затварящата скоба от значението й ’край на набора от символи в дадената позиция’ и я превръщаме в символ ]. В англоезичната терминология тази употреба на обратната наклонена черта със следващия символ наричат escape sequences, в нашата терминология се използва изразът екраниране на символи, а в руската экранирование символов.
Ако искаме да търсим обратната наклонена черта, също трябва да я екранираме: \\.
Механизмът на екранирането се използва и за въвеждане на служебни, управляващи символи:
\b — BackSpace (BS), код 8 (0x08);
\e — Escape (ESC), код 27 (0x1B);
\n — край на реда в подобните на UNIX операционни системи (new line, end of line, EOL, LF, line feed), код 10 (0x0A);
\r — връщане на каретката (carriage return, CR), код 13 (0x0D);
\t — табулатор (HT, TAB, horizontal tab), код 9 (0x09).
Така
:%s:\t: :g
ще замени всички табулатори в текста с шпация. Много полезна команда!
Филологическа забележка: Управляващите символи на английски се наричат control characters. Българските информатици калкират английския термин до безсмисленото „контролни символи“. Далече по-добре е да ги наричаме управляващи символи — те действително управляват потока информация между две устройства. Изобщо англ. control е от лошите другарчета на преводача — много по-често трябва да се превежда като управлявам/ръководя и доста по-рядко като контролирам/надзиравам.
Във vim са предварително определени и доста множества от символи:
\s — шпация и табулатор, тоест [ \t];
\S — всички символи освен шпация и табулатор, тоест [^ \t];
\d — арабски цифри (digits), тоест [0-9];
\D — всички символи без арабските цифри, тоест [^0-9];
\a — всички латински букви, тоест [A-Za-z];
\A — всички символи, които не са от латинската азбука, тоест [^A-Za-z];
\w — всички символи от латинската азбука плюс арабските цифри, тоест [A-Za-z0-9];
\W — всички символи, освен символите от латинската азбука и арабските цифри, тоест [^A-Za-z0-9].
Новото тук е символът ^ — въведен непосредствено след отварящата квадратна скоба, той има смисъл на логическо отрицание към множеството символи. Така [^ \t] означава ’всички символи, които не са шпация или табулатор’.
Очевидно в тези случаи обратната наклонена черта \ „освобождава“ следходния символ (буква) от значението му на символ (буква) и му придава „служебно“ значение.
Във vim има, разбира се, още доста дефинирани класове за английската латиница, като се спазва същият принцип — с малка буква се задава множеството от символи, а с главната буква — допълнителното множество. Препоръчват да се използват тези предварително определени класове от символи, но кирилицата, разбира се, не е включена в тях, така че тази препоръка е трудно да бъде изпълнена. Изобщо чуждите азбуки (не само кирилицата) ни лишават от някои удобства на vim, но не ограничават възможностите му — просто понякога се налага да пишем повече.
Точката . се използва със значение ’какъвто и да било символ в дадената позиция’. Ясно е, че за да потърсите самата точка, също трябва да я екранирате: /\..
Последователността \_ съдържа прозрачния символ за край на реда. Тя удобно се комбинира с други множества от символи, въведени непосредствено след нея. Например \_s обхваща символа за край на реда, шпацията и табулатора — ако търсите израз от две думи, ще е полезно да използвате тази комбинация за разделител между думите, защото между двете думи може да има шпация, ако са на един ред, но може и да няма, ако първата дума е в края на горния ред, а втората — в началото на долния. Очевидно в този случай разделител между думите е символът за край на реда.
Последователността \| представлява ’логическо или’, така
/Илия\|Иван
ще намира и ще маркира Илия и Иван.
Сега става ясно, че задаването на множества от символи в квадратни скоби, например [A-Za-z], е съкратен начин да създаваме подобни последователности: A\|B\|C\|...\|x\|y\|z.
Разбира се, символите, които отделят <команда>/<поле за търсене>/<поле за заместване>/<флагове>, също трябва да се екранират, за да се използват като обикновени символи. Така
/\/
ще намери наклонената черта (slash), зададена като екранирана последователност (\/). Ако изберем друг разделител, можем да не я екранираме, например:
:%s:/:\\:g
ще замени наклонената черта с обратна наклонена черта. Обратната наклонена черта обаче трябва да е екранирана (нали тя сигнализира „освобождаване“ или „екраниране“ на символа след нея).
Сега да направим някои експерименти. Потърсете
/[А-я]
Оцветяват се всички кирилски букви, както и очаквахме. Използвайте n, за да разберете на какво отговаря търсенето — ще се убедите, че то отделя всяка една кирилска буква като един отделен елемент.
И ето проблем — как, например, да направим търсене не на отделни букви, а на която и да е дума, изписана с кирилица? Очевидно е нужен някакъв механизъм на „множители“, които да определят колко пъти може да се повтарят елементи от зададено множество от символи. И това действително е предвидено в регулярните изрази:
* — поредица от нула или колкото е възможно повече символи, описани в предходния елемент;
\+ — поредица от един или колкото е възможно повече символи, описани в предходния елемент;
\= — поредица от нула или един елемент (има синоним \?).
Множителите могат да се задават и по още един начин — като екранирана последователност с фигурни скоби. Общата форма е такава — \{m,n}, — където m е минималният брой повторения, а n — максималният, при това колкото може повече. Липсата на m се тълкува като допустимост на нулево повторение, тоест липса на елемента; липсата на n се тълкува като „безкраен“ брой повторения. Ето варианти:
\{} — нула или повече повторения, колкото е възможно повече (това е синоним на звездичката *);
\{n} — точно n повторения;
\{m,} — най-малко m или колкото е възможно повече повторения;
\{,n} — нула до n повторения, колкото е възможно повече.
Сега вече можем да определим думите, изписани с кирилски букви:
/[А-я]\+
Смисълът на това е ’намери поредица от една или колкото е възможно повече кирилски букви’.
Използвайте клавиша n. Обърнете внимание, че множителят \+ изключва „празна“ дума. Заменете го с множителя *, който допуска и „празна“ дума, и вижте какво се получава. Експериментирайте още и с множителите \{} и \{1,}.
Продължаваме с експериментите. Въведете в редактора следния ред:
бббдддбббддд
Сега потърсете /бд* и помислете върху резултата.
Маркира се целият тестов ред, маркирани са и всички отделни „б“-та в текста. Защо?
Първото, което трябва да се отбележи, е, че множителят * (както и всички останали множители) се отнася само за предходния „атомарен“ елемент — а това е „д“-то. Ако вместо „д“ стоеше множество в квадратни скоби, то щеше да представлява такъв прост „атомарен“ елемент. Второто, което трябва да се отбележи, е, че множителят * допуска липса на предходния атомарен елемент, тоест на „д“-то, и съвсем нормално е да се маркират всички „б“-та, независимо от това има ли след тях „д“, или няма. Поставете курсора в началото на тестовия ред и с клавиша n вижте какви групи от символи е маркирал регулярният израз. Ще се убедите, че тестовият ред се разделя на
б-б-бддд-б-б-бддд
Изводът е — внимавайте с множителите, които допускат липса на предходния елемент. Резултатите често са неочаквани.
Сега да поправим това с израза /бд\+ — нали множителят \+ изисква поне едно „д“ след „б“-то.
И наистина — сега се маркират само комбинации от едно „б“ и „д“-та. Поставете курсора в края на текстовия ред и прибавете няколко „д“-та — ще се убедите, че всички те се маркират. В този смисъл множителят \+ е „лаком“ (англ. greedy), той се стреми да хване колкото може повече „д“-та.
Сега изтрийте всички „д“-та в края на тестовия ред и променете израза за търсене така: /бд{1,4}. Почнете пак да прибавяте „д“ в края на реда, за да се убедите, че и този множител е „лаком“ и маркира всичките до четири максимално разрешени „д“-та.
Това е смисълът на израза „колкото е възможно повече“, който аз непрекъснато повтарях в определенията на тези множители — всички те са „лакоми“. И сега започвам разяснения за следващата група множители, които не са „лакоми“ (англ. non-greedy; понякога ги определят и като „мързеливи“ — англ. lazy).
Всички не „лакоми“ множители се изграждат с екранирани фигурни скоби, като непосредствено след отварящата скоба се поставя знак минус (малкото тире, дефиса) — \{-m,n}. Смисълът на този израз е да се намерят от m до n повторения, колкото е възможно по-малко. И варианти:
\{-n} — точно n повторения — няма логическа разлика с множителя \{n};
\{-m,} — точно m повторения, колкото е възможно по-малко;
\{-,n} — нула до n повторения, колкото е възможно по-малко;
\{-} — нула или повече повторения, колкото е възможно по-малко.
Експериментирайте с не „лакомите“ варианти на множителите. Само не забравяйте — щом разрешите нулево повторение, няма да се маркира нищо. Ами да — по-малко от нулево повторение няма!
Поиграйте си малко на търсене с множители, за да сте сигурни, че разбирате как работят.
А в англоезичните текстове за такива изрази с множители може да срещнете и термините Kleene closures или само closures — американският математик Стивън Клини е изучавал тези изрази в дял от математическата логика, наречен теория на рекурсията.
А как да постъпим, ако искаме множителят да се отнася не до „атомарен“ елемент, а до група елементи?
Изходът е групиране. За да групирате няколко елемента и множителят да се отнася за цялата група, трябва да използвате кръгли скоби. Само че кръглите скоби трябва да се екранират, та се получава малко нечетливо… Така
/\(ха[-!]\)\+
ще открие и ще маркира израза Ха-ха-ха! като едно цяло. (Търсената последователност е ха[-!], тя е групирана \(ха[-!]\) и към групата е приложен множител \+ — последователност от едно или колкото е възможно повече повторения на групата.)
Техническа забележка: Не е удобно и дори е невъзможно да напишете такъв израз отляво надясно, както пишете текст. Разделете нещата на части. Например, напишете си първо последователността, после въведете скобите за групиране — лявата и дясната — и накрая приложете множителя. Вашата логика може да върви и по друг път — следвайте го.
Добра практика е, когато въвеждате „отварящ“ елемент (например, лявата скоба), веднага да въведете и „затварящия“ елемент (дясната скоба) — съдържанието между скобите може да въведете и след това.
Този пример е малко насилен, но групирането става много важно, когато започнете да използвате регулярните изрази не само за търсене, но и за заместване.
Например, искаме да намерим всички думи, след които има точка и запетайка. (Янакиев предлага да казваме „точка над запетайка“, за да не се създава двусмислица с комбинацията от два знака: точка и след нея запетая.) Това вече знаем как да направим:
/\<[А-я]\+;
Решаваме да заместим някои „точки над запетая“ със запетая. Проблемът е, че думата пред знака „точка над запетая“ може да е различна — значи трябва при заместването да преписваме същата дума. Ето как става това с групиране:
:%s:\(\<[А-я]\+\);:\1,:gc
В израза за заместване последователността \1 означава, че на това място ще влезе първата група от израза за търсене. Може да се използват до девет групи — от \1 до \9, които отговарят на последователността на групите в израза за търсене.
В израза за заместване е възможно да се използва и \0 — това не е точно група, това означава ’всичко, намерено по шаблона’, тоест това е синоним на & и, ако го използваме в примера, запетайката ще се поставя след „точката над запетая“.
Като повечето учебни примери и този е доста безсмислен — спокойно може да търсите само символа „точка над запетайка“, — но той илюстрира работата с групи и тъй нареченото „обратно препращане“ (англ. back-reference, backward reference) — заместващият израз взема информация „обратно“ от резултата, получен чрез израза за търсене. Впрочем „обратно препращане“ работи и в израза за търсене: /\(ха-\)\1 ще намери „ха-ха-“.
Използването на групиране е наложително в много случаи, например ако искате да размествате части от израза — в примерите показвам как може да се разместват колони в таблица чрез регулярни изрази (вж. Разместване на колони в таблица).
Изучаването на езика на регулярните изрази в cream (vim/gvim) при включено оцветяване на търсеното е лесно и нагледно. За да приложите знанията си към друга програма или към някакъв програмен език, вече няма да ви е необходимо да усвоявате нови принципи, а само „технически“ различия. Например, че при групиране кръглите скоби не се екранират или че обратното препращане към група в намереното става със суфикс $, а не с обратна наклонена черта, тоест $1, а не \1, и други такива дребни, но досадни различия.
При по-сложни изрази дори е разумно да ги „построите“ в cream и нагледно да се убедите, че работят, а след това да ги „транслитерирате“ за съответния диалект на езика на регулярните изрази. Такава транслитерация може даже да бъде програмирана като макрокоманда към cream/gvim/vim и ако някой от вас направи такава макрокоманда, нека ми я изпрати — ще я включа в glotta.
Допълнение. Разбира се, програми за тестване на регулярни изрази има, има и тестващи програми, достъпни по интернет. След като бях написал предишния абзац, Красимир Стефанов (lokster) публикува програмата RegEx Tester, която дава възможност на използващите пайтън (Python) да експериментират с регулярните изрази в този език по начин, много близък до описания от мене в cream. Диалектът на регулярните изрази в пайтън малко се различава от диалекта на регулярните изрази във vim/gvim/cream, та по тази причина програмата е много полезна. Впрочем, Красимир Стефанов е един от създателите и поддръжниците на прекрасната българска сглобка (дистрибуция) на линукс Учи свободен с Ubuntu.
Накрая да обобщя:
— в регулярните изрази всеки зададен символ е „атомарен“ и заема определена позиция в поредицата от символи;
— служебните символи трябва да се екранират с \, за да се разглеждат като обикновен символ; с екраниране се въвеждат и управляващите символи и някои предварително определени множества от символи;
— на мястото на всеки атомарен символ може да бъде въведено множество от символи;
— повторението на атомарния символ се определя от множители — те са два вида: „лакоми“ и не „лакоми“ („мързеливи“);
— предвидени са „котви“, които фиксират позиция в текста, към която ще се търси дадената поредица;
— части от израза за търсене могат да бъдат групирани чрез екранирани кръгли скоби; към групата могат да се прилагат множители или да се използва съдържанието й в изразите за търсене и за заместване.