headermask image



Регулярные выражения, их назначение и применение в Unix.

Текст

Одним из следствий У (точнее, правила human readable) в UNIX стало то, что все без исключения настройки системы и дополнительных служб, а также подавляющее большинство системных журналов и прочих хранилищ системной информации имеют текстовый формат. Это настолько бросается в глаза, что, наряду с интерфейсом командной строки, входит в понятие unix style. Даже если некоторые данные не содержатся в текстовом виде явно, всегда есть утилиты, выдающие их пользователю на терминал как текст.

Формально текстовый файл – это файл, разбитый на строки и слова, содержащий только печатные символы и символы конца строки. Таковы, как сказано в лекции 8, требования терминальной линии к передаваемой по ней информации: во-первых, некоторые символы (ascii-код которых меньше 32) считаются управляющими, во-вторых, поток байтов делится на строки при помощи символа конца строки (специальное обозначение – “\n“, ascii-код 12), в обработанном режиме именно строками система обменивается с пользователем, в-третьих, строка делится на слова с помощью символов-разделителей – пробелов и табуляций. По таким же правилам shell обрабатывает командную строку, однако разбиение на слова слегка усложняется из-за закавычивания. В языках программирования могут быть еще более сложные правила разбиения строк на слова: например, могут различаться идентификаторы, ключевые слова и знаки операций; разделитель между словами разного типа может быть необязательным и т. д.

Вообще, каждый тип текстового файла имеет свои представления о лексике (правилах построения слов), синтаксисе (правилах сочетания слов), семантике (правилах интерпретации текста). Представления эти, будучи формализованными, называются языком и задаются прикладной областью, а реализуются программой, которая будет разбирать содержимое файла. Неформальное требование к текстовому файлу – чтобы он был понятен, или, согласно И, потенциально понятен человеку. Для этого все слова, встречающиеся в файле, должны обозначать объекты, действия или отношения в прикладной области, а лексика, синтаксис и семантика образующегося языка должны быть понятны человеку. Собрав воедино все тексты, задающие решение пользовательской задачи и все нетекстовые исходные данные, мы получим главный элемент проективной системы – проект. Так, проект программного продукта – это исходные тексты программы на языке программирования и, возможно, данные, не влияющие на работу программы, – картинки, звуки и т. п.

В отличие от текста на естественном языке (например, на русском), тексты, составляющие различные проекты в UNIX, строго структурированы. Языки этих текстов имеют четкий синтаксис и довольно простую лексическую организацию. Если перед человеком стоит задача исправления проекта, для решения которой достаточно видоизменить имеющийся текст, а дописывать новые части проекта не надо, ее можно решать двумя способами: либо выбрасывать устаревшие куски и вписывать новые (что равносильно дописыванию), либо поручить эту работу роботу. Робот должен – на уровне, достаточном для данной работы, – разбираться в синтаксисе и лексике языка и иметь четкие инструкции, какие куски считать устаревшими и как их преобразовывать в новые.

Обработка текста

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

С задачей поиска помогает справиться мощный механизм представления структуры текста, называемый регулярным выражением (regular expression). Регулярное выражение – это строка, содержащая специальные символы, интерпретация которых такова, что одному регулярному выражению может соответствовать целый класс строк определенной структуры. С точки зрения области применения регулярное выражение напоминает шаблон, используемый в shell, однако возможностей у него намного больше. Термин “регулярное выражение” восходит к теории формальных языков и классификации формальных грамматик по Хомскому.

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

Про регулярные выражения написано несколько приличных руководств (re_format(7) или regex(7), regexp(3), grep(1)) и книг, так что все сказанное нами далее – попытка прояснить идею и очертить возможности регулярного выражения. В поисках более глубоких знаний рекомендуем обращаться к этим руководствам и в особенности к книге.

Базовое регулярное выражение

Регулярное выражение (далее РВ) служит для задания общего вида строки – части текста, завершающейся символом конца строки и не содержащей этого символа. РВ – это строка, состоящая из обычных символов, имеющих однозначное соответствие в тексте, и специальных символов, которые интерпретируются особым образом. Говорят, что фрагмент строки (или подстрока) соответствует РВ, если эта подстрока является одним из возможных частных случаев интерпретации РВ. Чтобы отличать регулярные выражения от строк, одни мы будем обозначать так: ‘регулярное выражение’, а другие – так: строка.

Любой обычный символ – это атомарное РВ. Такому выражению соответствует подстрока из одного этого символа. Например, f соответствует ‘f’.

Спецсимвол .атомарное РВ, которому соответствует подстрока из одного любого символа. Например, M или @ соответствуют ‘.’, а или abc – не соответствуют (хотя abc содержит три фрагмента – a, b и c, соответствующих ‘.’).

Конструкция из специальных символов [ и ], между которыми находится один или несколько других символов, – это атомарное РВ-множество. Ему соответствует подстрока из одного символа, принадлежащего множеству. Например, и x и u соответствуют ‘[unix]‘, а U и 8 – не соответствуют. Специальным символом для множества является знак “-“, который задает диапазон. При использовании диапазона в множество включаются все символы, код которых не меньше, чем у символа, стоящего слева от “-“, и не больше, чем у стоящего справа. Например, любая буква латинского алфавита соответствует выражению ‘[a-zA-Z]‘. Если нужно включить в множество сам символ “-“, он должен идти сразу за [ или перед ]. Если при определении множества вместо [ использовать [^ , такому атомарному РВ будет соответствовать подстрока из одного символа, не принадлежащего множеству. Например, A и ; соответствуют выражению '[^aeiu-z]‘, а e и v – не соответствуют.

Последовательность атомарных РВсоставное РВ. Ему соответствует подстрока, состоящая из фрагментов, каждый из которых соответствует очередному атомарному РВ в последовательности. Например, любое двузначное натуральное число соответствует выражению ‘[1-9][0-9]‘: в числе, допустим, 36 3 соответствует ‘[1-9]‘, а 6‘[0-9]‘. Последовательность составных РВ – тоже составное РВ. Составное РВ, заключенное в спецсимволы – круглые скобки, приобретает свойства атомарного; это называется операцией группирования.

Атомарное РВ, после которого стоит повторитель *, – тоже атомарное РВ. Ему соответствует подстрока, составленная из любого числа (включая 0) фрагментов, каждый из которых соответствует повторяемому РВ. Таким образом, пустая подстрока также соответствует РВ с повторителем. Например, выражению ‘a*’ соответствуют подстроки a, aa, aaa и т. д. Выражению ‘[0-9]*’ соответствует любая (включая пустую) последовательность десятичных цифр, например 1415926, потому что ее можно разбить на односимвольные фрагменты, каждый из котороых будет соответствовать выражению ‘[0-9]‘. Наконец, выражению ‘(Foo)*’ будут соответствовать подстроки Foo, FooFoo, FooFooFoo и т. д., но не будут соответствовать подстроки Fo, FFFoooooo, FFoo и Fwoo, так как их невозможно целиком разбить на фрагменты, соответствующие атомарному РВ ‘(Foo)’. Любой подстроке любой длины соответствует выражение ‘.*’.

Атомарное РВ, после которого стоит повторитель-интервал вида {минимум,максимум}, также атомарное РВ. Ему соответствует подстрока, состоящая из фрагментов, каждый из которых соответствует этому атомарному РВ, при этом количество фрагментов должно быть не меньше минимума и не больше максимума. Например, выражению ‘(he){2,4}’ соответствуют подстроки hehe, hehehe и hehehehe. Если в интервале опущен минимум, он считается равным нулю, а если опущен максимум, он считается равным бесконечности. Например, выражению ‘Z{2,}’ соответствуют подстроки ZZ, ZZZ, ZZZZ и т. д., а выражению ‘w{,3}’ – подстроки w, ww и www.

Спецсимволы ^ и $ определяют позицию подстроки в строке. Если в начало РВ вставить ^, ему будут соответствовать те же подстроки, только стоящие в начале строки. Если в конец РВ добавить $, ему будут соответствовать те же подстроки, только стоящие в конце строки. Например, выражение ‘^U’ будет встречаться во всех строках, начинающихся с U, а ‘!$’ – во всех строках, заканчивающихся на !. Таким образом, выражению ‘^[a-z]{1,}$’ будут соответствовать любые строки, состоящие из одних только маленьких латинских букв.

Спецсимвол \ лишает следующий за ним любой спецсимвол специального значения (известная уже нам операция закавычивания). Например, выражению ‘\.\.\.’ соответствует многоточие, а выражению ‘\$\*’ – подстрока $*.

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

Расширенное регулярное выражение

 

Операция выбора – |. Два РВ, разделенные ‘|’составное РВ, для которого определяется подстрока, соответствующая любому из двух РВ. Например, one, TWO и = соответствуют ‘=|one|TWO’, а =one, FooBar и Two – не соответствуют. Операция выбора – очень удобное средство, но стоит иметь в виду, что в сложных случаях, в сочетании с повторителем *, она может потребовать немало ресурсов – времени или оперативной памяти, как повезет.

Два частных случая повторителя-интервала: повторитель ?, обозначающий использование предшествующего атомарного РВ не более одного раза (может быть выражен с помощью ‘{,1}’), и повторитель +, обозначающий использование предшествующего атомарного РВ не менее одного раза (‘{1,}’). Оба частных случая постоянно встречаются в повседневной работе; кроме того, обработка + во многих случаях идет существенно быстрее, чем обработка *, поэтому + стоит использовать вместо * везде, где только возможно.

В расширенном РВ определяются именованные множества символов, вроде ‘[[:alnum:]]’ (ему соответствует одна буква или одна цифра), или ‘[[:blank:]]’ (ему соответствует один разделитель – пробел или табуляция). Все именованные множества описаны в руководстве. Главное достоинстсво именованных множеств – привязка к национальному языку (если она реализована в утилите, использующей РВ): буквой считается не только латинская, но и – в нашем случае – русская буква. Если то же РВ использовать в немецких настройках, задействованные в этом языке латинские буквы с диактрическими знаками или двойное s тоже будут соответствовать ‘[[:alpha:]]’ или ‘[[:alnum:]]’. По этой же причине рекомендуется не использовать именованные множества, если установленный язык заранее неизвестен.

Регулярное выражение Соответствует Не соответствует
‘a.c’ abc, aWc, a+c ac, a..c, cba
‘F.*d’ FddFd, Freud, F.*d feed, Sigmund, dF
‘u..[p-t]‘ user, u##s, uppp undo, uncut, u=t
‘wuff(-wuff)*’ wuff, wuff-wuff-wuff wuffwuff, wuff-
‘[+-]?[0-9]+’ +1, -0932, 333 +-7, -, 0+0
‘[fit|plug](in|out)+’ fitin, pluginoutin infit, plug, outin

К $ и ^ добавлено еще два позиционных оператора, ‘[[:<:]]’ и ‘[[:>:]]’, которым соответствует начало и конец слова (здесь уместно процитировать параграф BUGS руководства re_format: “The syntax for word boundaries is incredibly ugly”). Строго говоря, эти позиционные операторы слегка расширяют класс распознаваемых подстрок, потому что результат поиска ‘[[:>:]]’ зависит от символа, который не входит в подстроку-результат, т. е. зависит от контекста.

Новый синтаксис РВ получил название расширенного (extended) регулярного выражения.

Толкование неоднозначностей

Подстрока, соответствующая РВ, выбирается, как уже говорилось, из строки. Это значит, что контекст, внутри которого происходит поиск соответствия, ограничен началом и концом строки. Здесь механизм разбора РВ опирается, с одной стороны, на формальное свойство текстового файла состоять из строк, а с другой стороны, на то, что в языках, используемых в UNIX, конец строки обычно имеет значение строгого разделителя, поэтому задача поиска выражения, состоящего из нескольких строк, встречается нечасто.

Избавиться от неоднозначности при поиске подстроки в строке помогает правило левый-длинный (leftmost longest, дословный перевод: самая левая – самая длинная). Если в строке встречается несколько подстрок, которые соответствуют РВ, из них будет выбрана в качестве ответа та, что раньше всех начинается, то есть самая левая. Если при этом с одного и того же места в строке начинается несколько подстрок, соответствующих РВ, то среди них будет выбрана самая длинная. Например, из всех возможных соответствий выражению ‘a.*d’ в строке The syntax for word boundaries is incredibly ugly будет соответствовать подстрока от самого левого a до самого правого d: ax for word boundaries is incred.

Выражению ‘B*’ в строке BBcBBBd можно найти 17 соответствий (из них одна подстрока BBB, 3 BB, 5 B и 8 пустых подстрок в различных позициях). Правильным ответом будет BB в начале строки, потому что, во-первых, это самая левая из возможных подстрок, а во-вторых, самая длинная (среди допустимых, B и BB). А вот в строке aBBcBBBd первой будет найдена пустая подстрока, потому что ‘B*’ имеет cоответствие в самом начале строки, и притом ровно одно: пустую подстроку.

Если некоторая подстрока может быть поставлена в соответствие составному РВ несколькими способами, правило левый-длинный применяется так: среди всех возможных ответов выбираются ответы с наибольшей длиной самого левого фрагмента (соответствующего самому левому атомарному РВ). Среди оставшихся выбираются ответы с наибольшей длиной второго фрагмента и т. д. до конца регулярного выражения. Например, при поиске ‘[0-9]+[0-9]+’ в строке a123c5678d будет найдена подстрока 123, причем 12 будет соответствовать первому ‘[0-9]+’, а 3 – второму.

Почему так? Во-первых, 5678 отпадает, потому что есть соответствие, которое лежит ближе к началу строки, – 123. Во-вторых, будет найдено именно 123, а не 1 или 12, потому что 123 – самое длинное из возможных соответствий. В-третьих, начальное ‘[0-9]+’ и радо бы захватить для себя всю 123, но тогда на долю второго не остается ничего, и соответствия всему РВ не получается. Так что оно вынуждено уступить второму ‘[0-9]+’ хотя бы один символ – 3.

Подобное поведение регулярных выражений привело к тому, что во вполне серьезной литературе их называют жадными (greedy). “Жадность” регулярных выражений – удобное свойство, на него всегда можно положиться (в смысле однозначности ответа), однако встречаются задачи, решать которые было бы удобнее, если бы выбиралось самое короткое, а не самое длинное соответствие. Многие инструментарии, использующие регулярные выражения, включают в себя диалекты “нежадных” (non-greedy) РВ, например, вводят “нежадные” повторители. Важно знать, что с помощью “нежадных” РВ можно задать язык, не принадлежащий классу регулярных; это, с одной стороны, заметно расширяет наши возможности (непонятно только, часто ли они бывают востребованы), но, с другой стороны, отменяет все предположения о времени поиска и однозначности результата, сделанные для классического варианта РВ.

Поиск

Поиском подстрок в строках текста занимаются утилиты из семейства *grepfgrep, grep и egrep. Первая из них – fgrep (fast grep) – не использует регулярные выражения, она просто читает текст со стандартного ввода или из файла и выдает на стандартный вывод строки, содержащие заданный в командной строке шаблон-подстроку. Утилита grep интерпретирует свой первый параметр как базовое регулярное выражение и выдает строки, которые содержат подстроку, соответствующую этому РВ. Утилита egrep работает с расширенными РВ, а в остальном аналогична grep. Об имени (фамилии?) самого семейства – grep – рассказано в лекции 15.

Все утилиты семейства *grep распознают ключ -i (ignore case), при котором строчные и прописные буквы не различаются, ключ -v (invert), при котором выдаются строки, не содержащие шаблона, ключ -l, при котором выдаются только номера подходящих строк, и ключ -q (quiet, в cтарых версиях grep-s, silent), при котором вообще ничего не выводится, а команда используется ради кода ошибки, равного нулю, только когда шаблон найден. Кроме того, если в командной строке *grep всего один параметр, который не является ключом, этот параметр распознается как шаблон, а текст считывается со стандартного ввода; если же таких параметров несколько, то второй и последующие распознаются как имена файлов, и текст считывается из них. Наконец, если параметр-шаблон состоит из нескольких строк (закавычивание позволяет так сделать), считается, что каждая из них содержит отдельное выражение для поиска, и подходящей будет считаться строка входного текста, включающая любое из выражений.

Еще раз повторим, что задача grep – выбрать строки, части которых соответствуют регулярному выражению. В каком месте строки найдено соответствие и каким способом оно достигнуто, для grep неважно, важно только, нашлось или нет. В таком виде эта утилита весьма полезна при работе с системой и применяется повсеместно; нам настолько тяжело было отказаться от нее в наших примерах, что пришлось без особых объяснений ввести ее еще в лекции 6 для ограничения контекста в команде apropos.

Пример. Допустим, мы пишем сценарий на shell, в котором нам понадобится полное имя пользователя, запустившего этот сценарий. Полное имя пользователя (оно же GECOS) хранится в файле /etc/passwd. Для того чтобы извлечь его оттуда, надо знать входное имя пользователя, оно выдается командой logname. На всякий случай набираем в терминале

$ logname
max
$ egrep $(logname) /etc/passwd
max:x:510:500:Max B. Tough:/home/max:/bin/sh
maxim:x:541:500:Maxim Outsider:/home/maxim:/bin/sh
sorrow:x:612:600:Temporary account:/home/shamaxe:/bin/sh

А вот и ошибка! Нас интересует входное имя, а не любая подстрока из /etc/passwd. К счастью, входное имя нетрудно описать в виде регулярного выражения: оно находится в начале строки и заканчивается на “:“:

$ egrep "^$(logname):" /etc/passwd
max:x:510:500:Max B. Tough:/home/max:/bin/sh

Теперь осталось выделить из этой строки GECOS (“Max B. Tough”). Это пятое поле /etc/passwd (поля разделяются “:“); ниже будет показано, как выполнить такую задачу при помощи поиска регулярного выражения с заменой, но мы поступим проще: воспользуемся утилитой cut, которая как раз предназначена для вывода некоторых полей. Напишем команду в виде, готовом для сценария (выполним еще одну подстановку и присвоим результат переменной GECOS):

$ GECOS=$(egrep "^$(logname):" /etc/passwd | cut -d: -f5)
$ echo $GECOS
Max B. Tough

Готово!

Регулярными выражениями также пользуются не менее (а если вдуматься – гораздо более) популярная утилита less и ее прародительница more. Мы помним, что обе они предназначены для постраничного просмотра текстовых файлов, только less больше умеет. С некоторых пор к less приделали даже препроцессор – lesspipe; теперь если на вход less попадет нетекстовый файл, lesspipe превратит его в текстовый – распакует, если файл был упакован, покажет заголовок архива, если это – архив, на худой конец – запустит утилиту strings, которая вынимает из нетекстового файла все строки допустимого формата. В этих утилитах есть команда поиска по тексту – /, поиск вперед (а в less – и ?, поиск назад). Нетрудно догадаться, что в качестве шаблона в этих командах выступают именно регулярные выражения. Либо less либо more используется командой man, когда та показывает руководство, так что поиск в это время может очень пригодиться.

У less, кстати, есть одно незаметное для пользователя, но крайне полезное свойство. Показывая страницу помощи, less, как может, подкрашивает ее (подчеркиванием и ярким шрифтом). Так вот, операция поиска не зависит от того, какая часть найденной строки каким шрифтом набрана. Это совсем не тривиальное свойство, если вспомнить, что в самом отформатированном файле руководства, допустим, яркая буква A присутствует как три символа A+^H+A (напомним, что ^H – условное обозначение символа backspace (ascii-код 8, в восьмеричном виде – 10)), а подчеркнутая буква F – как _+^H+F. Однако, к счастью, команда /bn действительно находит bn в руководстве по less, хотя n выделена подчеркиванием. А grep без посторонней помощи не может справиться с такой задачей, приходится вызывать colcrt(1):

$ man less | colcrt | egrep bn
    -bn or --buffers=n

На вкус и цвет…

Настало время подлить ложку дегтя в бочку меда, которой представляется регулярное выражение. Существует стандарт на регулярные выражения – так называемый POSIX.2. В этот стандарт включены и базовое РВ, и расширенное РВ, что уже создает некоторую неразбериху. К сожалению, стандарт оставляет открытыми некоторые тонкости реализации разбора регулярного выражения, поэтому разные утилиты (точнее, разные библиотеки) могут обрабатывать эти частности по-разному. Не определены до конца всевозможные предположительно ошибочные ситуации, вроде незакрытой скобки или фигурных скобок без цифр внутри (считать их ошибкой или обычными символами?), странные конструкции, наподобие ‘**’ или ‘+?’, и многие \-последовательности (‘\n’ – это n или перевод строки?).

В теоретическом подходе символы, управляющие разбором регулярного выражения*, $, . и т. п., не принадлежат алфавиту, из которого формируются строки. В действительности алфавит приходится использовать один и тот же. Поэтому поначалу считалось, что только “*“, “.” и [ имеют специальное значение везде в РВ, а "^" и "$" - соответственно в начале и в конце. Все остальные символы считаются обычными, а если они должны быть специальными, тогда перед ними надо поставить "\". В базовых РВ это означает, что в группах вместо круглых скобок надо использовать \( и \), а в интервалах вместо фигурных - \{ и \}. В таком виде базовые РВ и вошли в стандарт, и именно так их понимают утилиты grep, sed, more, vi и некоторые другие. Если программа использует библиотеку GNU regexp (в Linux она входит в стандартную библиотеку libc), ситуация сложнее. В GNU regexp формат базового РВ получается из расширенного путем добавления \ ко всем спецсимволам, которые в базовом РВ считаются обычными. В ALT Linux, например, и grep, и sed, и more понимают шаблон 'etc\|bin' и исправно находят строки, в которых есть etc или bin:

linux$ ls -1 / | grep "etc\|bin"
bin
etc
sbin

Наконец, когда программисты расширяют синтаксис регулярного выражения сверх стандарта, они, как правило, не договариваются друг с другом. Так, границы слова в регулярных выражениях, используемых в языках Perl и Python, обозначаются \b (и начало слова, и конец), а в редакторе Vim - \< и \> соответственно. Именно в Perl и Vim, как считается, реализованы самые мощные расширения РВ. Perl regexp существует в виде самостоятельной библиотеки под именем pcre (perl compatible regular expression, см. pcre(3)). Похожие, тоже весьма мощные, диалекты PB используются в Python и некоторых других языках программирования. Собственный диалект расширенных РВ встроен в интегрированную среду программирования GNU Emacs.

Таким образом, существует несколько стилей оформления регулярных выражений. То, какой именно стиль использует утилита, зависит и от нее самой, и от того, какой regexp-библиотекой она пользуется, и от версии этой библиотеки. Однако путаница эта преодолима. Во-первых, расширенное РВ в различных реализациях практически совпадает. Во-вторых, хотя упомянутые Perl regexp и Vim regexp и отличаются друг от друга и от стандарта, но в них соблюдается совместимость с предыдущими версиями тех же библиотек. Вы можете отказаться от мысли использовать одно и то же регулярное выражение в разных инструментах и задействовать все способности того же Vim, не опасаясь, что к очередной версии или в другой ОС внезапно поменяется его стиль.

Потоковый текстовый редактор

До этого мы все время говорили только об операции поиска подстроки в строке, однако в качестве основной задачи постоянно упоминалось исправление текстовых файлов. Здесь, кроме шаблона поиска, необходим алгоритм изменения. Простейшим алгоритмом представляется поиск с заменой, в сочетании с регулярным выражением для решения многих задач его бывает достаточно.

Для поиска с заменой можно воспользоватья утилитой sed (stream editor - поточный редактор). Редактор sed весьма точно соответствует идеологии автоматического исправления проекта: это текстовый редактор, которым управляет не пользователь, а заданный sed-сценарий. Язык sed довольно своеобразен и слегка неудобочитаем: все его операторы - однобуквенные (так программисты понимали З тридцать лет тому назад... в чем-то они, наверное, были правы). Зато в нем операторов чуть больше дюжины, и все они подчиняются принципу аббревиативности (команда - первая буква слова, которое обозначает действие этой команды, например i - insert). Из-за своей краткости sed - самая удобная утилита для работы в командной строке.

Поиск с заменой в sed выполняет команда s (search). Синтаксис у нее такой: s/что_искать/на_что_заменять/. Не разбираясь пока в других командах, выполним обычную замену одной подстроки на другую. Для опытов изготовим файл из руководства по less (оно везде одинаково), обработав его уже известной нам командой colcrt:

$ man less | colcrt > dummy

Используем sed. Первый параметр - мини-сценарий, все остальные, если они - не ключи, представляют собой имена обрабатываемых файлов. Если неключевой параметр один, sed, как и многие утилиты UNIX, работает в режиме фильтра: читает со стандартного ввода и выводит на стандартный вывод. Заменим в файле dummy, скажем, file на fly и внимательно изучим получившийся текст из жизни насекомых:

$ sed 's/file/fly/' dummy | less
. . .

Обратим внимание на строчку named flys has been viewed previously, the new files may be entered (можно, например, поискать командой / подстроку previously). В этой строке, да и во всех остальных, sed заменил только первое найденное соответствие, причем сделал это, не разбирая, меняет ли он целое слово или только часть (а мы его о другом и не просили). Заменить все вхождения подстроки sed может и сам, для этого надо указать после последнего / в команде s модификатор g (global). Тогда, выполнив первую замену в строке, sed продолжит искать оставшиеся в ней соответствия.

А вот со словами придется разбираться самостоятельно. Неологизмы logfly или flyname выглядят вполне симпатично, но странное слово flys, образовавшееся из files, явно нуждается в доработке: по правилам английского языка множественное число от fly - flies. Это просто: никто не мешает выдать sed две команды; shell будет считать одним параметром все, что заключено между апострофами, переводы строки в том числе:

$ sed 's/files/flies/g
> s/file/fly/g' dummy

Стоит заметить, что в обратном порядке эти команды sed сработали бы по-другому: сначала команда 's/file/fly/g' заменила бы все file на fly, а 's/files/flies/g' работы вовсе не досталось бы.

Но, допустим, мы хотим заменять в этом тексте только слово file, а всевозможные profile и filename оставить как есть. Тут бы очень пригодились позиционные расширения РВ, отмечающие границы слова, но в sed их нет, потому что их нет в базовых РВ (в новых версиях sed обычно поддерживается некоторое приближение к расширенным РВ, но именно word boundaries устроены в каждом по-своему). Тогда поступим так: будем искать слово file, окруженное небуквами, и заменять его. Возникает сразу три затруднения.

Первое: регулярному выражению '[^a-z]file[^a-z]‘ соответствует подстрока из шести символов, поэтому команда ‘s/[^a-z]file[^a-z]/fly/g’ наделает немало беспорядка, удалив по символу с каждой стороны от file. Требуется средство запомнить то, что было вокруг file, и вставить это вокруг fly. Иными словами, найденные при помощи РВ подстроки хотелось бы уметь использовать при подстановке. На помощь приходит операция группирования (напомним, что в sed, согласно правилам базового РВ, группа создается при помощи \( и \)). Группы перенумерованы в порядке появления в РВ: группа, определяемая первой по счету открывающей скобкой, имеет номер 1, определяемая второй – 2 и т. п. Из найденной подстроки выделяются фрагменты, соответствующие каждой группе (если группы вложены друг в друга, что допустимо, фрагменты могут пересекаться); эти фрагменты имеют ту же нумерацию, что и группы. Фрагмент можно использовать в строке-подстановке любое число раз (можно и не использовать) в виде конструкции \номер_фрагмента.

Задачу с file можно было бы решить так:

$ sed 's/\([^a-z]\)file\([^a-z]\)/\1fly\2/' dummy

При этом на место \1 подставится то, что найдено по первому ‘[^a-z]‘, а на место \2 – то, что найдено по второму.

Попробуем разобраться подробнее, как работает редактор sed. Входной текст sed считывает построчно. К считанной строке он по очереди пробует применить каждую команду сценария. Применив все возможные команды к строке, sed выводит на стандартный вывод то, что от нее осталось. Команда сценария может начинаться с т. н. контекстного адреса, определяющего свойства строк, к которым эту команду можно применять. Простой контекстный адрес – это номер строки (команда применяется к единственной – совпадающей по номеру – строке входного потока), знак $ (команда выполняется после закрытия входного потока) или регулярное выражение (команда применяется ко всем строкам, в которых найдено соответствие этому РВ). Например, команда ‘sed “1s/_/ /g”‘ заменит в первой строке все подчеркивания на пробелы, а ‘sed “/^a/d”‘ удалит (delete) все строки, начинающиеся с a.

Два контекстных адреса, соединенные запятой, – это контекстный адрес-диапазон. Команды, снабженные таким адресом, применяются ко всем строкам, начиная с той, что удовлетворяет первому адресу диапазона и заканчивая той, что удовлетворяет второму. Например, ‘sed “10,20d”‘ удалит 11 строк текста, с десятой по 20-ю включительно, ‘sed “/if/,/fi/s/^/#/”‘ закомментирует все многострочные условные операторы в командном сценарии (если встретится однострочный, вида ‘if.*fi’, случится неприятность).

Мы обещали показать, как решить задачу с выделением GECOS из файла /etc/passwd при помощи поиска с заменой. Для этого нам понадобится еще одно свойство sed – ключ -n, с которым на стандартный вывод ничего не выводится, если не попросить специально. Попросить можно командой p (print) или модификатором p команды s. Окончательное решение выглядит так (обе подстановки и присвоение мы для простоты опустим:

$ sed -n "/^max:/s/\([^:]*:\)\{4\}\([^:]*\).*/\2/p"
                                        /etc/passwd
Max B. Tough

Перепишем выражение в расширенный формат, избавившись от лишних \:

/^max:/s/([^:]*:){4}([^:]*).*/\2/p

и разберемся. ‘/^max:/’ в начале – это контекстный адрес: команда применяется только к строкам, начинающимся на max:passwd такая одна). Дальше идет команда поиска с заменой. Выражению ‘[^:]*:’ соответствуют подстроки, имеющие : только в конце (так выглядят все поля passwd, кроме последнего). ‘([^:]*:){4}’ означает повторение такой посдстроки четыре раза, так что следом за ним идет как раз нужное нам пятое поле. ‘([^:]*)’ помечает его как вторую группу. ‘.*’ необходимо для того, чтобы s “съела” остаток строки, иначе она оставила бы его без изменения. ‘/\2/’ означает “заменить на содержимое второй группы”. И p в конце означает “выдать на стандартный вывод”, потому что с ключом -n sed сам этого не делает. Просто, не правда ли?

Но вернемся к файлу dummy, в котором мы хотим заменять только целые слова file, и для этого проверяем, что перед шаблоном и после него стоят не буквы. Второе затруднение – в том, что перед шаблоном и после него может вообще ничего не стоять! Тогда наше решение не сработает, в чем легко убедиться, обработав его выдачу с помощью | grep ‘file$’. Так что придется обработать еще три случая – ‘^file[a-z]‘, ‘[a-z]file$’ и ‘^file$’.

Есть еще третье затруднение, самое неприятное. Непонятно, что делать со строками вида -file-file-file-…. Заменив первый -file- на -fly-, sed ищет следующее после замены соответствие, которое начнется только с третьего -, и второе file будет, увы, пропущено. Средствами базовых РВ устранить это затруднение нельзя.

Дополнения РВ и инструменты, использующие РВ

Нам не пришлось бы ничего запоминать и восстанавливать, если бы в регулярных выражениях были средства задания контекста: специальные РВ, указывающие условия, в которых должна встречаться подстрока, но не входящие в нее. Дополненное таким образом регулярное выражение уже не будет регулярным – описываемый им язык выйдет за рамки класса регулярных формальных языков. Это дополнение называется предпросмотром (lookahead и lookbehind), оно реализовано во многих диалектах РВ. Так же, как и “нежадные” повторители, предпросмотр – довольно опасная конструкция, поведение которой во многих случаях неочевидно.

Завершая описание sed, отметим, что во всех примерах в качестве выходного потока у нас фигурировал стандартный вывод. Если в командном интерпретаторе попробовать перенаправить вывод любого фильтра обратно в тот же файл, хорошего будет мало. Поэтому надо либо заводить временный файл вывода, а потом переименовывать его, либо пользоваться особым свойством некоторых фильтров: редактировать не поток, а файл, переписывая его только после окончания работы (т. н. in-place editing). Для этого в FreeBSD-версии у sed существует ключ -i, а в ALT Linux – отдельная команда subst.

На случай, когда средств редактора sed недостаточно, – а это бывает, если алгоритм изменения проекта сложнее простой замены, – в UNIX существует целый спектр более сложных инструментов, сочетающий обработку текста с программированием. Ближайший к sed – язык обработки текстов AWK, получивший название в честь фамилий авторов: Alfred V. Aho, Peter J. Weinberger, Brian W. Kernighan. В awk есть расширенное по отношению к sed понятие контекстного адреса, команды записываются на языке, подобном Си, есть переменные, работа с несколькими потоками данных и т. п. Вариант AWK, разрабатываемый GNU, – gawk – существенно дополняет стандартные возможности, но не изменяет идеологии.

Язык программирования Perl – сумма возможностей sed, awk, Си, дополненная мощными собственными инструментами. Perl называют “мечтой системного администратора” (или “мечтой лентяя”, что одно и то же), потому что решение любой небольшой задачи на Perl можно написать очень быстро, коротко и совершенно нечитаемо для постороннего глаза. Впрочем, соблюдая дисциплину программирования, на Perl, как и на любом мощном языке, можно писать много и понятно. Дальше идут уже “классические” и “неоклассические” высокоуровневые языки программирования, вроде LISP или Python, в которых богатые диалекты регулярных выражений встроены в библиотеки.

 

Источник: INTUIT.ru

Похожие посты
  • Описание более 350 команд Linux c примерами.
  • FreeBSD: Базовый курс
  • Привилегии групповой политики: понимание и применение нацеливания на уровень элементов
  • Шелл, терминал и консоль – основы
  • Сервер каталогов LDAP как источник аутентификации – учётные записи
  • Однострочные скрипты SED
  • Метод использования одного почтового домена двумя почтовыми системами.
  • Сервер каталогов LDAP как источник аутентификации – интеграция с AD
  • Командные интерпретаторы: сравнения и история
  • LPI 101: GNU и UNIX команды. Приоритеты исполнения процесса
  • 3 комментов оставлено (Add 1 more)

    1. “Наконец, выражению ‘(Foo)*’ будут соответствовать подстроки Foo, FooFoo, FooFooFoo и т. д., но не будут соответствовать подстроки Fo, FFFoooooo, FFoo и Fwoo”

      Да уж… Выражению ‘(Foo)*’ соответствует вообще любая строка (где Foo встречается 0(!) или более раз) — это раз, а два — это то, что ‘(Foo)’ не соответствует Fo и Fwoo из примера, а вот FFFoooooo и FFoo как раз таки соответствует!

      1. Yando on December 28th, 2007 at 3:13 pm
    2. я думаю отчепятка, ага :)

      2. molse on December 25th, 2007 at 9:30 pm
    3. Спецсимвол . – атомарное РВ, которому соответствует подстрока из одного любого символа. Например, M или @ соответствуют ‘.’, а или abc – не соответствуют (хотя abc содержит три фрагмента – a, b и c, соответствующих ‘.’).

      Ага, взорви моск всякому, пытающемуся понять регулярные выражения фразой «M или @ соответствуют ‘.’, а или abc – не соответствуют».
      Эт почему же а не соответствует ‘.’ ??? :)))

      3. Yando on December 25th, 2007 at 9:25 pm

    Комментарии

    Your email is never published nor shared. Required fields are marked *

    *
    *