ファイルの検索2
■この章で解説するモジュール
File::Find
■この章でできること
・複雑な検索条件でファイルを検索する
・File::Find用のソースを自動生成する

 ファイルの検索1ではFile::Findモジュールの基本的な使い方について解説しました。ですが、File::Findモジュールはさらに便利な使い方をすることができます。

 File::Findモジュールは、完全ではないですが、UNIXのfindコマンドに限りなく近いことができます。つまり、いろいろ複雑な検索条件を指定してファイルやディレクトリを検索することができるのです。これを理解するためには、まず前提としてUNIXのfindコマンドを理解している必要があります。

 ここで、簡単にfindコマンドの説明をしておきましょう。例として次のようなコマンドを考えてみます。

find . -name temp1 -and -mtime -3 -and -prune -or -print

 上のコマンドは、説明上、省略可能なところも全部記述しています。まず、最初の「.」は検索を開始する起点です。「.」はカレントディレクトリです。それ以降はオプションの検索条件です。

 オプションは左から右に順番に評価されていきます。「-and」は左の評価か偽だった場合は右の評価はしません。「-or」は左の評価が真だった場合は右の評価はしません。つまり、一つのオプションは評価結果として真か偽のどちらかを返します。それをandやorでつなげて、左から右へ順に評価されていくのです。ということは、当然オプションを書く順序によっても結果が変わります。

 次に各オプションですが、「-name」は引数を一つとり、名前が引数と一致すると真を返します。「-mtime」は引数を一つとり、検索対象のファイルなどの最終更新日時と引数を比較して真か偽を返します。「-3」と記述すると、最終更新日時が3x24時間前より後であれば真になります。つまり、最近更新されたファイルなのかどうかを見ているのです。

 「-prune」は、現在の検索対象を起点としたサブディレクトリや、ディレクトリ以下のファイルを再帰的に検索しないという効果があります。prune自体の評価結果は必ず真になります。「-print」は現在の検索対象を標準出力に出力します。これも必ず真になります。

 このコマンドの実行結果は次のようになります。

[g@630m temp]$ tree
.
|-- file10_01.pl
|-- temp1
|   |-- file1.txt
|   `-- file2.txt
`-- temp2
    |-- file3.txt
    `-- subdir1
        `-- file4.txt

3 directories, 5 files
[g@630m temp]$ find . -name temp1 -and -mtime -3 -and -prune -or -print
.
./file10_01.pl
./temp2
./temp2/file3.txt
./temp2/subdir1
./temp2/subdir1/file4.txt
[g@630m temp]$ 

 直前のtreeコマンドの出力と比較してみましょう。temp1ディレクトリとその中身が除外されているのがわかります。中身まで含めて除外されているのは、pruneの効果によるものです。

 ここまででfindコマンドの説明は終わりです。ここでfindコマンドを説明した理由は、実はfindコマンドをそのままFile::Findに置き換えることができるからです。そのためのfind2perlというスクリプトがPerlには付属しています。コマンドラインから以下のように実行します。

find2perl . -name temp1 -a -mtime -3 -a -prune -o -print

 書式はfindコマンドとほぼ同じですが、「-and」や「-or」は省略形の「-a」「-o」を使う必要があります。また、「-maxdepth」など、一部対応していないオプションもあります。

 このコマンドの実行結果は次のようになります。

[g@630m temp]$ find2perl . -name temp1 -a -mtime -3 -a -prune -o -print
#! /usr/bin/perl -w
    eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'
        if 0; #$running_under_some_shell

use strict;
use File::Find ();

# Set the variable $File::Find::dont_use_nlink if you're using AFS,
# since AFS cheats.

# for the convenience of &wanted calls, including -eval statements:
use vars qw/*name *dir *prune/;
*name   = *File::Find::name;
*dir    = *File::Find::dir;
*prune  = *File::Find::prune;

sub wanted;



# Traverse desired filesystems
File::Find::find({wanted => \&wanted}, '.');
exit;


sub wanted {
    my ($dev,$ino,$mode,$nlink,$uid,$gid);

    /^temp1\z/s &&
    (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
    (int(-M _) < 3) &&
    ($File::Find::prune = 1)
    ||
    print("$name\n");
}

[g@630m temp]$ 

 指定したfindコマンドを、File::Findに置き換えたソースコードを自動生成してくれます。最終更新時刻を調べるために、lstat関数を使っているのがわかります。また、$File::Find::pruneのセットもしています。これを元に、次のようなソースを作ってみます。

#!/usr/bin/perl

#############################################
# ファイルの検索のサンプル2
# Author: "Perl Programming Tips"
#############################################

use File::Find;

# ファイルを検索する
print "find test  **********************\n";
find(\&want_func, ".");

# ファイル検索用サブルーチン
sub want_func
{
    my ($dev,$ino,$mode,$nlink,$uid,$gid);

    /^temp1\z/s &&
    (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
    (int(-M _) < 3) &&
    ($File::Find::prune = 1)
    ||
    print("$File::Find::name\n");
}

 実行結果は次のようになります。

[g@630m temp]$ tree
.
|-- file10_01.pl
|-- temp1
|   |-- file1.txt
|   `-- file2.txt
`-- temp2
    |-- file3.txt
    `-- subdir1
        `-- file4.txt

3 directories, 5 files
[g@630m temp]$ perl file10_01.pl 
find test  **********************
.
./file10_01.pl
./temp2
./temp2/file3.txt
./temp2/subdir1
./temp2/subdir1/file4.txt
[g@630m temp]$ 

 元になるfindコマンドと同じ実行結果が得られました。これがFile::Findモジュールの効果的な使い方です。

 ソースを見ると分かりますが、wantedサブルーチンの戻り値が真になる場合が、検索条件にヒットしているものを検索していることになります。ただし、戻り値はFile::Find内では特に使われません。