Next Previous Contents

Unix Review Column 19 -- Подключение файлов

Randal Schwartz

Март 1998

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

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

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

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

    while (<>) {
        &process($_);
    }

где process является подпрограммой, определенной где-то в другом месте. Это базовый цикл ``считать за раз одну строку в $_''. Если вы попытаетесь запустить этот код, то вам надо будет определить данную подпрограмму, так что просто используем следующий код:

    sub process {
        my $line = shift;
        print "processed: $line";
    }

Давайте, теперь скажем, что некоторые строки в текстовом файле являются строками ``подключающими файлы''. Если строка имеет вид:

    #include fred

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

    if (/^#include (\S+)/) {
        $name = $1;
        ...;
    }

И теперь мы должны решить, что необходимо поместить в ``...''. Конечно, нам необходимо открыть файл, построчно считать его содержимое и закрыть файл. Это не так сложно -- у нас уже имеется имя файла:

    if (/^#include (\S+)/) {
        $name = $1;
        open F, $name or die
            "Cannot open $name: $!";
        while (<F>) {
            &process($_);
        }
        close F;
    } else { # wasn't include
        &process($_);
    }

И это работает внутри внешнего цикла while. Великолепно, теперь мы можем обрабатывать подключаемые файлы.

Хорошо, по крайней мере это работает на один уровень подключения файлов. Hо что будет, если подключаемый файл также захочет подключить другой файл? Это не будет работать при использовании нашего кода. Во внутреннем цикле ``чтения F'' нет ничего, что выглядело бы как код для включения файлов. Hо оба цикла вызывают подпрограмму &process, так, что это дает нам некоторые возможности. Давайте немного изменим логику программы:

    while (<>) {
        &process_or_include($_);
    }

    sub process_or_include {
        local $_ = shift;
        if (/^#include (\S+)/) {
            &include($1);
        } else {
            &process($_);
        }
    }

Здесь мы изменили внешний цикл так, чтобы он для каждой строки вызывал подпрограмму &process_or_include. Эта подпрограмма получает строку и помещает ее в новую локальную переменную $_ (для того, чтобы облегчить выполнение регулярного выражения).

Далее, если строка начинается с директивы ``include'' и содержит непустое имя файла, то мы вызываем подпрограмму &include (определена ниже), а если нет, то мы вызываем оригинальную подпрограмму &process с аргументом равным переданной строке.

Теперь мы должны определить подпрограмму &include, которой передается имя файла, и предположительно будем вызывать подпрограмму &process_or_include для каждой строки. Это не должно быть тяжело... просто напишите, как в предыдущем коде:

    sub include {
        my $name = shift;
        open F, $name or die
            "Cannot open $name: $!";
        while (<F>) {
            &process_or_include($_);
        }
        close F;
    }

Как и в предыдущем случае, переданное имя файла помещается в переменную $name, которая затем используется для открытия файлового дескриптора F (прекращая работу, если нет данных). Затем из файлового дескриптора построчно считываются данные в переменную $_, и затем для этих данных вызывается подпрограмма &process_or_include (определенная выше). Затем, когда данные заканчиваются, мы закрываем файловый дескриптор.

Так. Правильно. О, нет, не правильно. Почему неправильно? Hа первый взгляд это выглядит правильным, каждая строка файла, открытого на файловом дескрипторе F, обрабатывается или считается как директива подключения. Hо это является проблемой. Существует только одно имя файлового дескриптора и оно не является локальным для подпрограммы, так что каждый рекурсивный &include использует один и тот же файловый дескриптор F.

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

    use IO::File;

в начало нашего скрипта. Эта директива ``расширяет'' Perl подключением знания об объекте IO::File. не беспокойтесь: вам не нужно знание объектно-ориентированного программирования для использования возможностей этого модуля. Просто используйте несколько простых синтаксических конструкций.

Вместо открытия явного дескриптора файла, нам теперь необходимо открывать локальный для подпрограммы объект IO::File:

    sub include {
        my $name = shift;
        my $F = IO::File->new($name)
            or die "Cannot open $name: $!";
        while (<$F>) {
            &process_or_include($_);
        }
    }

Заметьте, что вместо вызова функции open для файлового дескриптора <F>, мы теперь вызываем метод new для возврата объекта IO::File в локальный скаляр $F. Затем мы можем использовать этот скаляр везде, где мы ранее использовали файловый дескриптор, и он работает как надо. Однако, рекурсивный запуск этой подпрограммы будет создавать совершенно новые объекты IO::File!

Нижняя часть этой подпрограммы считывает по одной строке, но теперь из объекта содержащегося в переменной $F. Данные все равно считываются в переменную $_, но при этом они проверяются на существование.

Другим побочным эффектом является то, что нам больше не надо закрывать дескриптор файла. Когда $f покидает область видимости (в конце текущей подпрограммы), то соответствующий ``дескриптор файла'' автоматически закрывается. Это очень хорошо.

OK, у нас теперь имеется превосходный процессор подключения файлов, который обрабатывает включение файлов в самих включаемых файлах. Как мы можем сделать его более полезным? Что если ввести ``пути поиска'', задающие места в которых файл может быть найден, если его имя задано относительным путем?

Это не нат трудно. Вам нужно сделать некоторые изменения к процедуре открытия файла. Давайте сделаем это в подпрограмме подключения файла.

    
    sub include {
        my $name = shift;
        my $F = &find_file($name);
        while (<$F>) {
            &process_or_include($_);
        }
    }

Имя подключаемого файла сохраняется в переменной $name, как и ранее. Затем вместо прямого открытия файла, мы вызываем процедуру &find_file (определенную ниже). Возвращаемое значение является объектом IO::File, который точно также используется как и ранее, включая его окончательное освобождение в конце подпрограммы.

Подпрограмма &find_file определена следующим образом:

    sub find_file {
        local $_ = shift;
        if (/^\//) { # absolute
            return &must_open($_);
        }
        # relative
        for $dir (@path) {
            my $full = "$dir/$_";
            if (-e $full) {
                return &must_open($_);
            }
        }
        die "Cannot find $_ in @dirs";
    }

Входящий параметр является именем файла, который необходимо найти в каталогах, находящихся в путях поиска. Имя файла сохраняется в локальной переменной $_. Если имя в $_ начинается с прямого слэша, то оно является абсолютным путевым именем и мы должны использовать как есть. Так что мы вызываем подпрограмму &must_open (определенную ниже), передавая ей полной имя файла и возвращая то, что она возвращает нам (если есть) и что должно быть объектом IO::File.

Если имя не начинается с символа слэш, то нам необходимо попытаться найти файл в каждом из каталогов, определенных в путях поиска. Переменная $dir устанавливается равной каждому из элементов массива @path. Полное путевое имя помещается в переменную $full (временную скалярную переменную). Если файл с таким именем существует (тестируется с помощью -e), то мы пытаемся открыть его вызывая ту же самую подпрограмму &must_open. Если файл не существует, то мы пробуем использовать следующий путь из списка.

Если все пути из массива @path были использованы и файл не был найден, то эта подпрограмма выполняет die с соответствующим сообщением об ошибке.

Функция &must_open определена следующим образом:

    sub must_open {
        my $name = shift;
        IO::File->new($name) or die
            "Cannot open $name: $!";
    }

В подпрограмму передается имя файла $name, который будет открыт, или мы выполним die. Если открытие файла прошло успешно, то созданный объект становится возвращаемым значением. Мы можем передавать объекты IO::File через стэк --это намного удобней, чем файловые дескрипторы!

Таким образом, этот пример показал некоторые основы обработки текста, применение локальных файловых дескрипторов и рекурсивных подпрограмм. Не так плохо для небольшой утилиты. Увидимся с вами далее...


Next Previous Contents