《高阶Perl》——1.5 目录遍历的应用和变化

1.5 目录遍历的应用和变化

有一个遍历目录树的函数是有用的,可以将其应用于所有情况。例如,如果想要写一个像Unix的ls -R命令一样工作的递归的文件列出命令,将需要遍历目录树。可以期望函数的行为更像Unix的du命令,列出它找到的所有子目录的总大小以及所有文件的总大小。也可以期望函数搜索悬空符号链接,即指向不存在的文件的链接。在Perl新闻组和IRC频道中经常被问的一个问题是,怎样遍历一棵目录树并为每个文件重命名或对每个文件执行一些别的操作。

可以写许多不同的函数完成这些任务,每个有一点点不同。但是每个的核心部分是递归的目录遍历,因此希望把它提取出来,那样就能把它当工具使用了。如果可以剥离遍历部分,就可以把它放到库中,那么任何需要目录遍历的人都可以使用。

上一段落中出现了一个重要的观点变化。从现在起,直到此书剩余的大部分,将持有你此前未曾见过的一个观点:不再对开发一个可以整体使用的完整程序有兴趣。相反,将尝试把代码写成另一个程序员期望在另一个程序中可以复用的有价值的代码。不再写一个程序,而是写一个将被别的程序使用的库或模块。

从此出发的一个方向将是展示怎样为total_size()函数写一个用户界面(user interface),它可以提示用户需要一个目录名,或者从命令行或者图形控件读取一个目录名,然后以某种方式显示结果。将不做这个。加入提示用户输入目录名或读取命令行参数的代码并不困难。在此书的剩余部分,将不关注用户界面;相反,将关注程序员界面(programmer interface)。此书的剩余部分将讨论“用户”,但这不是普通用户。相反,这里的用户是另一个程序员,当他写他自己的程序时想用我们的代码。我们不再要求如何使我们的完整程序对一个最终用户使用起来既简单又方便,而将关注使其他程序员可以简单方便地在他们自己的程序中使用我们的函数与库的方法。

这样做有两个充足的理由。其一是如果我们的函数经过很好的设计,旨在易于复用 ,我们自己就将可以复用它们来节省时间和避免麻烦。与其反复地写类似的代码,不如把一个常见的目录遍历函数嵌入每个需要它的程序中。当在一个程序里改进了目录遍历函数时,在所有别的程序中的它也将自动改进。随着时间积累,我们将开发一套有用的函数与库的套件,这将使我们更多产,我们也将有更多有趣的程序。

但更重要的是,如果我们的函数是为复用而良好设计的,其他程序员将能使用它们,并得到和我们一样的益处。成为对别人有用的人是我们存在的首要理由。

头脑中观念的变化明确以后,就可以继续。已经有一个函数total_size(),包含有用的功能:它递归地遍历一棵目录树。如果能干净利落地把目录遍历部分的代码与总值计算部分的代码分开,那么就可以在许多别的项目中出于许多别的用途复用这部分目录遍历代码。如何分开两部分呢?

如同汉诺塔程序,这里的关键是向我们的函数传递一个额外的参数。这个参数自身是个函数,可以告诉total_size()要做的事情。代码将如下:

### Code Library: dir-walk-simple
sub dir_walk {
  my ($top, $code) = @_;
  my $DIR;

  $code->($top);

  if (-d $top) {
    my $file;
    unless (opendir $DIR, $top) {
      warn "Couldn't open directory $top: $!; skipping.\n";
      return;
    }
    while ($file = readdir $DIR) {
      next if $file eq '.' || $file eq '..';
      dir_walk("$top/$file", $code);
    }
  }
}

这个函数,我把它重命名为dir_walk()以尊重它新有的普遍性,接受两个参数。第一个,$top,和之前一样是期望从它开始查找的文件或目录名。第二个,$code,是新的。它是一个代码引用以告诉dir_walk想要对在文件树中发现的每个文件或目录进行的操作。每次dir_walk()发现一个新的文件或目录,它将以文件名作为参数执行我们的代码。

现在无论何时遇到另一个程序员问我们:“我怎样对目录树中的所有文件进行X操作?”都可以回答:“用这个dir_walk()函数,并给它一个做X操作的函数的引用。”参数$code是一个回调。

例如,要得到一个程序输出当前目录下的所有文件和目录的清单,可以用:

sub print_dir {
  print $_[0], "\n";
}

dir_walk('.', \&print_dir );

它输出一些如下这样的东西:

.
./a
./a/a1
./a/a2
./b
./b/b1
./c
./c/c1
./c/c2
./c/c3
./c/d
./c/d/d1
./c/d/d2

(当前目录包含三个子目录,称为a、b和c,子目录c含有一个子子目录,称为d。)

print_dir如此简单以至于不必浪费时间给它想个名字。如果可以简单地写出不必命名的函数,那将是方便的,可以写:

$weekly_pay = 40 * $hourly_pay;

而不必命名40或把它存放到一个变量中。Perl的确为此提供了语法:

dir_walk('.', sub { print $_[0], "\n" } );

sub { ... }引入一个匿名函数(anonymous function),即没有名字的函数。sub { ... }结构的值是一个代码引用,可以用来调用函数。可以把这个代码引用保存到一个标量中,或像其他引用一样作为一个参数传递给一个函数。单行代码做了和更冗长的print_dir函数所做的同样的事情。

如果期望函数输出文件大小以及文件名,那么只需稍微改动代码引用参数:

dir_walk('.', sub { printf "%6d %s\n", -s $_[0], $_[0] } );

  4096 .
  4096 ./a
   261 ./a/a1
   171 ./a/a2
  4096 ./b
   348 ./b/b1
  4096 ./c
   658 ./c/c1
   479 ./c/c2
   889 ./c/c3
  4096 ./c/d
   568 ./c/d/d1
   889 ./c/d/d2

如果期望函数找出悬空的符号链接,这也同样简单:

dir_walk('.', sub { print $_[0], "\n" if -l $_[0] && ! -e $_[0] });

-l测试当前文件是否是符号链接,-e测试链接指向的文件是否存在。

然而我的承诺有点不足。没有一个简单的方法使新的dir_walk()函数合计它发现的所有文件的大小。每次调用$code只处理一个文件,所以它从来没有机会合计。如果合计足够简单,那么可以通过在回调之外定义的一个变量完成它。

my $TOTAL = 0;
dir_walk('.', sub { $TOTAL += -s $_[0] });
print "Total size is $TOTAL.\n";

这种方法有两个缺点。一个就是回调函数必须在$TOTAL变量的作用域中,任何打算使用$TOTAL的代码也都必须如此。这常常不是个问题,就像在这个例子里,但是如果回调是在某处的库中的一个复杂函数,这也许会引起困难。我们将在2.1节中看到这个问题的一个解决方案。

另一个缺点是它仅当合计极其简单时才工作良好,就像这里的合计。假设不是计算单个总值,而是期望建立一个文件名与大小的散列结构,如下:

{
  'a' => {
           'a1' => '261',
           'a2' => '171'
         },
  'b' => {
           'b1' => '348'
         },
  'c' => {
           'c1' => '658',
           'c2' => '479',
           'c3' => '889',
           'd' => {
                    'd1' => '568',
                    'd2' => '889'
                  }
         }
}

这里的键是文件与目录的名字。对应一个文件名的值是该文件的大小,对应一个目录名的值是一个散列,它的键与值表示该目录的内容。如何使简单的$total合计回调产生一个像这样的复杂结构,这一点也许是不清楚的。

dir_walk函数不够普遍。需要它检查到文件时执行一些计算,如计算它们的总大小,然后把这个计算的结果返回给它的主调者。主调者可能是main程序,或者它可能是dir_walk()的另一个执行体,后者可以用它接收到的计算结果作为它为其主调者执行的计算的一部分。

dir_walk()如何知道怎么执行计算?在total_size()中,加法计算被固定进函数了。希望dir_walk()更普遍实用。

提供两个函数:一个为普通文件,一个为目录。当dir_walk()需要为普通文件计算结果时它将调用普通文件函数,当它需要为目录计算结果时它将调用目录函数,dir_walk()不会知道它自身如何做这些计算,它只知道它应该把实际的计算委派给这两个函数。

这两个函数都将接受一个文件名参数,并计算它关心的值,如以它的参数命名的文件的大小。由于目录是一列文件,目录函数将接受一列值并计算每个成员,它也需要这些值计算整个目录的值。目录函数知道怎么合计这些值并为整个目录产生一个新的值。

有了这个变化,就能进行total_size操作了。普通文件函数将只简单地返回要求它关注的文件的大小。目录函数将接受一个目录名和一列文件的大小,合计它们,并返回结果。通用的框架函数如下:

### Code Library: dir-walk-cb
sub dir_walk {
  my ($top, $filefunc, $dirfunc) = @_;
  my $DIR;

  if (-d $top) {
    my $file;
    unless (opendir $DIR, $top) {
      warn "Couldn't open directory $top: $!; skipping.\n";
      return;
    }
    my @results;
    while ($file = readdir $DIR) {
      next if $file eq '.' || $file eq '..';
       push @results, dir_walk("$top/$file", $filefunc, $dirfunc);
    }
    return $dirfunc->($top, @results);
  } else {
    return $filefunc->($top);
  }
}

为计算当前目录的总大小,将使用:

sub file_size { -s $_[0] }
sub dir_size {
  my $dir = shift;
  my $total = -s $dir;
  my $n;
  for $n (@_) { $total += $n }
  return $total;
}
$total_size = dir_walk('.', \&file_size, \&dir_size);

file_size()函数表明如何根据给定的普通文件名计算其大小,dir_size()函数表明如何根据给定的目录名和它的内容的大小计算目录的大小。

如果期望程序输出所有子目录的大小,就像du命令一样,加入一行代码:

sub file_size { -s $_[0] }

sub dir_size {
  my $dir = shift;
  my $total = -s $dir;
  my $n;
  for $n (@_) { $total += $n }
  printf "%6d %s\n", $total, $dir;
  return $total;
}

$total_size = dir_walk('.', \&file_size, \&dir_size);

这产生的输出如下:

 4528 ./a
 4444 ./b
 5553 ./c/d
11675 ./c
24743 .

为使函数产生之前看到的散列结构,可以提供以下一对回调:

### Code Library: dir-walk-sizehash
sub file {
  my $file = shift;
  [short($file), -s $file];
}

sub short {
  my $path = shift;
  $path =~ s{.*/}{};
  $path;
}

文件回调返回一个数组,其中包含文件名的缩写(不是全路径)和文件大小。与之前一样,总计求和在目录回调中执行:

sub dir {
  my ($dir, @subdirs) = @_;
  my %new_hash;
  for (@subdirs) {
    my ($subdir_name, $subdir_structure) = @$_;
    $new_hash{$subdir_name} = $subdir_structure;
  }
  return [short($dir), \%new_hash];
}

目录回调得到当前目录名,以及一系列对应于子文件与子目录的“名-值”对。它把这些对合并入一个散列,并返回一个新的对,包含当前目录的短名字和为当前目录新构造的散列。

曾写过的更简单的函数依然简单。这里是递归的文件列出器。对文件和目录使用同样的函数:

sub print_filename { print $_[0], "\n" }
dir_walk('.', \&print_filename, \&print_filename);

这里是悬空的符号链接检测器:

sub dangles {
  my $file = shift;
  print "$file\n" if -l $file && ! -e $file;
}
dir_walk('.', \&dangles, sub {});

一个目录不可能是悬空的符号链接,因此目录函数是空函数(null function),它不做任何事情并立即返回。如果愿意,就可以避免这个古怪的东西和与之相关的函数调用开销,如下:

### Code Library: dir-walk-cb-def
sub dir_walk {
  my ($top, $filefunc, $dirfunc) = @_;
  my $DIR;
  if (-d $top) {
    my $file;
    unless (opendir $DIR, $top) {
      warn "Couldn't open directory $top: $!; skipping.\n";
      return;
    }

    my @results;
    while ($file = readdir $DIR) {
      next if $file eq '.' || $file eq '..';
      push @results, dir_walk("$top/$file", $filefunc, $dirfunc);
    }
    return $dirfunc ? $dirfunc->($top, @results) : () ;
  } else {
    return $filefunc ? $filefunc->($top) : () ;
  }
}

这可以写成dir_walk('.', &dangles),而不是dir_walk('.', &dangles, sub {})。
作为最后一个例子,用稍微不同的方式使用dir_walk(),制造一棵文件树中所有普通文件的一个列表,不输出任何东西:

@all_plain_files =
  dir_walk('.', sub { $_[0] }, sub { shift; return @_ });

文件函数返回它遇到的文件的名字。目录函数丢弃了目录名并返回它所含的文件的列表。如果目录根本不含文件呢?那么它返回一个空列表给dir_walk(),这个空列表将合并入其他同级目录的结果列表。

时间: 2024-09-19 08:54:54

《高阶Perl》——1.5 目录遍历的应用和变化的相关文章

《高阶Perl》——导读

前 言 在编程圈子里有一句著名的俗语,一个优秀的Fortran程序员可以用任何语言写Fortran程序.然而,让人悲哀的是,不管他们是否愿意,Fortran程序员用任何语言写Fortran程序.类似地,作为Perl程序员,我们也在用Perl写C程序,不管我们是否愿意.这让人羞愧,因为Perl是一门比C更富有表现力的语言.我们本可以做得更好,以C程序员梦想不到的方式使用Perl,但是我们没有那样做. 怎么会这样呢?Perl的设计初衷是一方面作为C的替代品,另一方面作为UNIX脚本语言(如Bourn

《高阶Perl》——1.4 层次化数据

1.4 层次化数据 已经介绍的例子显示了递归过程的风采,但是它们遗漏了重要的一点.在介绍汉诺塔问题时,我说过当你想解决的问题可以简化成同样问题的更简形式时,递归是很有用的.但是这样的问题是普遍的这一点可能不清楚. 多数递归的函数是为处理递归的数据结构的建立.一个递归的数据结构就像一个列表.一棵树或者一个堆,它们根据相同数据结构的更小的实例定义而成.最常见的例子可能是文件系统目录结构.文件可以是: 文件可以是一个目录,含有一列文件,其中的一些可以是目录,再含有另一些文件,以此类推.处理这样的结构最

《高阶Perl》——第2章 分配表 2.1 配置文件处理

第2章 分 配 表 第1章介绍了如何用别的函数参数化函数的行为使函数更加灵活.例如,并没有把每次移动盘子就输出一条消息硬编码到hanoi()函数里,而是让其调用一个从外部传入的辅助函数.通过提供一个合适的辅助函数,可以使hanoi()输出一系列说明,或检查它自己的行动,或生成一个图形显示,而不必重新编写基本的算法.类似地,可以从total_size()函数的计算文件大小的行为中提取出目录遍历行为,得到一个更有价值和普遍适用的dir_walk()函数,它可以做许多不同的事情. 为了从hanoi()

《高阶Perl》——1.6 函数式编程与面向对象式编程

1.6 函数式编程与面向对象式编程 现在停下来看看已经做的.已经有了一个有用的函数,total_size(),它包含了在其他应用中遍历目录结构的有用代码.所以为使total_size()更具普遍意义,把所有与计算大小相关的部分提出来,替换成用户指定的任意函数的调用.结果就是dir_walk().现在,对于任何需要遍历目录结构并做某事的程序,dir_walk()处理遍历部分,参数函数处理"做某事"部分.通过传递合适的函数对给dir_walk(),可以使它做任何想要它做的事情.已经获得了灵

《高阶Perl》——1.7 HTML

1.7 HTML 我曾承诺递归对操作层次化定义的数据结构有用,我还用了文件系统作为一个例子.但它是个稍微特殊的数据结构的例子,因为通常认为数据结构是在内存里,而不是在磁盘上. 文件系统的树型结构让人联想到目录,每个目录都含有一列其他文件.任何拥有一些包含其他条目列表的领域都将含有树型结构.一个极好的例子是HTML数据. HTML数据是一系列元素和普通文本.每个元素含有一些内容,它们又是一系列更多的元素和更多的普通文本.这是个递归的描述,类似文件系统的描述,HTML文件的结构也类似于文件系统的结构

《高阶Perl》——3.2 内联缓存

3.2 内联缓存 给一个函数添加缓存的最直接的方式就是给函数一个私有的散列.在这个例子里,可以使用一个数组代替散列,因为fib()的参数总是一个非负整数.但是一般需要使用一个散列,那么将会看到: ### Code Library: fib-cached # Compute the number of pairs of rabbits alive in month n { my %cache; sub fib { my ($month) = @_; unless (exists $cache{$m

《高阶Perl》——3.5 MEMOIZE模块

3.5 MEMOIZE模块 本书不是关于Perl模块的内部细节的,而是关于一些Memoize模块内部使用的并和稍后要做的事情有直接联系的技术,所以现在简短地介绍一下. Memoize得到一个函数名(或引用)作为它的参数.它制造一个新的函数,后者维护一个缓存并在其中查找它的参数.如果新的函数在缓存里找到了参数,就返回缓存的值:如果没找到,就调用原始函数,把返回的值保存入缓存,并把它返回给原始的主调者. 制造完这个新的函数,Memoize就把它装入Perl的符号表以代替原始的函数,那样当你认为你在调

《高阶Perl》——3.10 可供选择的记忆术

3.10 可供选择的记忆术 大多数纯函数提供一个缓存的机会.尽管乍一看纯函数很少,它们只以一定频率出现.纯函数特别普遍的地方是在排序中用做比较器函数. Perl内置的sort操作符是通用的,它可以把一列任何种类的数据以程序要求的任何次序排序.默认状态下,它把一列字符串以字母表次序排序,但是程序员可以任意提供一个比较器函数(comparator function),告诉Perl怎样重排sort的参数列表.比较器函数被反复调用,每次带有待排序列表中的两个不同元素,如果两个元素次序正确,就必须返回一个

《高阶Perl》——3.8 对象方法里的缓存

3.8 对象方法里的缓存 对象方法,它经常不理解地把缓存的值保存在独立的散列里.考虑一个投资银行写的程序里的Investor对象.该对象表现了银行的一个客户: package Investor; # Compute total amount currently invested sub total { my $self = shift; # ... complex computation performed here ... return $total; } 如果$total不会改变,就可以缓存