• ベストアンサー

想定通りに動かずループしてしまうのは何故?

カレントディレクトリ以下に存在するすべてのファイルを列挙するスクリプトを作りました。 以下がソースです。(ちなみにこれを考えるのに一晩かかりました…恥ずかしいです) 再帰的にディレクトリを探査して、ファイルが見つかれば@dataに入れます。 また、ディレクトリが見つかれば@listに入れて、このリストが空でない限り 再帰的に処理します(するはずなんですが…)。 最後に@dataを出力して終了です。 #!/usr/bin/perl -w use strict; my (@list,@data); main(); exit(0); sub main {  push @list,'.';  rdir(); } sub rdir(){  if ($#list >= 0) {   my @dirlist;   my $d = shift @list;   opendir DIR,$d or die "$!\n";    @dirlist = readdir DIR;   closedir DIR;    shift @dirlist;    shift @dirlist;    foreach (@dirlist) {     my $dd = $_;     $dd = "$d/$dd";     if ( -d $dd ) {      push @list, $dd;     }     elsif ( -f $dd ) {      push @data,"$dd\n";     }    }   rdir();  }  #else {   print @data;  #} } これを実行するとrdir()の部分が延々とループされてしまいます (無限ではなくほっておくと終わります)。 期待している動作なら、すべてのディレクトリを一回再帰的に探査したら それでmain();がおわり、次のexit(0);で終わるはずなのですが、そうはなりません。 最後のコメントアウトしたelseを有効にすると期待通りの動作をします。 どうしてこうなるのかわかりません。 このelseは別になくてもいいのではないかと思うのですが。

質問者が選んだベストアンサー

  • ベストアンサー
  • bender
  • ベストアンサー率45% (108/236)
回答No.3

掲載されているプログラムはおおよそ以下のようになっています(最後の return; が無くても、関数はおよそそのように振舞うのですが、わかりやすいと思い加えました)。 sub rdir {  if(...) {    処理1;  }  処理2;  return; } これは、処理1の中に再帰呼び出しがあろうがなかろうが、この関数が呼び出されるたびに、(処理1の最後に return が無い限りは)必ず処理2が実行されることがわかります。 処理1に再帰呼び出しがあって「次のrdirの中に処理が移る」としても、その呼び出された関数の処理が終わり次第、もとの呼び出し箇所の続きから処理が始まります。 『仮に』プログラムというものがそういう風に(呼び出された関数での処理が終わり次第、もとの呼び出し箇所から処理を続行するように)機能『しない』のであれば、例えば質問文に書かれたプログラムで、関数 main の後に何かの処理(この場合 exit(0);)を書くことは意味がないことになります、関数呼び出し後は処理がもどって来ないのですから。

tochanx
質問者

お礼

なるほど! 関数の中から関数を呼べば最初の関数に残りの処理があっても無視されるものだと 勘違いしておりました。 ていねいに解説していただきありがとうございました。

その他の回答 (4)

  • guci-ok
  • ベストアンサー率33% (49/146)
回答No.5

Perlの世界では、車輪を2度発明するな、とよく言われます。 バグを作らない最も優れた方法はコードを書かないことです。 C:\Perl\lib\File>perl -MFile::Find -le "find sub { print $File::Find::name if -f },'.'" ./Basename.pm ./CheckTree.pm ./Compare.pm ./Copy.pm ./DosGlob.pm ./Find.pm ./Glob.pm ./Path.pm ./Spec.pm ./stat.pm ./Temp.pm ./Spec/Cygwin.pm ./Spec/Epoc.pm ./Spec/Functions.pm ./Spec/Mac.pm ./Spec/OS2.pm ./Spec/Unix.pm ./Spec/VMS.pm ./Spec/Win32.pm

tochanx
質問者

お礼

回答ありがとうございます。 車輪の再発明をするなといわれてしまえばちょっと縮こまってしまいますが、 確かに既存のモジュールでできることをわざわざ自分で一から書き起こすのは、 ましてや作成者のスキルが低いとまずいでしょうね。 ですが、どこかで公開するようなものでもないので、やはり自分でいろいろ うなりながら作った方が楽しいです。

回答No.4

>なんどスクリプトを眺めてもprint @dataが実行されるのはrdirの再起的な 探索が終わった後にくるとしか思えないのです 既に#2さんの解答もでていますが、一応。 if($#list>=0){...}の{...}部分は@listの要素数が0でない限り実行されて、@dataにファイルを溜めます。 @dataの中味を表示するのは、もう調べるリストが無くなったときでいいはずなので、$#list >= 0がみたされない場合に実行する。だからelseに書く。もしif($#list>=0){...}の{...}にprint @dataを書いて仕舞うと、@listに要素がある、つまり、調べるべきディレクトリが有る状態でrdir()の呼び出すごとに実行されてしまいます。

tochanx
質問者

お礼

どうもありがとうございます。 みなさんのおかげで納得できました。

  • bender
  • ベストアンサー率45% (108/236)
回答No.2

else は必要です。 else が無い場合、関数 rdir が呼ばれると、必ず一回 "print @data" を実行することがわかります。そこで、このプログラムを実行して「すべてのディレクトリを一回再帰的に探査」するとき、この関数が再帰的に呼ばれた回数だけ、この print 文が実行されなくてはならないことになります。 というわけで、このプログラムは、一回だけ「最後に@dataを出力」するプログラムではないことがわかります。 ところで、探索するディレクトリの一つが、例えば、親ディレクトリへの symbolic link である場合、このプログラムはとまらなくなってしまうと思います。

tochanx
質問者

補足

回答ありがとうございます。 シンボリックとは思いもよりませんでした。確かにこのままではまずいですね。 指摘して下さりありがとうございます。 シンボリックリンクについてはファイルテスト演算子の-lを利用して判別することにしました。 ただ、やはりどうしてelseが必要なのかよくわかりません。 最後のprint @dataのところに処理がくる前にrdir()を呼んでいるのだから、 printはされずに次のrdirの中に処理が移るはずで、printがされるのは @list(発見されたディレクトリのリスト)が空の状態、 つまり再起的に探索し終わったときになると思っているのですが、しかしそうはならないようです。 どうしてなのでしょうか? すみません。

回答No.1

プログラムをあまり良く見ていませんが、私の環境 RedHatLinux9, linux perl 5.8.0ではきちんと動きましたよ。 elseを入れないと何度も同じものが出力されますね。 elseは必要でしょう。 でも、最後に1回出力するようにした方がより分かりやすいのではと思います。

tochanx
質問者

お礼

わざわざ試していただきどうもありがとうございます。 確かにmainの下あたりにprintを持ってきた方がすっきりしますね。そうすることにします。 ですが、elseがどうして必要なのかさっぱりわからないのです。 なんどスクリプトを眺めてもprint @dataが実行されるのはrdirの再起的な 探索が終わった後にくるとしか思えないのです。 ひょっとしてひょっとしてperlエンジンに解釈される時点で何か私の預かり知らない最適化 でもなされているのでしょうか?