Опубликован: 14.12.2004 | Уровень: для всех | Доступ: платный | ВУЗ: Компания ALT Linux
Лекция 14:

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

< Лекция 13 || Лекция 14: 12345 || Лекция 15 >

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

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

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

Поиск с заменой в sed выполняет команда s ( s earch). Синтаксис у нее такой: 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 ( g lobal). Тогда, выполнив первую замену в строке, 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"' удалит ( d elete) все строки, начинающиеся с a.

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

Мы обещали показать, как решить задачу с выделением GECOS из файла /etc/passwd при помощи поиска с заменой. Для этого нам понадобится еще одно свойство sed - ключ -n, с которым на стандартный вывод ничего не выводится, если не попросить специально. Попросить можно командой p ( p rint) или модификатором 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.

Подробное изложение различных алгоритмов разбора регулярных выражений, описание и сравнение различных стилей, замечания по поводу "нежадных" РB, предпросмотра и прочего, а также множество практических рекомендаций можно найти в [ 27 ] (действительно замечательная книга!).

На случай, когда средств редактора 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, в которых богатые диалекты регулярных выражений встроены в библиотеки.

< Лекция 13 || Лекция 14: 12345 || Лекция 15 >
Andranik Avakian
Andranik Avakian

41. УК РФ и Комментарии (ст. 273)

М. 2000 г. Издательство: ALT Linux, Институт Логики

Уголовный Кодекс РФ и комментарии к нему?

По ссылке открывается сайт документации Linux, раздел Linux Installation and Getting Started

Сергей Петровский
Сергей Петровский

У Вас написано:

ls -dt1 `grep -il отчет *` | head -1

если знания по шелу мне не изменяют, то должно быть:

ls -dt | `grep -il отчет *` | head -1

Светлана Мишланова
Светлана Мишланова
Россия, Волгоград
Илдар Аллаяров
Илдар Аллаяров
Россия