opendir 関数でファイルの列挙

Perl で実用的なスクリプトを書くときに、「あるフォルダの中に存在するファイルを全て列挙したい」という場合は多いと思います。フォルダの中にあるファイルを全て処理したい場合などがそうですね。
Perl では、こういった事をやりたい場合、実現する方法が2通りあります。1つは「opendir、readdir、closedir」などの関数を使って「ディレクトリハンドル」を操作する方法で、もう1つは「ファイル名グロブ」と呼ばれる「glob」関数(または山カッコ演算子)を使う方法です。
このエントリでは、前者の opendir 関数を使った方法を勉強したいと思います。


ファイルを操作する時の基本的な手順は open、<>(行入力演算子)、close でしたね。開いたファイルに関連付けられた「ファイルハンドル」を通じて、色々な操作を行ないました。
opendir を使ったファイル列挙も、基本的には同じような手順となります。

  1. opendir 関数でディレクトリ(フォルダ)を開き「ディレクトリハンドル」を関連付ける
  2. readdir 関数でディレクトリハンドルから 1つ または 全て のファイル名を読み込む
  3. closedir 関数でディレクトリハンドルを閉じる

open、read、close という手順は、ファイルを読み込む時とほとんど変わっていませんね。(^_^)

opendir 関数

以下、「perldoc -f opendir」からの引用です。

opendir DIRHANDLE,EXPR
        Opens a directory named EXPR for processing by "readdir",
        "telldir", "seekdir", "rewinddir", and "closedir". Returns true
        if successful.

EXPR に指定されたディレクトリを開いて DIRHANDLE に関連付けます。成功すれば真を返します。readdir、telldir、seekdir、rewinddir、closedir が DIRHANDLE に対して利用できる関数ですね。
open 関数の時と同じように、「or die」でエラー処理を行なうのが通例のようです。

opendir CURDIR, "." or die "Can't open '.': $!\n";

開くディレクトリ(EXPR)として "." を渡しています。これは、多くのファイルシステムでのお約束で、「カレントディレクトリ」を表しています。例えば、Windows の場合 Perl スクリプトを実行する際にコマンドプロンプトを利用するかと思いますが

D:\home\palmo>

のように「>」の左に書かれているパスが、カレントディレクトリ(".")となります。こう考えると、カレントディレクトリ=スクリプトファイルがあるフォルダ、と考えてしまいますが、必ずしも一致しない事に注意してください。ショートカットの「作業フォルダ」などで変更できます。
ちなみに ".." は1つ上の階層のディレクトリ(親フォルダ)を表します。試しにカレントディレクトリを変更するコマンド「cd」を使って「cd ..」を実行してみると

D:\home\palmo>cd ..

D:\home>

と、カレントディレクトリが1つ上に移ったのがわかりますね。(^_^)

readdir 関数

readdir 関数をスカラーコンテキストで呼び出した場合、開いたディレクトリハンドルからファイル名を1つ取り出して返します。呼び出す度に内部のイテレータが1つ進むので、毎回違うファイル名が返ってきます。ファイルリストの最後まで到達すると undef を返します。
while を使えば、全てのファイル名を処理できますね。

while (defined(my $fn = readdir CURDIR)) {
  print "$fn\n";
}

defined 関数を使って、undef が返ってきていないか調べています。もし defined 関数を使わずに、代入文を直接条件部に書くとどうなるでしょうか。確かに、一見うまく動くように見えますね。(^_^)
でも、「0」という名前のファイルがあった場合、「偽」と判定されてしまうので、そこで列挙が中断してしまい、困った事になってしまいます。こういった稀なケースでもバグの原因にはなりうるので、注意するに越した事は無いと思います。


また readdir 関数をリストコンテキストで呼び出した場合、ディレクトリハンドルから残り全てのファイル名をリストとして返します。

my @names = readdir CURDIR;
print "@names";

この辺りの動作を見ると、行入力演算子(<>)とほぼ同じですね。というより、なぜ行入力演算子ディレクトリハンドルに対して使えるようにオーバーロードしなかったのかが不思議でなりません。何か理由があるのでしょうか。
あくまでディレクトリハンドルに対しての操作とファイルハンドルに対しての操作の違いを際立たせる為でしょうか。確かに Perl は「==」と「eq」など、演算子オーバーロードはなるべく避けるように設計されていると思える節があります。かと思えば、デリファレンスとシンボリックリファレンスが似ていたり、混同しやすくなっている部分もありますよね。不思議です。

closedir 関数

最後はやはり後始末、ということで closedir 関数でディレクトリハンドルを閉じます。

closedir CURDIR;

うーん、簡単簡単♪

まとめ

カレントディレクトリに存在するファイルの名前を全て print するには、以下のようになります。

use strict;
 
opendir CURDIR, "." or die "Can't open '.': $!\n";
 
while (defined(my $fn = readdir CURDIR)) {
  print "$fn\n";
}
 
closedir CURDIR;

実行結果は以下の通りですが、今までのぱるも日記でスクリプトファイルが結構な数になっているので、中略しています。(^_^;)

.
..
a.pl
aaa.txt
accessor.pl
accessor2.pl
accessor3.pl
accessor4.pl
accessor5.pl
accessor6.pl
add.pl
(中略)
typeglob.pl
undef.pl
undef2.pl
universal.pl
vartest.pl
wraplist.pl

実行結果の最初の2つに注目してください。"." と ".." がファイル名として列挙されていますね。これは、さきほど説明しましたが、カレントディレクトリと親ディレクトリを表しているのでした。これらがファイルリストに含まれるのは恐らく仕様ですので、もし不要だったら取り除いてしまいましょう。

my @names = readdir CURDIR;
@names = grep { $_ ne '.' && $_ ne '..' } @names;
for (@names) { print "$_\n"; }

また、拡張子でフィルタする事もできますね。正規表現によるパターンマッチを使えば簡単です。

use strict;
 
opendir CURDIR, "." or die "Can't open '.': $!\n";
 
my @names = grep /\.txt$/, readdir CURDIR;
for (@names) { print "$_\n"; }
 
closedir CURDIR;

末尾に「.txt」が付くファイル名だけを抽出しています。grep の第2引数はリストなので、readdir はリストコンテキストで評価されます。
実行結果は以下の通りです。

aaa.txt
bbb.txt
ccc.txt
fruits.txt
hello.txt
readme.txt
textmode.txt
tr2.txt

なんだか色々なテキストを書いたようです。(^_^;)