Перевод 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
).
Я надеюсь, что вы получите удовлетворение от этого маленького введения в иерархические файловые системы, и от приведенных примеров, которые в будущем сэкономят вам времени.