Next Previous Contents

Unix Review Column 9

Randal Schwartz

Июль 1996

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

Сначала, давайте воссоздадим простейшую форму программы, которая эмулирует стандартную команду grep из поставки Unix.


        #!/usr/bin/perl
        $search = shift;
        $showname = @ARGV > 1;
        while (<>) {
                next unless /$search/o;
                print "$ARGV: " if $showname;
                print;
        }

Первая выполняемая строка после заголовка #! получает первый параметр командной строки (находящейся в массиве @ARGV), вырезает его из массива и помещает в переменную $search. Здесь используется свойство, что по умолчанию оператор shift применяется к массиву @ARGV.

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

Затем идет типичный цикл (``diamond-loop''): каждая строка из файлов указанных в командной строке считывается в переменную $_, и тело цикла получает возможность работать со строкой.

Первая строка в цикле производит поиск значения хранимого в $search (оно интерпретируется как регулярное выражение Perl) внутри содержимого переменной $_. Если ничего не найдено, то мы переходим к следующей строке. Модификатор ``o'' в регулярном выражении является ускорителем работы -- без него регулярное выражение ``компилировалось'' бы на каждом проходе цикла, без нужды замедляя работу, поскольку наше регулярное выражение не меняется.

Затем происходит обращение к переменной $showname: если она имеет истинное значение, то нам необходимо вставить имя файла в выводимую строку. С счастью Perl хранит имя файла в переменной $ARGV (только случайно названной также как и дескриптор файла ARGV и массив @ARGV).

В заключение, независимо от того, выдается ли имя файла или нет, происходит вывод текущей строки (из переменной $_, поскольку оператор print без аргументов производит вывод переменной $_).

Так, что данная маленькая программа эмулирует простейшую форму команды grep: а именно, ее запуск в виде:


        grep регулярное_выражение [файл ... ]

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

Итак, зачем я переписываю grep? Затем, что я могу придать ему дополнительную функциональность! Поставляемая с системой команда grep рассматривает и текстовые и двоичные файлы. Много раз мне хотелось просто выполнить что-то подобное:


        textgrep regex *

и чтобы эта волшебная команда ``textgrep'' просматривала только текстовые файлы, пропуская двоичные файлы соответствующие *. В частности мой личный каталог ``binary'' ($HOME/.bin) имеет набор двоичных программ, но в основном это исполнимые скрипты (в основном на Perl :-), и мне хотелось бы иногда выполнить:


        cd $HOME/.bin
        textgrep "строка" *

для выполнения поиска только в скриптах. Другим применением может быть запуск внутри ``рабочего'' каталога с набором исходных текстов программы и объектных файлов. Если вы ищете определенную строку в файле исходных текстов, то вы не хотите просматривать двоичный файл который был скомпилирован из файла исходных текстов!

Так что давайте посмотрим, сможем ли мы сделать из нашего ``grep-на-perl'' команду ``textgrep''. Первым шагом должно быть отличие текстовых файлов от двоичных. Perl выполняет это легко, используя встроенный оператор -T. Этот оператор возвращает истинное значение, если строка аргумента (имя файла) или файловый дескриптор представляет ``текстовый файл''. Unix не имеет простого признака файла, является ли он ``текстовым'' или ``двоичным'', так что вместо этого Perl считывает кусок данных из файла и пытается угадать является ли этот файл текстовым или двоичным. Обычно он угадывает правильно, но иногда может быть ошибка.

Поскольку массив @ARGV содержит имена файлов, то логично будет протестировать каждый из них с помощью оператора -T для определения к какому типу относится файл. Мы можем сделать это в сокращенной форме, используя оператор grep (не путайте с командой grep из Unix, хотя имя оператора было выбрано из-за похожего действия).

Оператор grep оценивает блок кода для каждого из элементов списка, временно помещая каждый из элементов в переменную $_. Для тех элементов, для которых блок кода вернул истинное значение эти элементы остаются в списке, так что на первый взгляд, мы можем просто записать:


        @ARGV = grep { -T } @ARGV;

Однако это не работает в тех случаях, когда нам необходимо использовать ``-'' в списке @ARGV. ``-'' обозначает ``считывание из стандартного ввода'', что к счастью считается текстовым файлом. Однако -T будет отбрасывать этот несуществующий файл, так что мы должны учитывать этот специальный случай. Это не так тяжело, но теперь код выглядит так:


        @ARGV = grep { -T or $_ eq "-" } @ARGV;

Великолепно! @ARGV теперь содержит только текстовые файлы! Только есть один раздражающий вариант. Когда оригинальный список файлов содержит только двоичные файлы, то теперь @ARGV будет пуст. Это неудобно, поскольку теперь программа будет производить считывание со стандартного ввода, даже хотя пользователь задал в командной строке набор имен файлов. Давайте взглянем как я обрабатываю данный случай в законченной программе:


        #!/usr/bin/perl
        $search = shift;
        $showname = @ARGV > 1;
        @ARGV = "-" unless @ARGV;
        @ARGV = grep { -T or $_ eq "-" } @ARGV;
        exit 0 unless @ARGV;
        while (<>) {
                next unless /$search/o;
                print "$ARGV: " if $showname;
                print;
        }

Заметьте, что четвертая строка заменяет пустой массив @ARGV на массив @ARGV состоящий исключительно из ``-''. Это не не изменяет поведения программы, но позволяет мне протестировать дальнейшее поведение программы при использовании пустого массива.

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

Теперь у нас есть программа, которую мы могли бы назвать ``textgrep''. Она не имеет аргументов, хотя и понимает ``-'' в списке файлов, что значит ``стандартный ввод'', и предполагает, что стандартный ввод всегда является текстовым файлом.

Давайте пойдем далее. Стандартная команда grep имеет ключ ``-l'' (маленькая буква L), которая заставляет выводить только список соответствующих файлов, без вывода совпавшей строки. Это полезно для выполнения различных операций. Hапример, для редактирования всех файлов из текущего каталога, которые содержат строку ``fred'', я мог бы сказать:


        vi `grep -l fred *`

Или для перемещения всех этих файлов в каталог ../freds,


        mv `grep -l fred *` ../freds

Так что давайте придадим ``textgrep'' такую же возможность. Это делается таким кодом:


        #!/usr/bin/perl
        $names++, shift if $ARGV[0] eq "-l";
        $search = shift;
        $showname = @ARGV > 1;
        @ARGV = "-" unless @ARGV;
        @ARGV = grep { -T or $_ eq "-" } @ARGV;
        exit 0 unless @ARGV;
        while (<>) {
                next unless /$search/o;
                if ($names) {
                        print "$ARGV\n";
                        close ARGV;
                } else {
                        print "$ARGV: " if $showname;
                        print;
                }
        }

Давайте рассмотрим дополнительные строки: первая строка после комментария изучает $ARGV[0] (первый аргумент). Если он равен ``-l'', то мы хотим работать в режиме ``только имена'', так что я устанавливаю флаг $names, и удаляю ключ ``-l'' из массива. К счастью, следующий элемент является регулярным выражением и оно сохраняется в следующей строке.

Другое изменение находится внутри тела цикла. Заметьте, что когда строка найдена, то я проверяю флаг $names. Если он имеет истинное значение, то программа выдает имя файла, за которым следует перевод строки, а затем закрывается файл ARGV. Мы закрываем файл ARGV поскольку оператор считывания автоматически перейдет к следующему файлу, когда мы вернемся к началу цикла. После нахождения требуемой строки нет необходимости в проверке остатка файла. Это также обеспечивает, что имя файла будет показано только однажды, вне зависимости от количества совпадений в файле.

Если $names имеет ложное значение, т.е. не был задан ключ ``-l'', то выполняется код из предыдущих версий, заключенный в блок ``else''.

Используя эту программу, я могу работать только с текстовыми файлами. Hапример, редактировать текстовые файлы в которых есть строка ``fred'':


        vi `textgrep -l fred *`

Или даже послать все текстовые файлы на устройство печати:


        pr `textgrep -l '^' *` | lpr -Pslatewriter

Заметьте, что ``^'' соответствует началу строки, что обычно является истинным для любого из файлов в которых производится поиск, но запомните, что textgrep отбрасывает нетекстовые файлы!

Как вы смогли увидеть, всего с помощью дюжины строк на Perl, я воссоздал очень популярную утилиту Unix, и даже придал ей дополнительную функциональность. Я надеюсь, что вы получили наслаждение от этого короткого экскурса в работу с текстом. Увидимся в следующий раз!


Next Previous Contents