†Sibylのお部屋†

【Sibyl】ファイル/ディレクトリ一覧の取得

作成開始日 2021.12.26
最終更新日 2023.02.06

基本的な流れ

Sibyl(Pascal)でファイル/ディレクトリ一覧を取得するには、FindFirst/FindNextを使用する。基本的には、FindFirstで取得される最初の項目と、FindNextで取得される2番目以降の項目は、別々に扱わなければならない。で、まあ、それじゃあ処理が重複してみっともないので、たいていは以下のようなループで一元化するわけだが…

rc:=FindFirst('c:\mydir\*.*', faAnyFile, myrec);	//myrecはTSearchRec型
While rc=0 Do
Begin
  (〜取得したファイル情報の処理〜)
  rc:=FindNext(myrec);
End;
FindClose(myrec);
ここで、rcはLongInt型の戻値で、成功ならば0、失敗ならば負の値が返されるそうだ(実際は正の値の事もあるような気がするが…)。が、このループは、な〜んかしっくりこない。初回のループ判断および最初のファイル情報の処理が、FindFirstの結果を使う点にかなり異和感を感じる。ループの外の出来事をループ内に持ち込む感じがイヤ。

それに、この構造では、ループ内でContinue命令を使うと無限ループに陥る。しかし、一定の条件(ファイル名とか作成日時とか)で、処理をスキップするような場合は頻繁にあるので、Continueが封じ手になるのはけっこうキツイ。

repeat〜untilループを使えばもうちょっと奇麗になるが、エントリがない場合にエラーになる?該当エントリがなくても、一度は処理ルーチンを通ることになるので…該当エントリがない場合は事前にハネるという方法もあるが、但し書きの多い読み難いソースになるなぁ…

んじゃ、どんな形がスッキリするのかと言うと…

FindFirst('c:\mydir\*.*', faAnyFile, myrec);
While FindNext(myrec)=0 do
Begin
  (〜取得したファイル情報の処理〜)
End;
FindClose(myrec);
これだと、もの凄〜くスッキリした感じになるのだが、最初のエントリを読み飛ばすことになる。しかし、HPFSの最初のエントリは「.」で、そもそも使い途がない。なので、読み飛ばしはむしろ好都合。FATやLANドライブもサブディレクトリであれば同様である。ところが、FAT/LANドライブのルートだけは例外で「.」も「..」も存在しない。したがって、実在する最初のエントリが読み飛ばされてしまう。これでは使い物にならない(u_u;)

【追記】HPFSでも、システムドライブ(C:)のルートには「.」「..」が存在しなかった。データドライブには存在しているのだが…

いっそ、TSearchRec型のリスト(または動的配列)を作成しておいて、そこに全ての取得データを一時保管してから、処理に掛かる方が良いかも知れない。これをユーザー関数にするのが一番奇麗な形かな?「FileList:=GetFileList(path, attr)」みたいに。

FindFirst('c:\mydir\*.*', faAnyFile, myrec);
If myrec.name<>'.' then (リストに追加);
While FindNext(myrec)=0 do (リストに追加);
FindClose(myrec);
(〜取得したファイル情報の処理〜)
リスト(TList型)の扱いはけっこう面倒臭いので、具体的なコードは後述(予定)。

TSearchRec

まずは、検索結果を入れる構造体TSearchRec;これには検索したファイルの名前、サイズ、日付、属性などが入る。検索結果は「st:=myrec.Name;」「x:=myrec.Size;」などのように参照する。

プロパティデータ型備 考
SearchRec.NameStringファイル名/ディレクトリ名
SearchRec.AttrByteファイル属性(faReadOnly、faHidden、faDirectory等)
SearchRec.TimeLongInt日時情報(内部形式)FileDateToDateTimeで標準形式に変換
SearchRec.SizeLongIntファイルサイズ
SearchRec.HDirLongWord恐らくファイルハンドルだと思うが詳細不明

属性や日時に関しては補足が必要だろうが、ここでは割愛。要するに、さまざまなデータが一度に取得できる、と言うこと。

FindFirst

TSearchRecの機能を踏まえた上で、改めて以下のような命令を考えてみる。
FindFirst('c:\mydir\myimage.jpg', faAnyFile, myrec);
これは何かと言うと、実は「myimage.jpg」というファイルの各種属性を取得する命令である。ある特定のファイルのサイズや作成日時を知りたいときは、このコマンド1つで取得可能なのである。これはけっこう便利だ−−と言うよりも、そもそも、「FindFirst」はそうした用途のための命令なのではないだろうか?つまり、そもそもはループで一覧を取得するような使い方を想定してなかったのではないか?

プロシージャ名も「FindFirst」ではなく、単なる「Find」あるいは「FindFile」などの方が相応しい気がする。

なお、2番目の引数「faAnyFile」は全てのファイル(隠しファイルなども含む)を検索対象とする、と言う意味である。ここに「0」を指定すれば通常のファイルのみが検索対象になる。

ファイル/ディレクトリ一覧の取得

さて、ではファイル/ディレクトリ一覧を取得するときはどうするかと言うと;
// myrecをTSearchRec型、fnameを文字列型配列、nをInteger型とする

n:=1;
FindFirst('c:\mydir\*.*', faDirectory, myrec);
fname[n]:=myrec.Name;		// 例としてディレクトリ名/ファイル名を取得

While FindNext(myrec)=0 do
Begin
  inc(n);
  fname[n]:=myrec.Name;		// 2番目以降のディレクトリ名/ファイル名
  .....
End;
この処理には突っ込み所が満載だ。まず、「faDirectory」という属性指定。これは、検索対象に「ディレクトリを含める」と言う意味だ。「ディレクトリのみ」ではないのがミソ(ディレクトリのみは「faMustDirectory」)。

属 性意 味
0 通常ファイルのみ(何で定数定義されていないんだ?)
faAnyFile すべてのファイルを含む
faArchive faMustArchive アーカイブファイルを含む/アーカイブファイルのみ
faDirectory faMustDirectory ディレクトリを含む/ディレクトリのみ
faHidden faMustHidden 隠しファイルを含む/隠しファイルのみ
faReadOnly faMustReadOnly 読取専用ファイルを含む/読取専用ファイルのみ
faSysFile faMustSysFile システムファイルを含む/システムファイルのみ
※これらの属性を全て試したわけではない。faMustDirectoryなんかバグっぽいかも知れない…

つまり、「faDirectory」を指定すると検索対象は「通常ファイル+ディレクトリ」となる。字面からはディレクトリのみのように見えてしまうのが困ったところ。それに、ファイルとディレクトリの両方を検索対象にするなら「faAnyFile」でもいいんじゃね?と言いたいところだが、これだと隠しファイルやシステムファイルも取得してしまう。

次に考えるべきは、ディレクトリ一覧の最初の要素である。未だにFindFirst/Nextで取得されるエントリの順序がどうなっているのか良く判らないのだが(ディレクトリ名/ファイル名混在の名前順?)、確実なのは最初のエントリが「.」(カレントディレクトリ)で、次が「..」(親ディレクトリ)であること。検索対象にディレクトリが含まれていれば、これは動かないようだ。

つまり「FindFirst('*.*', faDirectory, myrec);」で取得されるのは必ず「.」なのである。これって要らないよね?「..」の方はまだ使い途があるが、「.」はまったく不要と言ってよい。すなわち、「FindFirst」は不要なエントリを読み飛ばしてくれるわけだ。変態仕様だと思ったが、そう考えるとなかなか便利である。

無論、便利なことばかりではない。使用頻度の高い「FindFirst('*.txt', 0, myrec);」のような使い方では、最初のファイルと2番目以降のファイルは、別々に処理しなければならない。処理としてカッコ悪いのである。FindFirst/FindNextを使って、REXXの「SysFileTree」関数互換のユーザー関数を自作するのも手かも知れない。

最後に、「FindNext」のループの問題を指摘しておくと、エントリの取得に成功すると戻り値が「0」という仕様は混乱を招きやすい。DOS系コマンドの共通ルールかも知れんが、ソースの可読性を考えると、やっぱり適当な自作関数を被せる方が良いと思う。

ディレクトリとファイルを分けてリスト表示

faDirectoryを指定して検索した場合、ディレクトリ名/ファイル名が混在して取得されるが、これはちょっと不便だ。ディレクトリ名とファイル名を別々にまとめるには、属性でマスクを掛けて別々に取得して、後で結合すると言う手法がある。が、これはループを2度回すことになって、ちょっとカッコ悪い。

しかし、TStringsクラスの「Add」メソッドと「Insert」メソッドを使い分ければ、もう少しスマートにまとめることができる。たとえば、ListBox1にディレクトリとファイルを別けて登録するには、次のようにする。この場合、0〜n-1がディレクトリ、n〜がファイルとなる。

n:=0;	//ディレクトリを追加する位置のインジケータ
FindFirst('e:\downlad\*.*', faDirectory, myrec); //1項目の[.]は読み飛ばし
While FindNext(myrec)=0 Do
Begin
  if myrec.attr=faDirectory
  then Begin
    ListBox1.Items.Insert(n,'['+myrec.Name+']'); //ディレクトリ名は[]で囲む
    Inc(n);
  End
  else ListBox1.Items.Add(myrec.Name)
End;

ファイル/ディレクトリ一覧の読み込みプロシージャ (2023.01.29)

で、まあ、ファイル/ディレクトリ一覧の読み込みプロシージャ「GetFileList」を作成してみた。プロシージャじゃなくて関数の方が良いかもと思ったが、関数内でTList型を生成すると、何かメモリがおかしくなるので、プロシージャとした。
Procedure TForm1.GetFileList(path:string; attr:byte; List:TList);
Var
  rec : TSearchRec;
  prec:^TSearchRec;

Begin
  FindFirst(path, attr, rec);
  If rec.name<>'.' then
  Begin
    new(prec); prec^:=rec; List.Add(prec);
  End;

  While FindNext(rec)=0 do
  Begin
    new(prec); prec^:=rec; List.Add(prec);
  End;
  FindClose(rec);
End;
で、これをどうやって使うのかと言うと、第1引数にパスを、第2引数に属性を指定すれば 、FListファイルに一覧が取得できる。以下の例では、その後でファイル/ディレクトリごとにまとめている。ソート等はその後の処理になる。
  ///// ファイル一覧の読み込み
  FList.Clear;  //FListはTList型で事前に生成されているものとする
  GetFileList('c:/mydir/*.*', faDirectory, FList); //FListにファイル一覧を取得

  //ここから後は追加の処理例
  //ファイル/ディレクトリの分離、ディレクトリ数のカウント、ファイルサイズの合計
  filesize:=0;
  m:=0;
  For n:=0 to FList.Count-1 do
  If TSearchrec(Flist[n]).Attr=faDirectory then
  Begin
    Flist.Exchange(n,m);
    inc(m);
  End
  Else filesize:=filesize+TSearchRec(FList[n]).size;    //ファイルサイズの合計

  iDir:=m-1;    //ディレクトリの末尾のインデックス
  iFil:=m;      //ファイルの先頭のインデックス

マスクを設定する場合 (2023.01.29)

ワイルドカードでマスクを掛けてファイルを取得する場合は…どうやるんだろう?普通に考えれば、FindFirstのパス指定にワイルドカード指定をすれば良さそうなものだが、それでは、ディレクトリにもマスクが掛かってしまう。基本的に、ディレクトリはマスク指定とは無関係に全て取得できないと不便だ。

なので、取得時にマスクを掛けるのではなく、いったん全てのファイル/ディレクトリを取得した後に、マスクで振り分ける方法を取るしかないと思う。あるいは「全ディレクトリ取得」と「マスク付きファイル取得」の2度ループを回すか?−−faMustDirectoryが正常ならば、だが。SPCCのファイル関連のダイアログあたりを使えば簡単な方法を見つけられるかも知れないが…

とりあえず、コマンドラインではEditFileName関数が使用できそうだ。EditFileName関数はもともとファイルの拡張子の付け替えをするためのものらしい;

st:=EditFileName('myfile.txt','*.bak')  //'myfile.bak'が得られる
この機能を利用すれば、マスクを掛けることが可能になる。もし、元ファイル名と、拡張子を付け替えたファイル名が同じならば、そのファイルの拡張子は、マスクで指定した拡張子と同じだと判断できる。ちょっとややこしいが、こう言うこと;
fname:='mypic.jpg'
If fname=EditFileName(fname','*.jpg') then ... //真になる(mypic.jpg =  mypic.jpg)
If fname=EditFileName(fname','*.png') then ... //偽になる(mypic.jpg <> mypic.png)
ちなみに、EditFileName関数のマスクは拡張子だけでなく、「s*.*」とか「a*.txt」のようにファイル名の本体部分に対しても指定が可能。したがって、ファイル名一般のマスクとして使用できる。これを使って、ファイル一覧を取得する関数を書き直すと…(流石に今回は処理の重複がカッコ悪いので、While rc=0ループで一元化した)。
//-----------------------------------------------------------------------------
  Procedure TForm1.GetFileList(path,mask:string; attr:byte; List:TList);
//-----------------------------------------------------------------------------
Var
  rec  : TSearchRec;
  prec :^TSearchRec;
  rc   : LongInt;
  fname: String;

Begin
  rc:=FindFirst(path, attr, rec);
  While rc=0 Do
  Begin
    fname:=rec.name;                            //ファイル名
    If fname='.' then FindNext(rec);            //[.] 除外
    If pos('.',fname)=0 then fname:=fname+'.';  //拡張子なしファイル対策

    If (fname=EditFileName(fname,mask))         //maskに該当するか、
     | (rec.attr=faDirectory) Then              //ディレクトリの場合は追加
    Begin
      new(prec); prec^:=rec; List.Add(prec);
    End;
    rc:=FindNext(rec);
  End;
End


【Sibylのお部屋目次】 【ホーム】