Next Previous Contents

Unix Review Column 16 -- Рекурсивная работа с каталогами

Randal Schwartz

Сентябрь 1997

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

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

Например, возьмем типичную задачу: удаление ненужных пустых каталогов. Предположим, что у вас имеется WWW- или FTP-сервер для распространения пакетов программного обеспечения, и некоторые пакеты время от времени добавляются в, и удаляются из области доступа (скажем, /archive). Утилиты для ``публикации'' конкретного пакета знают достаточно для создания подкаталогов при необходимости, но по некоторым причинам утилиты, которые выполняют ``удаление'' пакета не удаляют родительский каталог пакета.

Через некоторое время дерево каталогов окажется частично заполненным, а некоторые каталоги будут пустыми. Вы вы решаете, что наступило время для очистки дерева каталогов, возможно в ночных заданиях cron. Вы захотите удалить все пустые каталоги, но оставить все каталоги, которые выполняют какую-либо работу.

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

    &smash("/archive");
    sub smash {
      my $dir = shift;
      opendir DIR, $dir or return;
      my @contents =
        map "$dir/$_",
        sort grep !/^\.\.?$/,
        readdir DIR;
      closedir DIR;
      foreach (@contents) {
        next unless !-l && -d;
        &smash($_);
        rmdir $_;
      }
    }

В начале, подпрограмма &smash запускается с полным путевым именем каталога верхнего уровня нашего архива. Этот путь сохраняется в переменной $dir внутри подпрограммы, которая открывает заданный каталог. (Hеправильные каталоги автоматически пропускаются при сбое opendir).

Содержимое каталога затем очищается и обрабатывается. Читая 4-х строковое выражение с конца к началу, мы считываем каталог, отбрасывая ``.'' and ``..'', сортируем его в алфавитном порядке и помещаем имя каталога в начало каждого имени.

После этого, цикл foreach продвигает $_ по всему списку имен, находя возможные кандидаты в каталоги (не символьные ссылки, но каталоги). Для каждого из подкаталогов мы вызываем подпрограмму &smash и затем пытаемся удалить его в надежде, что он является пустым каталогом. Если rmdir() не выполняется, то это не является большой проблемой: это значит, что каталог все еще активен и служит реальным целям.

Поскольку этот код работает великолепно, проходя по подкаталогам и является общей операцией, что было бы стыдно снова и снова переписывать код, который выглядит примерно также. Для таких вещей очень легко сделать небольшую ошибку, (например не проверять символьные на ссылки при просмотре каталогов). К счастью, стандартная библиотека File::Find, предоставляет правильный код для выполнения таких операций.

Вот тот же код, переписанный с использованием стандартной библиотеки:

    use File::Find;
    finddepth (\&wanted, "/archive");
    sub wanted {
      rmdir $_;
    }

Здесь, File::Find устанавливает подпрограмму с именем finddepth. Эта программа принимает ссылку на подпрограмму (здесь это \&wanted) как первый параметр, и список каталогов, как следующие параметры. finddepth рекурсивно проходит через всю файловую систему, начиная с указанных каталогов и вызывая &wanted для каждого из найденных имен, с установкой некоторых переменных, чтобы сделать работу более легкой. Hапример, $_ устанавливается равной базовому имени файла (без имени каталога, но здесь все нормально, поскольку finddepth автоматически выполняет chdir() в родительский каталог). Так что мы можем попытаться выполнить команду rmdir() для этого имени, которая не будет выполняться, если это имя не является каталогом или не пусто!

Даже сейчас в этом коде немного больше текста чем возможно. Мы можем использовать анонимные подпрограммы вместо создания именованной подпрограммы:

    use File::Find;
    finddepth (sub {
                 rmdir $_;
               }, "/archive");

Здесь, конструкции sub { } достаточно для создания подпрограммы без имени и передачи ее coderef (ссылки на код) как первого параметра.

Вы даже может сделать все это из командной строки Unix:

    perl -MFile::Find \
        -e 'finddepth(sub {rmdir $_}, @ARGV)' \
        /archive

Здесь, -M выполняет тоже самое, что и директива use и мы будем брать аргументы из @ARGV, делая таким образом команду, которая легко превращается в алиас.

Если это кажется слишком сложным для запоминания, то вы также можете использовать великолепную утилиту find2perl для генерации кода, который выглядит примерно также. Эта утилита принимает расширенный синтаксис команды find, и создает скрипт для Perl:

                find2perl /archive -depth -eval 'rmdir $_' |
        perl

Заметьте, что здесь я сразу передаю скрипт Perl. (Для дополнительной информации смотрите справочную страницу для find2perl).

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

    use File::Find;
    find (sub {
            $size{$File::Find::name} =
              -s if -f;
          }, @ARGV);
    @sorted = sort {
      $size{$b} <=> $size{$a}
    } keys %size;
    splice @sorted, 20 if @sorted > 20;
    foreach (@sorted) {
      printf "%10d %s\n", $size{$_}, $_;
    }

Здесь я снова использую File::Find, который также определяет процедуру find вместе с finddepth, с аналогичными параметрами. (Различие между этими двумя процедурами является небольшим, отличаясь тем, получаем ли мы уведомление о конкретном каталоге до или после его содержимого). Внутри анонимной процедуры кроме получения $_ равной базовому имени файла, мы также получаем переменную $File::Find::name равную полному имени файла, что удобно здесь.

Для каждого из имен $_, которое является файлом (определяется с помощью -f), мы сохраняем его размер (полученный с помощью -s) в хэш, ключом которого является полное путевое имя ($File::Find::name). Когда данные получены полностью, мы получаем список файлов, отсортированный по их размерам, используя блок сортировки. Затем мы отбрасываем все записи после первых 20 и печатаем результат.

Маленький, элегантный, простой скрипт. File::Find позволил нам атаковать проблемы рекурсии намного проще, чем соответствующий скрипт командного процессора.

Давайте рассмотрим один более сложный пример. Предположим, что у нас есть исходные тексты утилиты zark (фиктивная утилита), которые распакованы в каталог /usr/local/src/zark, и мы хотим собрать эту утилиту машин supercycle и ultracycle, в отдельных каталогах, но совместно используя дерево исходных текстов насколько это возможно. Одним из способов является создание дерева символьных ссылок, которое отражает структуру дерева исходных текстов и содержит символьные ссылки на соответствующий файлы.

В частности, выполнение этих действий вручную будет выглядеть следующим образом:

    
        ## Bourne shell: создание переменных
    source=/usr/local/src/zark
    build=/usr/local/build/supercycle/zark
    mkdir $build
    ln -s $source/Makefile $build/Makefile
    ln -s $source/zark.c $build/zark.c
    ## и так далее...
    ## создание подкаталогов:
    mkdir $build/zifflelib
    ln -s $source/zifflelib/Makefile $build/zifflelib/Makefile
    ln -s $source/zifflelib/zif.c $build/zifflelib/zif.c
    ## и так далее...
    mkdir $build/zufflib
    ln -s $source/zufflib/Makefile $build/zufflib/Makefile
    ln -s $source/zufflib/zuf.c $build/zufflib/zuf.c
    ## и так далее...

Как вы можете видеть, это достаточно утомительно. Так, что выполним это простым способом... с помощью Perl:

    use File::Find;
    $src = shift; # первый аргумент -- исходные тексты
    $dst = shift; # второй аргумент -- целевой каталог
    find(sub {
           (my $rel_name = $File::Find::name)
             =~ s!.*/\./!!s;
           my $src_name = "$src/$rel_name";
           my $dst_name = "$dst/$rel_name";
           if (-d) {
             print "mkdir $dst_name\n";
             mkdir $dst_name, 0777
               or warn "mkdir $dst_name: $!";
           } else {
             print "ln -s $src_name $dst_name\n";
             symlink $src_name, $dst_name
               or warn "symlink $src_name $dst_name: $!";
           }
         }, "$src/./");

Самой сложной частью подпрограммы для find является определение общей части путевых имен исходных файлов и файлов, которые надо создать, что я делаю включая фиктивный путь "/./" в середину пути к исходным текстам. Это не затрагивает эффективного пути, но удобно для сканирования с помощью регулярного выражения (показано здесь в вычислении $rel_name).

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


Next Previous Contents