Finding files, part 3
By Mark G. Wiseman
If you’ve read parts 1 and 2 in this series of articles, you know I am building a program that will delete all the unnecessary files created by C++Builder during the lifetime of a project.
I created a class, TFindFile, and some support classes that I was going to use to search for these files on my hard drives. In this article, I had planned to discuss how I was going to use TFindFile to actually find and delete files. However, I made some discoveries along the way that caused me to alter the original design.
Best laid plans…
In my original design, I was going to create another class, TMultiFindFile, which used TFindFile to match multiple file patterns in one pass through a hard drive. The TFindFile class can only match one file pattern per pass, so I had planned to use TFindFile to match all files using the *.* pattern and then pick the correct files to delete with TMultiFindFile. I designed these classes for the greatest flexibility. TFindFile would be very fast but not versatile and TMultiFindFile would be versatile but not very fast. I thought I might need the speed of TFindFile in a future project, so it made sense to have two classes.
However, when I tested TMultiFindFile, I was surprised to discover that it, like TFindFile, was very fast. What I had was a level of complexity that I no longer needed. So, I have rewritten TFindFile to include the abilities of TMultiFileFind and to avoid confusion, I have named this new hybrid class TFileFinder.
Powerful and fast
If you’ve looked at the code for TFileFind, most of TFileFinder will look familiar. Listings A and B contain the source for TFileFinder. I still use the TFFStack class to avoid using recursion while searching subfolders. And I still use an event to report when a file is found that matches a specified pattern.
So what has changed? Well, TFileFinder will accept a list of patterns to match and a list of patterns to exclude. It will also accept a list of paths to search and a list of paths to skip or not search.
If you compare the public interface of the old TFindFile and the new TFileFinder, you will see only a few changes. I have added five properties and deleted one property. Also, the Find() method no longer takes any arguments.
Paths
Four of the properties I added reflect the power of TFileFinder. These properties are Paths, SkipPaths, Include, and Exclude. Paths and SkipPaths are of type TFFPathList. I derived TFFPathList from TStringList. The source for TFFPathList can be found in Listing C. The sole purpose of TFFPathList is to store a list of path strings with a bool, SearchSubfolders, attached that indicates whether the path’s subfolders should be searched.
Using Paths and SkipPaths is easy. All of my C++Builder projects can be found in subfolders of two folders on my C drive, C:\PROJECTS and C:\ARTICLES. I want to search these folders for files to delete; but I don’t want to search the folder C:\PROJECTS\CURRENT or any of its subfolders. The following code will set up TFileFinder to do this:
Paths->Add("c:\\Projects",
true);
Paths->Add("c:\\Articles", true);
SkipPaths->Add("c:\\Projects\\Current", false);
I call Add() to add the C:\PROJECTS path to Paths and also pass in true to indicate that the subfolders of C:\PROJECTS should also be searched. I add C:\PROJECTS\CURRENT to the list of paths that TFileFinder should skip while searching. In this case, I pass in false to indicate that the subfolders of C:\PROJECTS\CURRENT should not be searched. Had I passed in true, C:\PROJECTS\CURRENT would not be searched, but all of its subfolders would be searched.
I dropped the property SearchSubfolders used in the original TFindFile class, since each path now carries a bool to indicate the search method.
Patterns
The Include and Exclude properties of TFileFinder are of type TStringList and are lists of file patterns to either include or exclude when matching files.
If I want to match files that have the extensions .TDS and .CSM but only if those files don’t start with the letter ’f’, I could use the following code:
Include->Add("*.tds");
Include->Add("*.csm");
Exclude->Add("f*.*");
When the FindFiles() method of TFileFinder does its work, it first checks to see if a file matches any of the patterns in the Include list. If the file does match, FindFiles() checks to see if it also matches any of the patterns in the Exclude list and if it does the file is not reported as a match.
I use the VCL function, MatchesMask(), to compare file names and patterns. This function uses a subset of regular expressions, so you can create some sophisticated patterns to match file names against. Check the VCL online help for MatchesMask() to learn more.
Matching folders
Normally, when searching a hard drive for files, you won’t want TFileFinder to report matches on folder names in addition to file names. But, you might; so I have included the property MatchFolders. If this property, a bool, is true, the OnFileFound event will also report any folder names that match.
Finding files
Finally, the last change I made when creating TFileFinder was to drop the pattern argument from the Find() function originally use in TFileFind. The Paths, SkipPaths, Include and Exclude properties of TFileFinder eliminate the need for an argument to Find().
Conclusion
There is a test program for TFileFinder on the Bridges Publishing Web site at www.bridgespublishing.com. You can use it to see how the Paths, SkipPaths, Include and Exclude properties work.
In part 2, I told you that this article would finally delete some files. Well, it doesn’t. I do think, though, that the very powerful TFileFinder class is worth making you wait until part 4, where I really will delete some files.
Listing A: FileFinder.h
#ifndef FileFinderH
#define FileFinderH
#include "FFData.h"
#include "FFPathList.h"
class TFFStack;
class TFileFinder;
typedef void __fastcall (__closure TFileFound)
(TFileFinder *Sender, String
fileName, String foldername, TFFData data);
typedef void __fastcall (__closure *TSearchFolder)
(TFileFinder *Sender, String
folderName, bool
&skip);
class TFileFinder
{
public:
TFileFinder();
~TFileFinder();
void Find();
void Stop();
bool IsRunning();
__property TStringList
*Include = {read =
include, write = SetInclude};
__property TStringList
*Exclude = {read =
exclude, write = SetExclude};
__property TFFPathList
*Paths = {read =
paths, write = SetPaths};
__property TFFPathList
*SkipPaths = {read =
skipPaths, write = SetSkipPaths};
__property bool
MatchFolders = {read =
matchFolders, write = matchFolders};
__property TFileFound
OnFileFound = {read =
onFileFound, write = onFileFound};
__property
TSearchFolder OnSearchFolder = {read=onSearchFolder,
write=onSearchFolder};
Listing B: FileFinder.cpp
#include
<vcl.h>
#pragma hdrstop
#include <Masks.hpp>
#include "FileFinder.h"
#include "FFStack.h"
TFileFinder::TFileFinder()
{
onFileFound = 0;
onSearchFolder = 0;
include = exclude = 0;
paths = skipPaths = 0;
try {
include = new
TStringList;
exclude = new
TStringList;
paths = new
TFFPathList;
skipPaths = new
TFFPathList;
}
catch(...) {
delete include;
delete exclude;
delete paths;
delete skipPaths;
throw;
}
stop = true;
matchFolders = false;
}
TFileFinder::~TFileFinder()
{
delete include;
delete exclude;
delete paths;
delete skipPaths;
}
bool TFileFinder::SkipDir(const String dir)
{
bool skip = false;
for (int i=0;i<skipPaths->Count
&& skip == false;i++)
{
skip =
dir.AnsiCompareIC(
IncludeTrailingBackslash(skipPaths->Strings[i])) == 0 ||
(dir.Pos(skipPaths->Strings[i])
== 1 &&
skipPaths->SearchSubfolders[i]
== false);
}
if (onSearchFolder)
onSearchFolder(this, dir, skip);
return(skip);
}
void TFileFinder::Find()
{
stop = false;
TFFStack dirStack;
for (int i=0;i<paths->Count
&& stop == false;i++)
{
String curDir =
IncludeTrailingBackslash(ExpandFileName(paths->Strings[i]));
if (onFileFound
&& SkipDir(curDir) == false)
FindFiles(curDir);
if (paths->SearchSubfolders[i])
{
while
(stop == false) {
Application->ProcessMessages();
FindDirs(curDir,
dirStack);
if
(dirStack.IsEmpty())
break;
curDir
= dirStack.Pop();
if
(SkipDir(curDir))
continue;
if
(onFileFound)
FindFiles(curDir);
}
}
}
stop = true;
}
void TFileFinder::FindFiles(String dir)
{
TFFData ffData;
String filePattern = dir +
"*.*";
HANDLE handle =
FindFirstFile(filePattern.c_str(),
&ffData.data);
if (handle != INVALID_HANDLE_VALUE)
{
bool found = true;
while (found == true
&& stop == false) {
Application->ProcessMessages();
if (ffData.IsFolder()
== false || matchFolders == true) {
String
name = ffData.GetName();
bool
match = false;
for
(int i=0;i<include->Count && match == false;i++)
match = MatchesMask(name, include->Strings[i]);
for
(int i=0;i<exclude->Count && match == true;i++)
match = !MatchesMask(name, exclude->Strings[i]);
if
(match == true && name != "." && name != "..")
onFileFound(this, name, dir, ffData);
}
found = (FindNextFile(handle,
&ffData.data) != 0);
}
FindClose(handle);
}
}
void TFileFinder::FindDirs(String baseDir, TFFStack
&stack)
{
TFFData ffData;
String dirPattern = baseDir +
"*.*";
HANDLE handle =
FindFirstFile(dirPattern.c_str(),
&ffData.data);
if (handle != INVALID_HANDLE_VALUE)
{
bool found = true;
while (found == true
&& stop == false) {
Application->ProcessMessages();
if (ffData.IsFolder()
&& ffData.GetName() != "." &&
ffData.GetName() != "..")
stack.Push(baseDir
+ ffData.GetName() + "\\");
found = (FindNextFile(handle,
&ffData.data) != 0);
}
FindClose(handle);
}
}
void __fastcall TFileFinder::SetInclude(TStringList *val)
{
include->Assign(val);
}
void __fastcall TFileFinder::SetExclude(TStringList *val)
{
exclude->Assign(val);
}
void __fastcall TFileFinder::SetPaths(TFFPathList *val)
{
paths->Assign(val);
}
void __fastcall TFileFinder::SetSkipPaths(TFFPathList *val)
{
skipPaths->Assign(val);
}
Listing C: FFPathList.h.
#pragma package(smart_init)
#ifndef FFPathListH
#define FFPathListH
#include <vcl.h>
#pragma hdrstop
class TFFPathList : public TStringList
{
public:
virtual int __fastcall
Add(const String path);
virtual int __fastcall
Add(const
String path, bool searchSubfolders);
__property bool
SearchSubfolders[int index] =
{read = GetSearchSubfolders,
write
= SetSearchSubfolders};
private:
bool __fastcall
GetSearchSubfolders(int index);
void __fastcall
SetSearchSubfolders(int index, bool search);
__property
System::TObject* Objects[int Index]= {read=GetObject,
write=PutObject};
virtual int __fastcall
AddObject(const
AnsiString S,System::TObject* AObject);
};
inline int __fastcall TFFPathList::Add(const String path)
{
return(TStringList::Add(path));
}
inline int __fastcall TFFPathList::Add(const String path, bool
searchSubfolders)
{
return(AddObject(path, (TObject *)searchSubfolders));
}
inline bool __fastcall TFFPathList::GetSearchSubfolders(int
index)
{
return((bool)Objects[index]);
}
inline void __fastcall TFFPathList::SetSearchSubfolders(int index,
bool search)
{
Objects[index] = (TObject
*)search;
}
inline int __fastcall TFFPathList::AddObject(
const AnsiString S,
System::TObject* AObject)
{
return(TStringList::AddObject(S,
AObject));
}
#endif // FFPathListH