ディレクトリの中のファイルを再帰的に取り出してなにかしたいと思ってみたけど Class::Path::Dir の挙動の解釈ができずに困る修行

ディレクトリの中のファイルを再帰的に取り出してなにかしたい。Path::Class::Dir (Path::Class::Dir - Objects representing directories - metacpan.org) の recurese メソッドだと「なにか」を引数に渡さないといけない。どちらかというと next メソッドのような使い方をしたい。で、next メソッドは、ディレクトリの中身をディレクトリも含めて順に出してくれるけれど、再帰的に取り出してはくれない。

ので、Path::Class::Dir の派生クラスとして Unau::Path::Dir を作成し、そのクラスに nextf (next file の意) を作ってみた。でもうまくいかない。実際のプログラムがこれ。

use strict;
use warnings;

package Unau::Path::Dir;
use base qw/Path::Class::Dir/;

sub new {
    my $class = shift;
    my $instance = $class->SUPER::new(@_);
    $instance->{_r_dirs}  = [ $instance ];
    $instance->{_r_files} = [];
    print "[constructed an instance of $class]\n";
    bless $instance, $class;
}

sub nextf {
    my ($self) = @_;
    my $file = pop @{$self->{_r_files}};
    return $file if $file;
    my $dir = pop @{$self->{_r_dirs}};
    return undef if ! $dir;
    foreach my $child ($dir->children) {
        my $class = ref $child;
        if ($class eq 'Path::Class::File') {
            push @{$self->{_r_files}}, $child;
        }
        else {
            push @{$self->{_r_dirs}}, $child;
        }
    }
    return $self->nextf;
}

package main;

my $dir = Unau::Path::Dir->new('r', 'in');
print "base dir = " . $dir->stringify . "\n";
my $n = 0;
while (my $file = $dir->nextf) {
    $n++;
    print "file $n : " . $file->stringify . "\n";
}

__END__

ディレクトリ構成は次のようになっている。

[takeyuki@sunya ~]$ tree r/in
r/in
|-- 0-1.txt
|-- 1st
|   |-- 1-1.txt
|   `-- 1-2.txt
`-- 2nd
    |-- 2-1.txt
    `-- 2-2.txt

2 directories, 5 files

ので、五つのファイルに対して print されることを期待している。ところが、実行すると次のようになる。

[takeyuki@sunya ~]$ perl g2.pl
[constructed an instance of Unau::Path::Dir]
base dir = r/in
[constructed an instance of r/in]
[constructed an instance of r/in]
file 1 : r/in/0-1.txt
Can't locate object method "children" via package "r/in" at g2.pl line 22.

nextf メソッドの中の $dir->children のところで、Unau::Path::Dir::new が呼び出されている。そして、そのときのクラス名が "r/in" になってしまっている模様。"r/in" で bless されてしまっているので、後々「"r/in" に "children" メソッドはないよ」と怒られている。

おそらく Class::Path::Dir が派生クラスを作らないことを想定して作られているのだろう。が、わからない点が二つ。

  • Class::Path::Dir::children (一応 Unau::Path::Dir::children だが) でディレクトリエントリをオブジェクト化するときにどうして Class::Path::Dir::new ではなくて Unau::Path::Dir::new が呼ばれるのか。
  • Unau::Path::Dir::new に渡されている "r/in" はどこから来たのか。

追記

Path::Class::Dir.pm を読んでみてわかったことがある。やっぱり Class::Path::Dir は派生クラスを作らないことが前提になっている。移譲しよ、以上。

追記 2009-11-21

メソッドをたくさん書くのが面倒なので移譲はやめた。クラスの名前も Unau::RecursiveIterativeDir にしてみた。

use strict;
use warnings;

package Unau::RecursiveIterativeDir;
use Path::Class::Dir;

sub new {
    my $class = shift;
    my $dir = Path::Class::Dir->new(@_);
    my $instance = {
                    _dir  => $dir,
                    _r_dirs => [ $dir ],
                    _r_files => [],
                };
    bless $instance, $class;
}

sub dir {
    $_[0]->{_dir};
}

sub nextf {
    my ($self) = @_;
    my $file = pop @{$self->{_r_files}};
    return $file if $file;
    my $dir = pop @{$self->{_r_dirs}};
    return undef if ! $dir;
    foreach my $child ($dir->children) {
        my $class = ref $child;
        if ($class eq 'Path::Class::File') {
            push @{$self->{_r_files}}, $child;
        }
        else {
            push @{$self->{_r_dirs}}, $child;
        }
    }
    return $self->nextf;
}

# sub stringify {
#     my $self = shift;
#     $self->{_dir}->stringify(@_);
# }

package main;

my $dir = Unau::RecursiveIterativeDir->new('r', 'in');
# print "base dir = " . $dir->stringify . "\n";
print "base dir = " . $dir->dir->stringify . "\n";
my $n = 0;
while (my $file = $dir->nextf) {
    $n++;
    print "file $n : " . $file->stringify . "\n";
}

__END__

実行結果は次のとおり。

[takeyuki@sunya ~]$ perl g2.pl
base dir = r/in
file 1 : r/in/0-1.txt
file 2 : r/in/2nd/2-2.txt
file 3 : r/in/2nd/2-1.txt
file 4 : r/in/1st/1-2.txt
file 5 : r/in/1st/1-1.txt