. 폴더 검색

파일검색 기능을 구현하기 위해서는 다음 두 가지 검색을 해야 한다. 이 두 검색 과정이 제대로 중첩되어 있어야 복수 개의 파일에서 문자열 검색이 가능하다. 예를 들어 C:\Project 폴더에서 *.cpp *.h파일을 뒤져 Print라는 문자열을 모두 찾는다고 해보자.

첫 번째로 조건에 맞는 파일을 찾아야 한다. 시작 폴더인 C:\Project에 포함되어 있는 모든 파일은 물론이고 이 폴더 아래에 있는 서브폴더까지 뒤져서 확장자가 cpp이거나 h인 모든 파일을 다 찾아내야 한다. 복수 개의 폴더에 대해 복수 개의 조건에 맞는 복수 개의 파일을 임의의 깊이까지 탐색해야 하는데다 디스크는 메모리 외부의 기억 장치이기 때문에 결코 쉬운 문제가 아니다.

두 번째로 조건에 맞는 파일을 찾았으면 이 파일의 전체 내용에 대해 문자열을 검색해야 한다. 파일의 전체 내용을 버퍼로 읽어들인 후 Print라는 문자열이 있는 위치를 모두 다 찾아낸다. 그것도 단순히 문자열의 존재만 검사하는 것이 아니라 대소문자 구성이 맞는지, 완전한 단어인지도 일일이 점검해보아야 한다.

이 두 종류의 검색(폴더 검색, 문자열 검색)을 한 함수에서 다 처리하고자 한다면 물론 그렇게 할 수도 있다. 하지만 워낙 여러 개의 조건들이 적용되는데다가 폴더 구조가 재귀적이기 때문에 한 함수에서 모든 처리를 다 하기가 어렵다. 이 두 검색 중 폴더 검색 절차는 찾기와 바꾸기 과정에서 동일하기 때문에 분리된 함수로 모듈화를 해놓으면 코드의 중복을 막을 수 있으며 다른 프로그램에도 재활용하기 편리할 것이다.

여기서는 일단 문자열 검색은 잠시 접어두고 폴더 검색을 위한 함수를 먼저 만들어 보도록 하자. 폴더 검색 기능은 다른 프로젝트에서도 자주 요구되는 기능이므로 재사용하기 쉽도록 최대한의 유연성을 부여하여 작성해보도록 하자. 여기서 만들 폴더 검색 함수의 설계 원칙은 다음과 같다.

 

서브폴더까지도 검색할 수 있어야 한다. 임의의 깊이에 상관없이 모든 서브폴더를 다 뒤질 수 있도록 재귀 호출 방식을 사용할 것이다. 물론 옵션 선택에 의해 시작 폴더만 검색할 수도 있어야 한다.

임의의 복수 패턴을 검사할 수 있도록 한다. *.cpp;*.h같이 확장자를 조건으로 줄 수도 있고 ?a*.htm? 같은 복잡한 와일드 카드식을 쓸 수도 있다. 여러 개의 패턴을 동시에 지정할 때는 세미콜론으로 패턴을 연결한다.

옵션에 의해 숨은 파일도 검색에 포함시킬 수 있다.

서브폴더에 대해서도 패턴을 적용할 수 있다. 검색 패턴이 a*일 경우 a로 시작되는 파일을 모두 찾기도 하지만 서브폴더도 a로 시작되는 서브폴더만 검색하도록 한다.

조건에 맞는 파일을 발견했을 때 미리 등록된 콜백함수를 호출한다. 콜백함수는 파일에 대한 고유의 처리를 하게 되는데 이렇게 함으로써 폴더 검색 기능과 파일 처리 기능을 원칙적으로 분리시킨다.

콜백함수로 32비트의 사용자정의 데이터를 전달할 수 있다. 사용자정의 데이터는 최초 검색을 의뢰하는 부분에서 검색결과를 다양하게 활용하고자 할 때 사용된다.

하드디스크의 서브폴더를 뒤지는 작업은 굉장한 시간을 요구한다. 빨라도 수초, 늦으면 수분이 걸리고 극단적인 경우는 10분이 넘어갈 수 있다. 일반적으로 이런 긴 작업을 할 때는 취소를 위한 장치가 마련되어 있어야 한다.

 

요구 조건이 결코 간단한 수준이 아닌데 이 함수를 구현하는데 있어 가장 어려운 점은 복수 개의 파일에 대해 복수 개의 패턴을 검사해야 한다는 점이다. 어떤 조건을 먼저 검사하는가에 따라 다음 두 가지 정책 중 하나를 선택할 수 있다. 예를 들어 *.cpp;*.h 패턴에 맞는 복수 파일을 찾는다고 해보자.

첫 번째는 각 패턴에 대해 파일을 검사하는 방식이다. 패턴 하나에 대해 이 패턴과 일치하는 파일이 있는지 검색한다. 즉 확장자가 cpp인 파일을 먼저 찾아 보고 다음으로 확장자가 h인 파일을 찾아 보는 것이다. 두 번째는 각 파일에 대해 패턴을 검사하는 방식이다. 파일을 먼저 찾아 놓고 이 파일이 패턴과 일치하는지를 검색한다. 검색된 파일의 확장자가 cpp인지 보고 아니면 h인지를 본다.

논리상으로는 첫 번째 방법이 훨씬 더 간단하다. 왜냐하면 Win32 FindFirstFile 함수가 하나의 패턴에 대해서는 직접 비교를 해서 이 패턴에 맞는 파일만 찾아 주기 때문이다. 첫 번째 방법을 사용한다면 복수 개의 패턴을 직접 비교할 필요없이 패턴을 하나씩 분리해서 FindFirstFile 함수로 차례대로 넘겨주기만 하면 된다.

하지만 속도는 두 번째 방법이 훨씬 더 유리하다. 패턴 비교보다는 파일검색이 훨씬 더 느리기 때문이다. 임의의 파일 이름이 복수 개의 패턴과 일치하는지를 검사하는 것은 무척 복잡한 연산을 필요로 하지만 하드디스크를 돌리는 것과는 비교가 되지 않는다. 그래서 여기서는 파일을 먼저 찾고 패턴을 검사하는 두 번째 방법을 사용할 것이다.

그럼 폴더 검색 함수를 작성해보자. 먼저 함수의 원형과 콜백 함수 포인터 타입, 검색 옵션 플래그들을 Util.h에 먼저 선언한다.

 

#define FIF_DEEP 1

#define FIF_DIRFILTER 2

#define FIF_INCHID 4

extern BOOL bContFIF;

typedef int (*FIFCALLBACK)(TCHAR *, DWORD, LPVOID);

void FindInFiles(TCHAR *Path, TCHAR *Pattern, DWORD Flags, FIFCALLBACK pCallBack, LPVOID pCustom);

BOOL IsMatch(TCHAR *Path, TCHAR *Pattern);

 

FindInFiles 함수가 폴더 검색 함수이며 IsMatch 함수는 파일의 패턴을 검사하는 보조 함수이다. 이 함수의 인수는 다음과 같다.

 

인수

설명

Path

검색을 시작할 폴더 경로이다. 폴더와 서브폴더의 모든 파일을 검색한다.

Pattern

검색 대상 파일에 적용할 패턴 문자열이다. 세미콜론으로 여러 개의 패턴을 동시에 지정할 있다.

Flags

폴더 검색 방식을 지정하는 옵션값이며 다음 가지 옵션들의 조합을 지정할 있다.

 

플래그

설명

FIF_DEEP

서브폴더까지 검색한다.

FIF_DIRFILTER

서브폴더도 패턴과 비교하여 패턴과 일치하는 서브폴더만 검색한다.

FIF_INCHID

숨김 속성을 가지는 파일도 검색한다.

 

pCallBack

파일 발견시 호출될 콜백함수의 포인터이다. 콜백함수는 반드시 FIFCALLBACK 함수 포인터와 같은 원형을 가져야 한다. 콜백함수의 번째 인수로 발견된 파일의 경로가 전달되며 번째 인수로 파일의 속성이 전달된다.

pCustom

콜백함수의 번째 인수로 전달될 사용자 정의 데이터이다.

 

두 함수의 코드를 Util.cpp에 다음과 같이 작성한다. 복잡한 요구 사항에 비해서 함수의 길이는 비교적 짧은 편이다. 먼저 이 함수의 코드를 스스로 분석해보기 바란다.

 

BOOL IsMatch(TCHAR *Path, TCHAR *Pattern)

{

     TCHAR Pat[MAX_PATH];

     TCHAR *t,*p;

     TCHAR Ext[_MAX_EXT];

     TCHAR Name[MAX_PATH];

     TCHAR *p1,*p2;

     BOOL bOther;

 

     _splitpath(Path,NULL,NULL,Name,Ext);

     lstrcat(Name,Ext);

     CharUpper(Name);

 

     t=Pattern;

     for (;;) {

          p=Pat;

          while (*t!=‘;’ && *t!=0) {

              *p=*t;

              p++;

              t++;

          }

          *p=0;

 

          CharUpper(Pat);

          p1=Name;

          p2=Pat;

          for (;;) {

              if (*p2==‘?’) {

                   p1++;

                   p2++;

              } else if (*p2==‘*’) {

                   p2++;

                   while (*p1!=*p2 && *p1!=0)

                        p1++;

              } else {

                   if (*p1!=*p2) {

                        break;

                   }

                   p1++;

                   p2++;

              }

 

              if (*p1==0 && *p2==0) {

                   return TRUE;

              }

              if (*p1!=0 && *p2==0) {

                   break;

              }

              if (*p1==0 && *p2!=0) {

                   bOther=FALSE;

                   while (*p2) {

                        if (*p2!=‘.’ && *p2!=‘*’) {

                            bOther=TRUE;

                        }

                        p2++;

                   }

                   if (bOther==FALSE) {

                        return TRUE;

                   }

                   break;

              }

          }

 

          if (*t==0) {

              break;

          }

          t++;

     }

     return FALSE;

}

 

BOOL bContFIF=TRUE;

void FindInFiles(TCHAR *Path, TCHAR *Pattern, DWORD Flags, FIFCALLBACK pCallBack, LPVOID pCustom)

{

     TCHAR SrchPath[MAX_PATH];

     TCHAR szFinded[MAX_PATH];

     WIN32_FIND_DATA wfd;

     HANDLE hSrch;

     BOOL nResult=TRUE;

 

     lstrcpy(SrchPath, Path);

     if (SrchPath[lstrlen(SrchPath)-1] == ‘\\’) {

          SrchPath[lstrlen(SrchPath)-1]=0;

     }

     lstrcat(SrchPath, "\\*.*");

     hSrch=FindFirstFile(SrchPath,&wfd);

     if (hSrch == INVALID_HANDLE_VALUE) {

          return;

     }

 

     while (nResult) {

          wsprintf(szFinded,"%s\\%s",Path,wfd.cFileName);

          if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {

              if (

                   ((wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)==0 || (Flags & FIF_INCHID))

                   && ((Flags & FIF_DIRFILTER)==0 || IsMatch(szFinded, Pattern))

                   ) {

                   pCallBack(szFinded,wfd.dwFileAttributes,pCustom);

              }

              if ((Flags & FIF_DEEP) && wfd.cFileName[0]!=‘.’) {

                   FindInFiles(szFinded,Pattern,Flags,pCallBack,pCustom);

              }

          } else {

              if (((wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)==0 || (Flags & FIF_INCHID))

                   && IsMatch(szFinded, Pattern)) {

                   pCallBack(szFinded,wfd.dwFileAttributes,pCustom);

              }

          }

          if (bContFIF==FALSE)

              break;

          nResult=FindNextFile(hSrch,&wfd);

     }

     FindClose(hSrch);

}

 

먼저 FindInFiles 함수를 분석해보자. 이 함수는 인수로 전달된 시작 폴더에 \*.*를 덧붙여 시작 폴더에 포함된 모든 파일을 검색한다. 앞에서 이미 얘기 했다시피 패턴을 하나씩 분리해서 파일을 찾는 것이 아니라 파일을 찾아놓고 패턴을 비교하기 때문에 일단 모든 파일에 대해 검색해야 한다. 이렇게 해야 복잡한 패턴에 대해서도 비교를 할 수 있으며 서브폴더까지 검색할 경우는 폴더 목록을 일단 얻어야 하기 때문에 검색식 자체는 *.*여야 한다. *.cpp파일을 찾는다고 해서 FindFirstFile의 첫 번째 인수를 *.cpp로 주면 확장자가 cpp가 아닌 서브폴더는 검색대상에서 제외되어 버릴 것이다.

while 루프는 현재 폴더의 모든 파일이 검색될 때까지 반복된다. 찾은 파일이 폴더인 경우 FIF_DEEP 플래그가 설정되어 있는지 보고 재귀 호출하도록 하되 단 자기 자신과 부모 폴더일 때는 무시한다. 즉 서브폴더는 이 폴더를 시작 폴더로 하여 FindInFiles 함수를 다시 호출한다. 폴더도 검색 대상이므로 콜백 함수로 전달된다. 이때 FIF_DIRFILTER 플래그가 설정되어 있으면 폴더에 대해서도 패턴을 검사하여 조건에 맞는 폴더만 콜백 함수로 전달된다. 숨겨진 폴더 전달 여부는 FIF_INCHID 플래그가 결정한다.

폴더가 아니면 일단 숨겨진 파일인지 먼저 검사한다. 숨겨진 파일이 아니거나 숨겨진 파일이더라도 FIF_INHID 플래그가 설정되어 있으면 IsMatch 함수를 호출하여 패턴이 일치하는지 비교해 본다. 이 함수가 TRUE를 리턴하면 파일을 제대로 찾은 것이므로 콜백 함수를 호출하여 검색된 파일을 처리하도록 한다.

bContFIF라는 전역변수가 하나 선언되어 있는데 이 변수는 외부에서 검색을 즉시 중지하고자 할 때 사용된다. FindInFiles 함수는 하나의 파일을 검색할 때마다 이 변수값이 FALSE로 바뀌었는지 항상 감시하며 FALSE가 되는 즉시 검색을 중지하고 리턴하도록 되어 있다. 이 변수의 초기값은 TRUE로 되어 있으며 외부에서 이 변수값을 조작하지 않으면 FindInFiles는 예정대로 검색을 마칠 것이고 중간에 FALSE가 되면 검색은 즉시 종료된다.

만약 FindInFiles 함수가 아주 깊은 폴더를 탐색중일지라도 bContFIF 변수가 FALSE가 되는 즉시 스택에 쌓인 호출 고리를 따라 연속적으로 리턴되어 결국은 종료된다. 이 변수는 FindInFiles가 참조하지만 외부에서 이 함수를 조정하기 위한 용도로 사용되므로 Util.h에 미리 extern 선언이 되어 있다. 따라서 FindInFiles 함수의 원형을 알고 있는 모듈은 누구든지 이 변수를 액세스할 수 있도록 되어 있다.

다음은 파일의 패턴을 비교하는 IsMatch 함수를 분석해보자. 인수로 파일의 경로와 적용할 패턴 문자열을 전달받는다. 파일의 경로는 드라이브, 폴더명을 포함한 완전 경로이므로 파일명과 확장명만 분리해 낸다. 윈도우즈의 파일 시스템은 대소문자를 보존하기는 하지만 구분하지는 않으므로 파일명과 패턴을 모두 대문자화하여 비교하는 것이 좋다.

이 함수는 전체적으로 두 개의 for 루프로 구성되어 있다. 바깥쪽 for 루프는 패턴 문자열에서 패턴을 하나씩 분리해 낸다. 예를 들어 인수로 전달된 Pattern *.cpp;*.h;*.txt 일 때 *.cpp 패턴과 *.h 패턴 그리고 *.txt 패턴을 분리하여 각 패턴에 대해 일치 여부를 점검한다. 분리된 패턴은 Pat 버퍼에 저장된다.

안쪽 루프는 조사된 Pat 패턴에 대해 이 파일이 일치하는지 검사한다. 패턴 비교를 위해 두 개의 포인터를 사용하는데 p1은 파일의 이름을 가리키는 포인터이며 p2는 패턴 문자열을 가리키는 포인터로 초기화된다. 이 루프는 p2가 가리키는 문자열을 하나씩 순회하면서 다음과 같이 패턴을 검사한다.

 

 

패턴

검사

?

임의의 문자와 대응되므로 p1, p2 하나씩 증가시킨다. ? 대응되는 p1 문자가 무엇인가에 상관없이 무조건 다음 문자로 이동한다. 패턴이 ?bcd이면 파일명이 abcd이든 xbcd이든 문자의 종류에 상관하지 않는다는 뜻이다.

*

임의의 복수 문자와 대응되므로 p1 p2+1 문자와 일치하거나 또는 p1 끝이 때까지 p1 계속 증가시킨다. 패턴이 *d이면 abcd ad 또는 d같이 다음 d 만날 때까지 파일명에 어떤 문자가 나타나든지 무시한다는 뜻이다.

외의 문자

와일드 카드가 아닌 문자가 나타나면 p1 p2 일대일로 비교한다. p2 a라면 p1 a여야 하고 p2 x라면 p1 x여야 한다. 만약 비교에서 일치하지 않으면 패턴과 일치하지 않는 것이다. 문자가 일치하면 p1 p2 각각 하나씩 증가시킨다.

 

p2 문자를 하나씩 점검할 때마다 패턴과 파일명의 일치 여부를 점검한다. p1, p2의 문자가 둘 다 0이면 즉 파일명과 패턴이 모두 끝났으면 무사히 검사를 통과한 것이며 파일명이 패턴과 일치한 것이다. 더 검사해 볼 필요없이 TRUE를 리턴하면 된다.

p1은 끝이 아닌데 p2만 끝이면 이 패턴과 일치하지 않은 것이다. 예를 들어 패턴(p2) *abc인데 파일명(p1) myabcd라면 패턴보다 더 많은 문자가 파일명에 남아 있으므로 이 파일은 패턴과 일치하지 않은 것이다. 이때는 안쪽 루프를 탈출하여 다음 패턴을 점검하도록 한다.

반대로 p1은 끝인데 p2는 끝이 아닌 경우는 p2의 남은 문자가 .이나 *인지 본다. .이나 *만 남아 있으면 패턴이 일치한 것으로 판단한다. 예를 들어 abc*.* 패턴에서 파일명 abc는 이 패턴에 맞다. * .은 실제로 문자를 의미하지 않으므로 패턴에 남아 있어도 무시할 수 있다. 일치되는 패턴을 찾았으므로 곧바로 TRUE를 리턴하면 된다.

이런 식으로 안쪽 루프는 하나의 패턴에 대해 검사를 수행하며 이 패턴과 일치하지 않을 때는 안쪽 루프를 탈출하여 다음 패턴을 검사한다. 바깥쪽 루프에서 모든 패턴을 다 검사하고도 일치되는 패턴을 찾지 못했다면 이 파일은 패턴과 일치하지 않는 것이다. 이때는 FALSE를 리턴하면 된다. 다음 그림은 실제 패턴과 파일명과의 비교 과정을 그린 것이다.

패턴의 첫 문자가 ?이므로 파일명의 첫 문자가 무엇인가에는 상관하지 않는다. 첫 문자는 D K S나 어떤 문자라도 패턴과는 일치되며 패턴의 다음 문자로 진행된다. ? 다음의 문자는 a인데 이 문자와 파일명의 두 번째 문자는 반드시 일치해야 한다. 패턴의 다음 문자인 *는 임의의 복수 문자와 대응되므로 * 다음의 문자인 .이 파일명에 나타날 때까지 모든 문자를 무시하면 된다. 따라서 * 문자는 ngeun 모두와 대응된다.

.htm은 일대일로 비교되며 모두 일치한다. 마지막 문자인 ?는 파일명의 마지막 문자 l과 대응된다. ?는 임의의 한 문자와 대응되지만 문자가 없는 상태여서는 안되는데 만약 파일명의 마지막에 l이 없다면 ? 패턴이 점검되기 전에 if (*p1==0 && *p2!=0)조건에 걸리게 되고 패턴의 남은 문자가 . *도 아닌 ?이므로 이 비교는 실패하게 될 것이다. 모든 패턴 문자에 대한 비교가 끝나면 if (*p1==0 && *p2==0)조건이 만족되므로 전체 비교 결과는 TRUE가 된다.

IsMatch 함수가 무척 복잡해보이는가? 하지만 이런 문자열처리 함수는 분석하는 것보다 직접 만드는 것이 솔직히 더 쉽다. 몇 개의 패턴과 파일명을 연습장에 그려놓고 나라면 저 두 문자열을 어떻게 비교할 것인가를 생각해보고 그대로 코드로 옮기기만 하면 된다. 사실 컴퓨터라는 놈은 사람이 한 번 해 본 작업을 아무 생각없이 그대로 따라할 줄만 하는 단순한 녀석이되 생각이 없다보니 조금 속도가 빠를 뿐이다.