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

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

Выбор алгоритма

Для сравнения каталогов можно использовать несколько различных алгоритмов. Например, можно применить алгоритм сравнения, который будет считать количество каталогов в дереве, количество файлов, а также анализировать имена файлов, их размеры и временные параметры. Идеально было бы кроме этого выполнять также анализ содержимого файлов, чтобы удостовериться, что одни и те же файлы содержат одни и те же данные. Бывают ситуации, в которых два различных файла могут иметь один и тот же относительный путь и размер, но при этом различаться по содержимому. Рассмотрим пример. Имеется два файла: C:Dir1 eadme.txt and C:Dir2 eadme.txt. Относительные пути у этих файлов одинаковые (относительно C:Dir1 или C:Dir2), имена одинаковые (readme.txt), размер файлов также совпадает (21 байт). Однако при этом один из них содержит текст This is a readme file («это файл readme»), а другой - текст I think I smell food! («Я думаю, что чую пищу!»).

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

В ходе создания алгоритма было установлено, что можно использовать единственный хеш, в котором описание каждого пути будет храниться в виде ключа хеша (hash key). Значение, ассоциированное с ключом хеша, это подчиненный хеш (subhash), ключ которого указывает на анализируемый каталог (1 или 2), а значение ключа определяет размер файла. Результирующий хеш может выглядеть так, как показано на Рисунке 1. В рассматриваемом примере хеша файл с именем FileNumber1.txt существует в обоих каталогах (1 и 2), и оба файла имеют одинаковый размер (1234 байта). Однако файл FileNumber2.txt существует только в каталоге 1 и имеет размер 32 байта.

При использовании данного алгоритма формирование отчета о результатах должно быть достаточно несложным. Сценарий должен просто последовательно перебрать все ключи в хеше %FileList. Если значение ключа хеша %FileList содержит только один ключ подчиненного хеша с названием {1} или {2}, следовательно, файл существует только в одном каталоге, и сценарий должен вывести путь к этому файлу на экран. Если существуют оба ключа ({1} и {2}), сценарий должен сравнивать их значения. Если эти значения не совпадают, значит файлы имеют разный размер, и в этом случае сценарий должен вывести на экран пути к каждому из этих файлов и их размер.

Реализация алгоритма

Сценарий DirDiff.pl демонстрирует, как был реализован тот алгоритм, который я решил использовать. Фрагмент этого сценария приведен в Листинге 1. Во фрагменте, обозначенном в Листинге 1 меткой A, объявляется часть переменных, используемых сценарием. В строке use vars объявляются глобальные переменные %Config и $gFileCount. Необходимо, чтобы эти переменные были доступны для обращения к ним из различных процедур. Таким образом, лексически они не могут быть объявлены с помощью ключевого слова my.

Переменные %FileList, %File и %Size объявляются в начале сценария, поскольку далее их будет использовать команда write процедуры PrintReport(). Для того чтобы команда write могла корректным образом отображать значения этих переменных, они должны быть объявлены локально в данном конкретном диапазоне. Однако, поскольку сценарий использует конструкцию strict (как это должно быть в любом грамотном сценарии Perl), эти переменные должны быть сначала объявлены. Так как вы не можете локализовать область действия лексических скалярных переменных (т.е. тех, которые были созданы с помощью ключевого слова my), то они объявляются лексически в начале сценария. Обычно переменные Perl подобным образом не объявляются, и единственная причина, по которой это было сделано в данном сценарии, состоит в использовании команды write.

Основной блок сценария DirDiff.pl обозначен меткой B. В данном блоке кода для каталогов, подлежащих сравнению, вызывается процедура CollectFileList(), после чего каталоги последовательно обрабатываются в цикле foreach. Казалось бы, проще было в явном виде прописать два вызова CollectFileList() и передать имена двух каталогов, но тогда сценарий будет менее функциональным. В конце имеется блок кода, вызывающий процедуру PrintReport().

Во фрагменте, обозначенном меткой B, обратите внимание на оператор print, указывающий на дескриптор файла STDERR. Такие операторы print можно встретить в разных местах сценария DirDiff.pl. Они включены в сценарий для тех пользователей, которым требуется не выводить данные на экран, а перенаправлять их в файл. В результате использования этих операторов все данные, выводимые в STDOUT (дескриптор файла вывода, используемый по умолчанию ), будут перенаправлены, однако поток STDERR будет по-прежнему выводиться на экран. Таким образом, информация о ходе выполнения сценария не будет засорять данные, выводимые в файл.

Процедура CollectFileList(), обозначенная меткой C, создает список файлов, содержащихся в выбранном каталоге. Данная процедура допускает четыре параметра. Первый параметр ($Path) определяет путь к каталогу, который нужно проверить.

Второй параметр ($FileList) представляет собой ссылку на хеш %FileList. Ссылка используется из-за того, что данный хеш будет модифицироваться, а нам нужно, чтобы эти изменения отслеживались постоянно при каждом вызове процедуры. Можно применить другой способ, при котором вместо ссылки используется передача хеша, после чего возвращается его модифицированное содержимое. Однако при большом объеме каталогов размер хеша будет существенно увеличиваться, и передача таких больших объемов данных в процедуру и их последующий возврат отрицательно скажется на эксплуатационных качествах сценария в плане использования памяти и быстродействия.

Два последних параметра определяют проверяемый каталог ($Context) и относительный путь к нему ($RelativePath). Использование относительного, а не полного пути является важным моментом, поскольку сценарий должен проверять пути к файлам относительно этих двух каталогов. И несмотря на то, что полные пути к файлам никогда не будут совпадать друг с другом, для относительных путей это должно выполняться.

Когда сценарий открывает каталог, определяемый параметром $Path, далее обрабатывается каждый объект в данном каталоге. Сценарий создает строку, в которой путь к файлу размещается в одну строку (без автоматического переноса) на экране, а содержимое этой строки записывается в переменную $Pretty-Path. Чтобы исключить автоматический переход на новую строку, в описаниях путей к файлам используется процедура AbbreviatePath(), в которой часть символов опускается и заменяется многоточием (...), тем самым длина строки уменьшается в достаточной степени для ее вывода на экран. Далее строка $PrettyPath выводится в STDOUT для отображения хода выполнения сценария. В тех случаях, когда обрабатывается большое количество файлов, иметь информацию о ходе выполнения очень важно.

Далее DirrDiff.pl проверяет, является ли каждый из обнаруженных объектов каталогом. Если объект является каталогом, сценарий заносит его в массив @DirList для последующей обработки. Если объект представляет собой файл, то в этом случае извлекаются данные о его размере (если пользователь не указал в командной строке ключ -l, один из параметров командной строки, которые будут обсуждаться ниже), а информация о файле заносится в хеш %FileList. Если в каталогах требуется использовать рекурсию (что реализуется указанием в командной строке ключа -s), то в этом случае сценарий заносит имена каталогов в массив @DirList и для каждого из них последовательно вызывает CollectFileList().

Процедура PrintReport() (хотя это и не очевидно из ее названия) делает нечто большее, чем просто вывод на экран результатов работы сценария; она также выполняет сравнение каталогов. Как видно из фрагмента, обозначенного меткой D, в начале этой процедуры обнуляется значение $gFileCount, таким образом, его можно использовать для определения разницы в количествах файлов в каталогах. После записи в отчет заголовка процедура устанавливает формат данных файла. После этого каталоги подготавливаются к сравнению, для чего полный перечень путей к файлам заносится в хеш %FilList. по каждому из этих путей процедура проверяет существование файла в каталоге 1 и каталоге 2 и извлекает данные о размере файлов.

Фрагмент кода процедуры PrintReport(), который непосредственно выполняет сравнение файлов в двух каталогах, обозначен меткой E. Для каждого из файлов сначала проверяется, существует ли он в обоих каталогах. Если выясняется, что данный файл есть только в одном из каталогов, соответствующая информация о расхождении заносится в отчет. Если данный файл имеется в обоих каталогах, выполняется сравнение размера этих файлов в каталогах 1 и 2. Если выявлено расхождение (т.е. размеры различаются), соответствующие строки хеша %File модифицируются, и в них заносятся данные о размере файла. В конце данные о выявленных расхождениях выводятся на экран, для чего процедура использует команду write.

Как работать со сценарием

Для запуска сценария DirDiff.pl ему нужно передать данные о путях к двум сравниваемым каталогам. Например, если требуется сравнить содержимое каталогов C:Dir1 и C:Dir2, можно воспользоваться следующей командой запуска:

Perl DirDiff.pl C:Dir1 C:Dir2 -s

При этом будет произведен анализ всех вложенных подкаталогов. Если рекурсивный анализ подкаталогов не требуется, просто уберите из команды ключ -s.

Обработка многих тысяч файлов с извлечением данных об их размере может потребовать значительного времени (особенно если один из каталогов находится на удаленном компьютере или сетевом ресурсе), поэтому иногда бывает полезно отключить анализ размера файлов из процедуры сравнения. Для этого в строке запуска используется ключ -l. Например, для того чтобы сравнить только имена файлов в сетевых каталогах server_ashare_1 и server _bshare_1, можно использовать команду:

Perl DirDiff.pl
server_ashare_1 server_bshare_1 -s -l

Следует иметь в виду, что хотя здесь команда выглядит как две строки, при вводе ее в окне командной строки она должна быть размещена в одной. Данный сценарий был разработан и испытан на компьютерах с Windows Server 2003, Windows XP и XP 64-Bit Edition.

Простой, но полезный инструмент

Написав сценарий DirDiff.pl, я обнаружил, что использую его гораздо чаще, чем предполагалось. И хотя принцип его работы достаточно прост, это нисколько не умаляет его ценность.


Листинг 1: Фрагмент сценария DirDiff.pl

use strict;
use Getopt::Long;
# НАЧАЛО ФРАГМЕНТА A
use vars qw( %Config $gFileCount $KILOBYTE $MEGABYTE
$GIGABYTE $TERABYTE );
$TERABYTE = 1024 * ( $GIGABYTE = 1024 * ( $MEGABYTE = 1024 *
( $KILOBYTE = 1024 ) ) );
my %FileList;
my %File;
my %Size;
# КОНЕЦ ФРАГМЕНТА A

$| = 1;
Configure( \%Config );
if( $Config{help} )
{
Syntax();
exit;
}

# НАЧАЛО ФРАГМЕНТА B
foreach my $DirNumber ( 1, 2 )
{
$gFileCount = 0;
CollectFileList( $Config{dir_path}->{$DirNumber}, \%FileList, $DirNumber, "." );
}
print STDERR " ", " " x 80, " ";
PrintReport( \%FileList );
# КОНЕЦ ФРАГМЕНТА B

# НАЧАЛО ФРАГМЕНТА C
sub CollectFileList
{
my( $Path, $FileList, $Context, $RelativePath ) = @_;
my( @DirList );
if( opendir( DIR, $Path ) )
{
foreach my $Object ( readdir( DIR ) )
{
next if( "." eq $Object || ".." eq $Object );
my $ObjectPath = "$Path$Object";
my $RelativeObjectPath = lc "$RelativePath$Object";
if( ++$gFileCount < 200 || $gFileCount % 100 )
{
my $PrettyPath = sprintf( "%6d) %s", $gFileCount,
AbbreviatePath( $ObjectPath, 70 ) );
print STDERR " " . "$PrettyPath" . " " x ( 79 - length( $PrettyPath ) );
}
if( -d $ObjectPath )
{
push( @DirList, $Object ) if( $Config{recurse_subdir} );
}
else
{
$FileList->{$RelativeObjectPath}->{$Context} =
( $Config{dont_collect_file_length} ) ? undef: -s $ObjectPath;
}
}
closedir( DIR );
}

foreach my $Dir ( @DirList )
{
CollectFileList( "$Path$Dir", $FileList, $Context, "$RelativePath$Dir" );
}
}
# КОНЕЦ ФРАГМЕНТА C

sub PrintReport
{
# НАЧАЛО ФРАГМЕНТА D
my( $FileList ) = @_;
$gFileCount = 0;
$~ = "Header";
write;
$~ = "Data";
foreach my $Path ( sort( keys( %{$FileList} ) ) )
{
foreach my $DirNumber ( 1, 2 )
{
$File{$DirNumber} = ( exists $FileList->{$Path}->{$DirNumber} )? $Path : "";
$Size{$DirNumber} = $FileList->{$Path}->{$DirNumber} || 0;
}
# КОНЕЦ ФРАГМЕНТА D
# НАЧАЛО ФРАГМЕНТА E
if( $File{1} ne $File{2} || $Size{1} != $Size{2} )
{
++$gFileCount;
foreach my $DirNumber ( 1, 2 )
{
$File{$DirNumber} = sprintf( "%s (%s)", $File{$DirNumber},
FormatNumberPretty( $Size{$DirNumber} ) )
if( exists $FileList->{$Path}->{$DirNumber} );
}
write;
}
# КОНЕЦ ФРАГМЕНТА E
}
}