Perl для системного администрирования

         

Обход файловой системы при помощи модуля File::Find


Теперь, когда вы познакомились с основами просмотра файловой системы, покажем более быстрый и изящный способ сделать то же самое. В Perl есть модуль F; ie: : Fi nd, позволяющий эмулировать команду find из Unix. Самый простой способ начать работать с этим модулем - воспользоваться командой find2perl для

генерирования кода-прототипа Perl.

С flnd2perl не всегда легко работать в Perl в отличных от Unix-системах. Для ее выполнения пользователям MacOS, например, понадобится Macintosh Programmer's Workshop (MPW) либо им придется изменить исходный код, чтобы получать ffARGV в диалоговом окне. Вот пример кода, позаимствованный у Криса Нандора (Chris Nandor), соавтора «MacPerl: Power and Ease» (MacPerl: Сила и легкость):

@ARGV = @ARGV 9 @ARGV :

split '\s". MacPerl: :Ask( "Arguments'7' );

Во всех перенесенных версиях есть модуль File; Fid, который используют команды find2perl и find.pl, так что ото не должно стать проблемой. Позже в этой главе мы покажем, как вызвать его напрямую.

Предположим, что вам нужна программа для поиска файлов beeskneen в каталоге /home. Вот командная строка, использующая команду Unix find:

% find /home -name beesknees -print

Передадим те же параметры команде

find2perl:

% find2perl /home -name beesknees -print и получим:

S'/usr/bin/oerl

eval 'exec /usr/bin/perl -S $0 ${1 + 'W}' if Srunning under some shell;

require "find.pl";

ff Обходим нужные файловые системы

&find('/home'):

exit;

sup wanted {

/"bee^kneesS/ && p--irt("$narrev' ';.

}

Код, сгенерированный командой find2perl, довольно прямолинеен. Он загружает необходимую библиотеку find.pl при помощи оператора require, затем вызывает подпрограмму &гinc() с именем начального каталога. Вскоре мы обсудим назначение подпрограммы &war'terj( ), поскольку именно здесь будут находиться все интересные изменения, которые мы хотим изучить. Перед тем как вносить изменения в этот код, важно обратить внимание на те немногие моменты, которые могут не показаться очевидными при рассмотрении приведенного фрагмента:




  • Те, кто работал над модулем File: :Find, столкнулись с проблемой переносимости этого модуля на другие платформы. Внутренние подпрограммы модуля File: : Find действуют так, что один и тот же код работает и в Unix, и в MacOS, и в NT, и в VMS и т. д.


  • Хотя код, сгенерированный find2perl, на первый взгляд похож на код Perl 4 (например, тут используется require для загрузки файла .pi), find.pl в действительности устанавливает несколько псевдонимов из PerlS. Обычно бывает полезно заглянуть «под завесу», прежде чем использовать модуль в собственной программе. Если вам нужен исходный код модуля, уже установленного в системе, то, выполнив команду perl -F или следующую команду, вы получите список каталогов стандартных библиотек:


  • % perl-e 'print join("\n",@INC,"")'

    Давайте поговорим о подпрограмме &wanted(), которую мы изменим для своих нужд. Подпрограмма &wanted() вызывается для каждого найденного &find() (или &File: : Find: :f ind(), чтобы быть точным) файла или каталога при обходе файловой системы. Именно код из &war': j(} должен выбирать «интересные» файлы или каталоги и работать с ними. В примере, приведенном выше, сначала проверяется соответствие имени файла или каталога строке beesknees. Если они совпадают, оператор && заставляет Perl выполнить оператор print, чтобы вывести имя найденного файла.

    При создании собственных подпрограмм &wanted() нам придется учитывать два практических момента. Поскольку &w,,nted() вызывается по одному разу для имени каждого файла или каталога, важно сделать этот код коротким и аккуратным. Чем быстрее мы сможем выйти из подпрограммы &warted(), тем быстрее find сможет перейти к новому файлу или каталогу и тем быстрее будет выполняться вся программа. Также важно иметь в виду «закадровые» соображения совместимости, о которых мы недавно упоминали. Было бы позором одновременно иметь переносимый вызов &fina() и системно-зависимую подпрограмму &wanted() (кроме случаев, где этого невозможно избежать). Несколько подсказок, помогающих избежать такой ситуации, можно получить, посмотрев на текст модуля File: : Find.



    Для первого использования модуля File:: Find давайте перепишем пример, созданный для удаления core-файлов, и затем немного его расширим. Для начала наберем:

    # find2perl -name core -print что даст нам следующее: require "! ind.pi"

    &find('. ' ) exit,

    sub wanted !

    /"coreS/ && DrintC'Sname^i"):

    }

    Затем мы добавим -s к строке вызова Perl и изменим подпрограмму

    &wanted():

    sub wanted (

    /"core$/ && prin+("$i>ame\n") && dpfired $r && ::nifik($rapf4;

    При вызове программы с ключом -г мы получаем необходимую нам функцию (удаление core-файлов). Внесем небольшое изменение, добавляющее некоторую степень защиты нашему потенциально разрушительному коду:

    oud wanted {

    /"coreS/ && -s $nairie *& print("$name\n") &&

    defined $r s& i;nlink($name); }

    Теперь, перед тем как выводить имя файла или удалять его, мы проверяем, не является ли размер файла core нулевым. Некоторые пользователи иногда создают ссылку на /dev/null с именем core в своем домашнем каталоге, чтобы core-файлы в нем не сохранялись. Параметр —s указывается для того, чтобы убедиться, что по ошибке не будут уда лены ссылки или файлы нулевой длины. Если мы хотим действовать осторожнее, нам следует выполнить еще две дополнительные проверки:

  • Открыть файл и убедиться, что этот файл действительно является core-файлом. Сделать это можно как из Perl, так и вызвав команду Unix file. Определить, что файл действительно является core-файлом, может оказаться достаточно сложно в случае, если файловые системы смонтированы по сети с компьютерами другой архитектуры с иными форматами core-файлов.


  • Посмотреть на время изменения файла. Если кто-то в настоящий момент отлаживает программу, используя файл core, он вряд ли обрадуется, если вы утащите этот файл прямо «из-под нее».


  • Давайте на время отдохнем от мира Unix и посмотрим на примеры, имеющие отношение к MacOS и Windows NT/2000. Ранее в этой главе я уже говорил, что в MacOS у каждого файла есть два атрибута - созда телъ и тип,



    позволяющие операционной системе определить, какое

    приложение создало этот файл и какого он типа. Эти атрибуты хранятся в виде четырехсимвольных строк. Например, для текстового документа, созданного приложением SimpleText, эти атрибуты будут иметь значения ttxt (для создателя) и TEXT (для типа). Из Perl (только для MacPerl) мы можем получить эту информацию при помощи функции MacPerl: :GetFileInfo(). Синтаксис ее таков:

    $type = MacPerl::GetFileInfo(filename); или:

    ($creator,$type) = MacPerl::GetFileInfo(filename);

    Чтобы найти все текстовые файлы в файловой системе MacOS, мы можем выполнить следующее:

    use File::Find;

    &File::Find::find(\&wanted,"Macintosh HD:");

    sub wanted{ -f $_ && MacPerl;:GetFileInfo($_) eq "TEXT" &&

    print "$Find::File::name\n"; }

    Вы, должно быть, заметили, что это выглядит немного иначе, чем наши предыдущие примеры. Однако действует этот код точно так же. Мы просто вызываем процедуры из File: :Find напрямую, без find.pl. Кроме того, мы используем переменную $name, определенную в пространстве имен File: :Find, чтобы вывести абсолютный путь файла, а не только его имя. Взгляните на полный список переменных, определяемых File: : Find при обходе файловой системы (табл. 2.2).

    Таблица 2.2. Переменные File::Find



    Переменная
    Смысл
    $_

    Имя текущего файла
    $File: :Find: :dir

    Имя текущего каталога
    $File: : Find: : name Полный путь для текущего файла (т. е. $File: : Find :dir/$_)
    Вот похожий пример, но для NT/2000:

    use File::Find: use Win32::File:

    &File::Find':find(\&wapted."\\");

    sub wanton i -f $.. && ft значение переменной attr присваивается функцией

    # Win32::File::GetAttributes (Win32: :File: :GetAttnbutes($_. Sattr)) &&

    ($at.tr & HIDDEN) &&

    print "SFile: : Find: : narreV1",

    }

    Этот пример ищет по всей файловой системе на текущем диске все скрытые файлы (т. е. те файлы, у которых установлен атрибут HIDDEN). Этот код работает и на NTFS и на FAT.



    А вот пример для файловой системы NTFS, который ищет все файлы, если к ним разрешен полный доступ для специальной группы Everyone, и выводит их имена:

    use File::Find;

    use Win32: : FileSecunty;

    tt Определяем маску DACL для полного доступа $fullmask = Win32:

    : FileSecunty: :MakeMask(FULL);

    &find(\&wanted,"\\");

    sub wanted {

    fl Win32::FileSecurity::Get не любит файл подкачки и pagefile.sys, пропустить его

    next if ($_ eq "pagefile.sys"); (-f $_) &&

    Win32: :FileSecunty: :Get($_, \%users) &&

    (defined $users{"Everyone"}) &&

    ($users{"Everyone"} == Sfullmask) &&

    print "$File::Find::name\n";

    }

    В вышеприведенном коде мы запрашиваем все файлы у списка контроля доступа ACL (кроме файла подкачки Windows NT). Затем мы проверяем, есть ли в этом списке запись для группы Everyone. Если есть, мы сравниваем запись Everyone со значением для полного доступа (полученным MakeMask()) и выводим абсолютный путь файла, если они совпадают.

    А вот еще один пример из реальной жизни, демонстрирующий, насколько полезным может оказаться даже самый простой код. Недавно я пытался дефрагментировать (заново перестроенный) раздел NT на диске своего портативного компьютера, но все закончилось сообщением об ошибке Metadata Corruption Error (повреждение метаданных). Внимательно изучая веб-сайт производителя программного обеспечения, я нашел там замечание, что «такая ситуация может быть вызвана наличием файлов, длина имен которых превышает допустимую в Windows NT». Там было предложено найти эти файлы, копируя каждый каталог на новое место и сравнивая количество файлов в оригинале л Обход файловой системы при помощи модуля File::Find

    копии. Если в копии каталога файлов меньше, необходимо найти те файлы, которые не были скопированы.

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

    require "find.pl";



    ft Обходим нужные файловые системы

    &find(\&wanted. '. ')'; print "max:$max\n";

    exit;

    sub wanted {

    return unless -f $_; if (length($_) > $maxlength)f $max = $name;

    Smaxlength = length($_); }

    if (length($name) > 200) { print $name,"\rT:}

    }

    В результате будут выведены имена файлов длиной более 200 символов, а также самое длинное найденное имя. Работа сделана, спасибо Perl.

    Давайте снова вернемся к Unix, чтобы закончить этот раздел довольно сложным примером. Идея, которая представляется слишком простой в контексте системного администрирования, но в итоге может принести огромную пользу - это понятие наделения пользователя полномочиями. Если ваши пользователи могут решить свои проблемы самостоятельно при помощи средств, которые вы им предоставляете, от этого все только выиграют.

    Большая часть этой главы посвящена решению проблем, возникающих при переполнении файловой системы. Зачастую это происходит из-за того, что пользователи недостаточно осведомлены о своем окружении, либо из-за того, что слишком обременительно выполнять операции по управлению дисковым пространством. Множество писем в службу поддержки начинаются со слов «В моем домашнем каталоге больше нет свободного места, но я не знаю из-за чего». Вот скелет сценария

    needspace, который может помочь пользователям, столкнувшимся с этой проблемой. Пользователь просто набирает needspace, и сценарий пытается найти в домашнем каталоге пользователя то, что

    можно удалить. Он ищет файлы двух типов: резервные копии и те файлы, которые можно автоматически создать заново. Давайте внимательно рассмотрим код:

    use File::Find; use File::Bascname;

    # массив расшипеннй файлов и расширений, из которых они могут быть получены

    % derivations = (" dvi" => '.tex".

    ".aux" => ".tex",

    ".toe" => ". tex"

    " . 0" =.' C".

    }

    Мы начнем с того, что загрузим нужные нам библиотеки: знакомый уже модуль File: :Fird и другую полезную библиотеку File: :Взяема;"е. Эта библиотека пригодится при разборе путей файлов. Затем мы инициализируем хэш-таблицу известными расширениями производных файлов; например, мы знаем, что если выполнить команду ТеХ или LaTeX для файла happy.tex, мы можем получить файл happy. Jui, и чти Обход файловой системы при помощи модуля File::Find



    happy. можно получить, скорее всего, скомпилировав файл happy.c

    компилятором С. Выражение «скорее всего» употреблено потому, что иногда требуется несколько исходных файлов, чтобы сгенерировать один файл. Однако мы можем делать только простые предположения, основываясь на расширениях файлов. Обобщенный анализ зависимостей - сложная задача, и мы даже не будем пытаться решать ее здесь.

    Затем мы определяем местонахождение домашнего каталога пользователя, получая идентификатор пользователя, выполняющего сценарий ($<), и передаем его функции getpwu;d(). qetowi,ii;() возвращает информацию из файла паролей в виде списка (подробности об этом позже); индекс массива ([7]) выбирает из этого списка элемент, соответствующий домашнему каталогу. Существуют способы получить эту информацию при помощи данных из командного интерпретатора (например, обратившись к переменной окружения $НОМЕ), но в таком виде код переносится лучше.



    Когда не надо использовать модуль File::Find


    Когда метод Filt: : Find не подходит? На ум приходят четыре ситуации:

  • Если файловая система, с которой вы работаете, не следует обычной семантике, вы не сможете применять этот модуль. Например, драйвер файловой системы NTFS для Linux, который я использовал при решении проблемы с упавшим компьютером, почему-то не выводил в пустых каталогах «. » или «..

    ». Это очень мешало File: : F i nrl.


  • Если вам нужно изменять имена каталогов во время обхода файловой системы, File: :Finn теряется и начинает вести себя непредсказуемым образом.


  • Если вам нужно разыменовывать символические ссылки на каталоги (для Unix), Fi le: : Find пропустит их.


  • Если вам нужно обойти файловую систему, смонтированную на вашей машине (например, файловую систему Unix, смонтированную через NFS на машине с Windows), File: :Find будет использовать семантику, принятую для «родной» файловой системы.


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

    Получив домашний каталог, мы переходим в него и начинаем сканирование, используя вызов &f ind() так же, как и в предыдущих примерах:

    $homedir = (getpwuid($<))



    # находим домашний катало; пользователе

    chdir($hO!!ied! r) or

    die "Невозможно войти в хк, домашний каталог Shoinodi':$!";

    $1=1; # не буферизованный вывод в STDOUT print "Поиск";

    find(\&wanted. "."), # проходим по каталогам, &wanted выполняет

    # всю работу

    Вот как выглядит вызываемая нами подпрограмма &wanted(). Сначала она ищет core-файлы, а также резервные копии и автосохраненные файлы, остающиеся после редактирования в emacs. Мы считаем, что эти файлы можно удалить, не проверяя существование исходных файлов (вероятно, это небезопасное предположение). Если такие файлы найдены, их размеры и пути к ним сохраняются в хэше, ключами которого являются пути к файлам, а значениями - размеры этих файлов.

    В остальной части подпрограммы подобным образом отыскиваются производные файлы. Мы вызываем подпрограмму uBascFiiotx, для того чтобы убедиться, что эти файлы можно получить из других файлов этого же каталога. Если подпрограмма возвращает значение «истина», мы сохраняем имя файла и его размер для последующего использования:

    sub wanted {

    ищем core-файлы, сохраняем их и возвращаемся $_ eq "core" &&

    ($core{$File ::Find :: n came} = (siai( })[7]) && return;

    # ищем резервные копии и автосохраненье после редактирования файла

    ($emacs{$File::Find::name} = (stat(_) ))[?]) &&

    return;

    && ($tex{$File: :Find: :name} = (stat) && return;

    # ищем производные файлов

    &BaseFileExists($File::Find::name) &&

    ($doto{$File::Find::name} = (stat(__))[7J) && return;

    Вот текст подпрограммы, проверяющей,

    можно ли получить данный файл из другого файла в этом же каталоге

    (например, существует ли файл happy, если мы нашли файл );

    sub BaseFileExists {

    my($name,$path,Ssuffix) = &File::Basename::fileparse($_[0], '\. . *' );

    # если мы не знаем, как получить файл этого типа return

    unless (defined Sderivations-1 ;{$suffix});

    # все просто, мы видели исходный файл раньше return 1 if (defined



    $baseseen{$path. $riame. $der rivations{$suf fix}});

    # если файл ( или ссылка на файл) существует и имеет ненулевой размер

    return 1 if (-s $name $derivations{$su '.,ffix} &&

    ++$baseseen

    {

    Вот как выполняется эта подпрограмма:

  • &File: : Basename: : fileparse() используется для выделения из пути имени файла, пути к файлу и его суг)ффикса (например resume.dvi, /home/cindy/docs/, .dui).


  • Затем суффикс файла проверяется, чтобы определить, считаем мы этот файл производным или нет. Если нет, мы возвращаем значение 0 (т. е. «ложь» в скалярном контексте).


  • Затем мы проверяем, встречался ли нам файл, исходный (base file) по отношению к данному, и если да, то возвращаем значение «истина». В некоторых ситуациях (в частности, в случае с TeX/LaTeX), из одного исходного файла можно получить несколько производных. Такая проверка ускоряет выполнение сценария, т. к. мы в этом случае избавлены от обхода файловой системы.


  • Если мы не встречали раньше исходный файл (с тем же именем, но другим расширением), то проверяем, существует ли он и больше ли нуля его размер. Если да, мы сохраняем информацию о файле и возвращаем 1 (т. е. «истина» в скалярном контексте).


  • Теперь нам остается только вывести информацию, которую мы собрали при обходе файловой системы:

    foreach my $path (keys %core){

    print "Найден core-файл, занимающий -1.&BytesToMeg($core{$path}).

    "MB в ",&File::Basename::dirname($path).".\n": }

    if (keys %emacs){

    print

    "Следующие файлы, скорее всего, являются резервными копиями, созданными emacs:\n";

    # изменяем путь, чтобы

    # вывод был аккуратнее print "$path ($emacs{$path} байт)\"

    }

    print "\пОни занимают ".&BytesToMeg($tempsize)."MB в сумме."; $tempsize=0; }

    if (keys %tex){

    print "Следующие файлы, скорее всего, можно получить заново, если

    # вывод был аккуратнее print "$path ($tex{$path} байт)":

    }

    print ЛпОни занимают ".&BytesToMeg(Stempsize),"MB в сумме.\п": $tenipsize=0: }



    if (keys %doto)! print

    " Следующие файлы, скорее, всего, можно получить, если вновь

    $path =" s/~$homedir/"/; tf изменяем путь, чтобы

    tt вывод был аккуратнее print "$path ($doto{$path} байт)";

    }

    print "\Они занимают ".&BytesToMeg($tempsize)."MB в сумме."; $tempsize=0; }

    sub BytesToMegl # преобразуем размер в байтах в формат ХХХМВ

    return sprintfCl%.2f,($_[0]/1024000)); }

    Прежде чем закончить этот разговор, надо заметить, что предыдущий пример можно расширять множеством способов. Пределов для программ такого типа просто не существует. Вот несколько идей:

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


  • Ищите каталоги, в которых веб-броузеры кэшируют страницы (это очень распространенный источник потерянного пространства на диске).


  • Предложите пользователю удалить найденные файлы. Для удаления файлов используйте оператор unlink() и подпрограмму rmpath из модуля File: :Path.


  • Больше анализируйте файлы, вместо того чтобы строить предположения по их именам.







  • Содержание раздела