Next Previous Contents

Unix Review Column 27

Randal Schwartz

Август 1999

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

Символических ссылок не было в первых версиях Unix с которыми я работал. Это был Unix V6, в 1977 году, когда размер ядра Unix был меньше 32K. Трудно представить что-нибудь меньшее 32K и связанное Unix настоящих дней.

Но где-то в недрах университета Калифорнии в Беркли, в начале 80-х годом, люди работающие над BSD изобрели схему для исправления двух самых крупных проблем связанных с жесткими ссылками: ссылки не могли создаваться для каталогов и они не могли указывать на точку в другой смонтированной файловой системе. Их решением были символьные ссылки, которые сейчас являются распространенным свойством.

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

Символические ссылки достаточно легко создаются из командной строки:

        ln -s /usr/lib/perl5 ./Lib

что создает ссылку Lib указывающую на /usr/lib/perl5/. Из Perl, тоже самое выполняется следующим кодом:

        symlink("/usr/lib/perl5", "./Lib") or die "$!";

И вы можете увидеть результат с помощью команды:

        ls -l

что вам покажет что-то похожее на:

        ..... Lib -> /usr/lib/perl5

показывает что происходит перенаправление. И тот же факт можно увидеть из Perl с помощью кода:

        my $where = readlink("Lib");
        print "Lib => $where\n";

Но что делать, если /usr/lib сама является символической ссылкой, указывающей например на /lib? Хорошо, система правильно определит ее при выполнении спуску с /usr в /usr/lib и перенаправит программу в /lib и там продолжит поиск perl5.

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

Каким же самым простым способом можно узнать на какой реальный файл указывает символическая ссылка? Вы можете попытаться многократно запускать команду ls -l и запоминать пути или просто написать программу на Perl, которая выполнит для вас все необходимые расширения.

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

Вот программа, которая просто делает это, представленная по нескольку строк за раз.

    #!/usr/bin/perl -w
    use strict;
    $|++;

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

    use File::Find;
    use Cwd;

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

    my $dir = cwd;

Теперь мы будем получать имя текущего каталога используя вызов cwd (импортированный из модуля Cwd). Нам необходимо это для правильного расширения относительных путевых имен в абсолютные путевые имена.

    find sub {
      ##### содержимое этого куска представлено ниже
    }, @ARGV;

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

Теперь перейдем к подпрограмме. В настоящей программе этот код находится в вышеприведенном коде, в месте обозначенном #####.

      return unless -l;

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

Следующие два строки создают основу подпрограммы. Я получаю переменные @left и @right. Думайте о @left как ``как далеко я дереве файлов я нахожусь?'', а о @right как ``Куда еще я должен перейти?''. Основной задачей является взять один элемент из начала @right, и попытаться приклеить его в конец @left, до тех пор пока у нас не останется элементов в @right. Если на любом из этапов, путь находящийся в @left является символьной ссылкой, то мы должны расширить его и начать сначала. Также, если исследуемый элемент из @right является признаком текущего или родительского каталогов, то мы вместо этого должны убрать элемент из @left.

      my @right = split /\//, $File::Find::name;

Переменная $File::Find::name содержит полное путевое имя, начиная с имени которое мы задали в командной строке. Если это было относительное путевое имя, то это также будет путевым именем относительно первоначального рабочего каталога (теперь сохраненного в переменной $dir). Вот как я разбиваю путевое имя на отдельные элементы.

      my @left = do {
        @right && ($right[0] eq "") ?
          shift @right :            # быстрый способ
            split /\//, $dir;
      };    # первый элемент всегда равен null

Это немного запутанно, так что я буду объяснить это медленно. Мы устанавливаем @left равным значению этого выражения, получаемого из блока do. Если первый элемент @right является пустым, то оригинальная строка начинается с символа слэш и нам необходимо производить отсчет относительно корневого каталога. Это обрабатывается перемещением этого пустого элемента из начала @right для того, чтобы он только он стал элементом @left. В противном случае у нас имеется относительное путевое имя и мы будеи загружать переменную @left значениями разбитого на части путевого имени начального рабочего каталога.

      while (@right) {

Теперь, поскольку у нас имеются объекты по которым необходимо пройти, мы сделаем это...

        my $item = shift @right;
        next if $item eq "." or $item eq "";

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

        if ($item eq "..") {
          pop @left if @left > 1;
          next;
        }

А если у нас имеется две точки (символ родительского каталога), то мы будем удалять один уровень от нашей текущей позиции (пока не достигнем корня).

        my $link = readlink (join "/", @left, $item);

Теперь, если путь в @left, вместе со следующим шагом является символьной ссылкой, то будет определено значение $link равным тому значение, которым мы должны заменить значение $item. В противном случае, мы просто переходим дальше.

        if (defined $link) {
          my @parts = split /\//, $link;
          if (@parts && ($parts[0] eq "")) { # absolute
            @left = shift @parts;   # quick way
          }
          unshift @right, @parts;
          next;

Так, что если у нас имеет символьная ссылка, то мы снова разделим ее на части. Если она является абсолютным путевым именем, то значение @left будет равным корневому каталогу. В противном случае, значение @left остается тем же самым. Мы также поместим, то что нашли в начало оставшейся части @right, так что оно будет воздействовать на оставшуюся часть пути.

        } else {
          push @left, $item;
          next;
        }

Если на данном этапе объект не был символической ссылкой, то все просто; мы просто перемещаемся в эту точку в @left.

      }
      print "$File::Find::name is ", join("/", @left), "\n";

Когда цикл заканчивается, мы просто выдаем окончательный путь содержащийся в @left.

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

Теперь вы никогда не удивитесь тому, куда указывают ссылки. Встретимся в следующий раз, наслаждайтесь!


Next Previous Contents