Как мне рекурсивно искать каталог и все подкаталоги с помощью perl

Я видел эту ссылку используя glob

Но это не совсем то, чем я хочу заниматься.

Вот мой план. Чтобы найти в каталоге любые файлы, которые частично соответствуют строке, заданной моей функции в качестве параметра, скажем /home/username/sampledata и строки, скажем, data.

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

Псевдокод того, который включает подкаталоги, будет выглядеть так.

Массив, в который я сохраняю пути к файлам, является глобальным

  @fpaths;

  foo($dir);

  sub foo{
      get a tmp array of all files

      for ($i=0 ; $i<@tmp ; $i++) {
          next if ( $tmp[$i]is a hidden file and !$hidden) ; #hidden is a flag too

          if($tmp[$i] is file) {
               push (@fpaths, $dir.$tmp[$i]);
          }
          if($tmp[$i] is dir) {
               foo($dir.$tmp[$i]);
          }

       }
   }

Выглядит довольно солидно.

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

Часть, которую я не знаю, как это сделать, - это получить список каждого файла. Надеюсь, это можно сделать с помощью glob.

Я смог использовать _4 _ / _ 5_ для чтения каждого файла, и я мог бы сделать это снова, если бы знал, как проверить, является ли результат файлом или каталогом.

Итак, мои вопросы:

  1. Как использовать glob с именем пути, чтобы получить массив каждого файла / подкаталога

  2. Как проверить, является ли элемент в ранее найденном массиве каталогом или файлом

Спасибо всем


person Chris Topher    schedule 23.05.2013    source источник
comment
for ($i=0 ; $i<@tmp ; $i++) { ... } условно пишется for my $i (0 .. $#tmp) { ... }   -  person Borodin    schedule 23.05.2013


Ответы (5)


  • Я не понимаю, почему glob решает вашу проблему, как проверить, является ли запись каталога файлом или каталогом. Если вы использовали readdir раньше, оставайтесь

  • Не забывайте, что вы должны осторожно обращаться со ссылками, иначе ваша рекурсия может никогда не закончиться.

  • Также помните, что readdir возвращает . и .., а также реальное содержимое каталога.

  • Используйте -f и -d, чтобы проверить, является ли имя узла файлом или каталогом, но помните, что если его loaction не является вашим текущим рабочим каталогом, вы должны полностью определить его, добавив путь, иначе вы будете говорить о совершенно другом узле, который, вероятно, не существует

  • Если это не учебный опыт, вам гораздо лучше написать что-нибудь готовое и протестированное, например File::Find < / а>

person Borodin    schedule 23.05.2013
comment
Да, я сначала проверяю скрытые файлы, их также необходимо исключить при проверке подкаталога. если у меня было полное имя файла, как мне проверить его с помощью -d / -f? это было бы так: if ($ path -d) print, это каталог? - person Chris Topher; 23.05.2013
comment
@ChrisTopher: Ссылки не имеют ничего общего со скрытыми файлы. Прочтите ссылку, на которую я указал в своем ответе, или ссылку, на которую вы указали в своем вопросе, чтобы узнать, как использовать -f и -d`. - person Borodin; 23.05.2013
comment
Это именно то, что я искал, я не мог сказать, что это ссылки. Ха, моя рекурсия работает, просто неправильно добавляется в массив. Огромное спасибо. Только во вторник начал изучать Perl, классный язык сценариев. Так что я ценю какое-то направление - person Chris Topher; 23.05.2013

Я бы использовал File::Find

Обратите внимание, что File::Find::name - это полный путь к данному файлу. Что будет включать каталоги, поскольку они также являются файлами.

Это всего лишь образец, чтобы читатель мог разобраться в остальных деталях.

use warnings;
use strict;
use File::Find;

my $path = "/home/cblack/tests";

find(\&wanted, $path);

sub wanted {
   return if ! -e; 

   print "$File::Find::name\n" if $File::Find::name =~ /foo/;
   print "$File::Find::dir\n" if $File::Find::dir =~ /foo/;
}

Еще лучше, если вы хотите поместить все это в список, вы можете сделать это так:

use File::Find;

main();

sub main {
    my $path = "/home/cblack/Misc/Tests";
    my $dirs = [];
    my $files= [];
    my $wanted = sub { _wanted($dirs, $files) };

    find($wanted, $path);
    print "files: @$files\n";
    print "dirs: @$dirs\n";
}

sub _wanted {
   return if ! -e; 
   my ($dirs, $files) = @_;

   push( @$files, $File::Find::name ) if $File::Find::name=~ /foo/;
   push( @$dirs, $File::Find::dir ) if $File::Find::dir =~ /foo/;
}
person chrsblck    schedule 23.05.2013
comment
Не совсем понимая, как вы перемещаетесь по каталогу, скорее всего, будет найдено несколько сотен файлов и дюжина случайных неизвестных подкаталогов. если какие-либо файлы или файлы в подкаталоге имеют имя, содержащее определенную строку, я хочу, чтобы весь путь был сохранен в массиве - person Chris Topher; 23.05.2013
comment
@ChrisTopher Я связал File::Find в первой строке своего сообщения. Вы должны это прочитать. Фактически, первая строка в описании модуля гласит: Это функции для поиска в деревьях каталогов, выполняющие работу с каждым найденным файлом аналогично команде find в Unix. А затем я перехожу к описанию функции в моем сообщении find. Почему бы вам не создать для себя небольшую песочницу, чтобы поиграть в ней? - person chrsblck; 23.05.2013
comment
Буду экспериментировать, спасибо - person Chris Topher; 23.05.2013
comment
@ChrisTopher Вот пример, делающий это рекурсивно без File::Find. Если вам интересно. - person chrsblck; 23.05.2013

Вдохновленный ответом Нимы Соруша, вот обобщенная рекурсивная функция глобализации, похожая на globstar в Bash 4 опция, которая позволяет сопоставить все уровни поддерева с **.

Примеры:

# Match all *.txt and *.bak files located anywhere in the current
# directory's subtree.
globex '**/{*.txt,*.bak}' 

# Find all *.pm files anywhere in the subtrees of the directories in the
# module search path, @INC; follow symlinks.
globex '{' . (join ',', @INC) . '}/**/*.pm', { follow => 1 }

Примечание. Хотя эта функция, сочетающая File::Find со встроенной функцией glob, вероятно, в основном работает так, как вы ожидаете, если вы знакомы с поведением glob, существует множество тонкостей в отношении сортировки и поведения символических ссылок - см. Комментарии внизу.

Заметным отклонением от glob() является то, что пробелы в данном аргументе шаблона считаются частью шаблона; чтобы указать несколько шаблонов, передайте их как отдельные аргументы шаблона или используйте фигурную скобку, как в примере выше.

Исходный код

sub globex {

  use File::Find;
  use File::Spec;
  use File::Basename;
  use File::Glob qw/bsd_glob GLOB_BRACE GLOB_NOMAGIC GLOB_QUOTE GLOB_TILDE GLOB_ALPHASORT/;

  my @patterns = @_;
  # Set the flags to use with bsd_glob() to emulate default glob() behavior.
  my $globflags = GLOB_BRACE | GLOB_NOMAGIC | GLOB_QUOTE | GLOB_TILDE | GLOB_ALPHASORT;
  my $followsymlinks;
  my $includehiddendirs;
  if (ref($patterns[-1]) eq 'HASH') {
    my $opthash = pop @patterns;
    $followsymlinks = $opthash->{follow};
    $includehiddendirs = $opthash->{hiddendirs};
  }
  unless (@patterns) { return };

  my @matches;
  my $ensuredot;
  my $removedot;
  # Use fc(), the casefolding function for case-insensitive comparison, if available.
  my $cmpfunc = defined &CORE::fc ? \&CORE::fc : \&CORE::lc;

  for (@patterns) {
    my ($startdir, $anywhereglob) = split '(?:^|/)\*\*(?:/|$)';
    if (defined $anywhereglob) {  # recursive glob
      if ($startdir) {
        $ensuredot = 1 if m'\./'; # if pattern starts with '.', ensure it is prepended to all results
      } elsif (m'^/') { # pattern starts with root dir, '/'
        $startdir = '/';
      } else { # pattern starts with '**'; must start recursion with '.', but remove it from results
        $removedot = 1;
        $startdir = '.';
      }
      unless ($anywhereglob) { $anywhereglob = '*'; }
      my $terminator = m'/$' ? '/' : '';
      # Apply glob() to the start dir. as well, as it may be a pattern itself.
      my @startdirs = bsd_glob $startdir, $globflags or next;
      find({
          wanted => sub {
            # Ignore symlinks, unless told otherwise.
            unless ($followsymlinks) { -l $File::Find::name and return; }
            # Ignore non-directories and '..'; we only operate on 
            # subdirectories, where we do our own globbing.
            ($_ ne '..' and -d) or return;
            # Skip hidden dirs., unless told otherwise.
            unless ($includehiddendirs) {  return if basename($_) =~ m'^\..'; }
            my $globraw;
            # Glob without './', if it wasn't part of the input pattern.
            if ($removedot and m'^\./(.+)$') { 
              $_ = $1;
            }
            $globraw = File::Spec->catfile($_, $anywhereglob);
            # Ensure a './' prefix, if the input pattern had it.
            # Note that File::Spec->catfile() removes it.
            if($ensuredot) {
              $globraw = './' . $globraw if $globraw !~ m'\./';
            }
            push @matches, bsd_glob $globraw . $terminator, $globflags;
          },
          no_chdir => 1,
          follow_fast => $followsymlinks, follow_skip => 2,
          # Pre-sort the items case-insensitively so that subdirs. are processed in sort order.
          # NOTE: Unfortunately, the preprocess sub is only called if follow_fast (or follow) are FALSE.
          preprocess => sub { return sort { &$cmpfunc($a) cmp &$cmpfunc($b) } @_; }
        }, 
        @startdirs);
    } else {  # simple glob
      push @matches, bsd_glob($_, $globflags);
    }
  }
  return @matches;
}

Комментарии

SYNOPSIS
  globex PATTERNLIST[, \%options]

DESCRIPTION
  Extends the standard glob() function with support for recursive globbing.
  Prepend '**/' to the part of the pattern that should match anywhere in the
  subtree or end the pattern with '**' to match all files and dirs. in the
  subtree, similar to Bash's `globstar` option.

  A pattern that doesn't contain '**' is passed to the regular glob()
  function.
  While you can use brace expressions such as {a,b}, using '**' INSIDE
  such an expression is NOT supported, and will be treated as just '*'.
  Unlike with glob(), whitespace in a pattern is considered part of that
  pattern; use separate pattern arguments or a brace expression to specify
  multiple patterns.

  To also follow directory symlinks, set 'follow' to 1 in the options hash
  passed as the optional last argument.
  Note that this changes the sort order - see below.

  Traversal:
  For recursive patterns, any given directory examined will have its matches
  listed first, before descending depth-first into the subdirectories.

  Hidden directories:
  These are skipped by default, onless you set 'hiddendirs' to 1 in the
  options hash passed as the optional last argument.

  Sorting:
  A given directory's matching items will always be sorted
  case-insensitively, as with glob(), but sorting across directories
  is only ensured, if the option to follow symlinks is NOT specified.

  Duplicates:
  Following symlinks only prevents cycles, so if a symlink and its target
  they will both be reported.
  (Under the hood, following symlinks activates the following 
   File::Find:find() options: `follow_fast`, with `follow_skip` set to 2.)

  Since the default glob() function is at the heart of this function, its
  rules - and quirks - apply here too:
  - If literal components of your patterns contain pattern metacharacters,
    - * ? { } [ ] - you must make sure that they're \-escaped to be treated
    as literals; here's an expression that works on both Unix and Windows
    systems: s/[][{}\-~*?]/\\$&/gr
  - Unlike with glob(), however, whitespace in a pattern is considered part
    of the pattern; to specify multiple patterns, use either a brace
    expression (e.g., '{*.txt,*.md}'), or pass each pattern as a separate
    argument.
  - A pattern ending in '/' restricts matches to directories and symlinks
    to directories, but, strangely, also includes symlinks to *files*.
  - Hidden files and directories are NOT matched by default; use a separate
    pattern starting with '.' to include them; e.g., globex '**/{.*,*}'
    matches all files and directories, including hidden ones, in the 
    current dir.'s subtree.
    Note: As with glob(), .* also matches '.' and '..'
  - Tilde expansion is supported; escape as '\~' to treat a tilde as the
    first char. as a literal.
 -  A literal path (with no pattern chars. at all) is echoed as-is, 
    even if it doesn't refer to an existing filesystem item.

COMPATIBILITY NOTES
  Requires Perl v5.6.0+
  '/' must be used as the path separator on all platforms, even on Windows.

EXAMPLES
  # Find all *.txt files in the subtree of a dir stored in $mydir, including
  # in hidden subdirs.
  globex "$mydir/*.txt", { hiddendirs => 1 };

  # Find all *.txt and *.bak files in the current subtree.
  globex '**/*.txt', '**/*.bak'; 

  # Ditto, though with different output ordering:
  # Unlike above, where you get all *.txt files across all subdirs. first,
  # then all *.bak files, here you'll get *.txt files, then *.bak files
  # per subdirectory encountered.
  globex '**/{*.txt,*.bak}';

  # Find all *.pm files anywhere in the subtrees of the directories in the
  # module search path, @INC; follow symlinks.
  # Note: The assumption is that no directory in @INC has embedded spaces
  #       or contains pattern metacharacters.
  globex '{' . (join ',', @INC) . '}/**/*.pm', { follow => 1 };
person mklement0    schedule 26.08.2015
comment
почему бы не перекинуть это на cpan? Не могу найти аналогичный модуль - person mikew; 29.08.2015
comment
Спасибо за предложение, @mikew. Подготовка этого CPAN требует немного больше работы и, следовательно, времени, которого у меня сейчас нет, но я буду иметь это в виду. - person mklement0; 30.08.2015

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

my @files;
push @files, list_dir($outputDir);

sub list_dir {
        my @dirs = @_;
        my @files;
        find({ wanted => sub { push @files, glob "\"$_/*.txt\"" } , no_chdir => 1 }, @dirs);
        return @files;
}
person Nima Soroush    schedule 25.04.2014
comment
Престижность за умную комбинацию File::Find и glob. Две тонкости: вы выполняете глобальный вызов каждого вызова подпрограммы wanted, что означает, что вы без необходимости вызываете glob() и для файлов, а не только для каталогов. И наоборот, если данный элемент представляет собой символическую ссылку на каталог, вы также глобализируете его содержимое (но не рекурсивно), что может быть неожиданным, учитывая, что вы не настраиваете find для отслеживания символические ссылки. Если вам не нужно следовать символическим ссылкам, вы можете вместо этого выполнить подстановку в подпрограмме preprocess и вернуть из нее пустой список. - person mklement0; 26.08.2015
comment
@ mklement0: Спасибо за тонкости. Вы абсолютно правы в этом - person Nima Soroush; 26.08.2015

Я пробовал реализовать это, используя только readdir. Я оставляю здесь свой код на случай, если он кому-то пригодится:

sub rlist_files{
    my @depth = ($_[0],);
    my @files;
    while ($#depth > -1){
        my $dir = pop(@depth);
        opendir(my $dh, $dir) || die "Can't open $dir: $!";
        while (readdir $dh){
            my $entry = "$dir/$_";
            if (!($entry =~ /\/\.+$/)){
                if (-f $entry){
                    push(@files,$entry);
                }
                elsif (-d $entry){
                    push(@depth, $entry);
                }
            }
        }
        closedir $dh;
    }
    return @files;
}

РЕДАКТИРОВАТЬ: как хорошо указано @brian d foy, этот код вообще не принимает во внимание символические ссылки.

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

sub rlist_files_nohash{
    use Cwd qw(abs_path);
    my $input_path = abs_path($_[0]);
    if (!defined $input_path){
        die "Cannot find $_[0]."
    }
    my $ignore_symlinks = 0;
    if ($#_>=1){
        $ignore_symlinks = $_[1];
    }
    my $regex;
    if ($#_==2){
        $regex = $_[2];
    }   
    my @depth = ($input_path,);
    my @files;
    my @link_dirs;
    while ($#depth > -1){
        my $dir = pop(@depth);
        opendir(my $dh, $dir) or die "Can't open $dir: $!";
        while (readdir $dh){
            my $entry = "$dir/$_";
            if (!($entry =~ /\/\.+$/)){
                if (-l $entry){
                    if ($ignore_symlinks){
                        $entry = undef;
                    }
                    else{
                        while (defined $entry && -l $entry){
                            $entry = readlink($entry);
                            if (defined $entry){
                                if (substr($entry, 0, 1) ne "/"){
                                    $entry = $dir."/".$entry;
                                }
                                $entry = abs_path($entry);
                            }
                        }
                        if (defined $entry && -d $entry){
                            if ($input_path eq substr($entry,0,length($input_path))){
                                $entry = undef;
                            }
                            else{
                                for (my $i = $#link_dirs;($i >= 0 && defined $entry); $i--){
                                    if (length($link_dirs[$i]) <= length($entry) && $link_dirs[$i] eq substr($entry,0,length($link_dirs[$i]))){
                                        $entry = undef;
                                        $i = $#link_dirs +1;
                                    }
                                }
                                if(defined $entry){
                                    push(@link_dirs, $entry);
                                }
                            }
                        }
                    }
                }
                if (defined $entry){
                    if (-f $entry && (!defined $regex || $entry =~ /$regex/)){
                        push(@files, abs_path($entry));
                    }
                    elsif (-d $entry){
                        push(@depth, abs_path($entry));
                    }
                }
            }
        }
        closedir $dh;
    }
    if ($ignore_symlinks == 0){
        @files = sort @files;
        my @indices = (0,);
        for (my $i = 1;$i <= $#files; $i++){
            if ($files[$i] ne $files[$i-1]){
                push(@indices, $i);
            }
        }
        @files = @files[@indices];
    }
    return @files;
}
#Testing
my $t0 = time();
my @files = rlist_files_nohash("/home/user/", 0, qr/\.pdf$/);
my $tf = time() - $t0;
for my file(@files){
    print($file."\n");
}
print ("Total files found: ".scalar @files."\n");
print ("Execution time: $tf\n");
person IMA    schedule 17.04.2021
comment
Не забудьте проверить наличие символических ссылок, иначе вы можете снова выполнить поиск в том же каталоге. Поверьте, я знаю :) - person brian d foy; 18.04.2021
comment
Спасибо! Вы абсолютно правы, этот код вообще игнорирует символические ссылки. Попытка следовать символическим ссылкам, рекурсивно избегая циклов, была хорошим опытом обучения. Я соответствующим образом отредактирую ответ. - person IMA; 19.04.2021