Рекурсивный поиск файлов с использованием WinAPI

4 декабря 2013

Продолжаем дружно вспоминать WinAPI. В отличие от всех предыдущих заметок сегодня мы напишем первую программу, которая делает что-то действительно полезное. А именно, позволяет найти файл на жестком диске по части его имени.

В этом нам помогут следующие процедуры и структуры данных.

GetLogicalDrives();

Процедура GetLogicalDrives возвращает битовую маску, по которой можно судить, какие диски есть в системе. Например, если 0-ой бит установлен в единицу, значит диск «A:» есть, иначе его нет. Аналогично, 1-ый бит соответствует диску «B:», 2-ой — диску «C:», и так далее до 25-го бита.

FindFirstFile(szPath, &fdFindData);

В качестве аргументов принимает строку типа «C:\windows\*.exe» и указатель на структуру WIN32_FIND_DATA. В случае успеха процедура возвращает хэндл, соответствующий поиску, а в fdFindData записывается информация о первом найденном файле — его имя, размер, атрибуты, время создания и так далее. В случае ошибки возвращается INVALID_HANDLE_VALUE.

FindNextFile(hFind, &fdFindData)

Принимает хэндл, полученный от FindFirstFile, и указатель на WIN32_FIND_DATA. Если FindNextFile вернул TRUE, значит в fdFindData записана информация о следующем файле. Если FALSE, значит все файлы, соответствующие маске, были перечислены.

FindClose(hFind);

Когда поиск завершен, FindClose закрывает хэндл, полученный от FindFirstFile.

Теперь с помощью перечисленных процедур, а также процедур, с которыми мы с вами познакомились в заметке Пишем простое консольное приложение на чистом WinAPI, не представляет труда написать программу, которая рекурсивно ищет файлы на всех дисках в системе, выводя те имена файлов, в которых содержится заданная в качестве аргумента подстрока:

#include <windows.h>
#include <shlwapi.h>

#define STRLEN(x) (sizeof(x)/sizeof(x[0]) - 1)

VOID ProcessFoundFile(HANDLE CONST hStdOut, LPWSTR CONST szPath,
                      WIN32_FIND_DATA CONST * CONST fdFindData,
                      LPWSTR CONST lpSearch) {
  TCHAR szEnd[] = L"\r\n";
  DWORD dwTemp;
  if(NULL != StrStrI(fdFindData->cFileName, lpSearch)) {
    WriteConsole(hStdOut, szPath, lstrlen(szPath), &dwTemp, NULL);
    WriteConsole(hStdOut, szEnd, STRLEN(szEnd), &dwTemp, NULL);
  }
}

VOID FindFirstFileFailed(HANDLE CONST hStdOut, LPWSTR CONST szPath) {
  TCHAR CONST szMsgTmpl[] = L"FindFirstFile() failed, "
                            L"GetLastError() = %d, szPath = %s\r\n";
  TCHAR szMsg[MAX_PATH*2];
  DWORD dwTemp;
  wsprintf(szMsg, szMsgTmpl, GetLastError(), szPath);
  WriteConsole(hStdOut, szMsg, lstrlen(szMsg), &dwTemp, NULL);
}

VOID RecursiveSearch(HANDLE CONST hStdOut, LPWSTR szPath,
                     LPWSTR CONST lpSearch) {
  WIN32_FIND_DATA fdFindData;
  HANDLE hFind;
  TCHAR * CONST lpLastChar = szPath + lstrlen(szPath);

  lstrcat(szPath, L"*");
  hFind = FindFirstFile(szPath, &fdFindData);
  *lpLastChar = '\0';

  if(INVALID_HANDLE_VALUE == hFind) {
    FindFirstFileFailed(hStdOut, szPath);
    return;
  }

  do {
    if((0 == lstrcmp(fdFindData.cFileName, L".")) ||
       (0 == lstrcmp(fdFindData.cFileName, L".."))) {
      continue;
    }
    lstrcat(szPath, fdFindData.cFileName);
    if(fdFindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
      lstrcat(szPath, L"\\");
      RecursiveSearch(hStdOut, szPath, lpSearch);
    } else {
      ProcessFoundFile(hStdOut, szPath, &fdFindData, lpSearch);
    }
    *lpLastChar = '\0';
  } while(FindNextFile(hFind, &fdFindData));

  FindClose(hFind);
}

VOID SearchOnAllDrives(HANDLE CONST hStdOut, LPWSTR CONST lpSearch) {
  TCHAR szCurrDrive[] = L"A:\\";
  TCHAR szPath[MAX_PATH+1];
  DWORD i, dwDisksMask = GetLogicalDrives();

  for(i = 0; i < 26; i++) {
    if(dwDisksMask & 1) {
      lstrcpy(szPath, szCurrDrive);
      RecursiveSearch(hStdOut, szPath, lpSearch);
    }
    dwDisksMask >>= 1;
    szCurrDrive[0]++;
  }
}

INT main() {
  DWORD dwTemp;
  INT nArgs = 0;
  LPWSTR CONST lpCmd = GetCommandLine();
  LPWSTR CONST * CONST lpArgs = CommandLineToArgvW(lpCmd, &nArgs);
  HANDLE CONST hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
  CONST TCHAR szUsage[] = L"Usage: find.exe <part-of-file-name>\r\n";

  if(nArgs < 2) {
    WriteConsole(hStdOut, szUsage, STRLEN(szUsage), &dwTemp, NULL);
  } else {
    SearchOnAllDrives(hStdOut, lpArgs[1]);
  }
  LocalFree((PVOID)lpArgs);
  ExitProcess(0);
}

Чтобы эта программа успешно заработала, нужно кое-что поменять в свойствах нашего шаблонного проекта. Во-первых, тип приложения нужно изменить с GUI на CLI. Во-вторых, в свойствах проекта нужно найти Linker → Input → Additional Dependencies, и прописать там «shlwapi.lib» без кавычек. Зачем это нужно, будет рассказано чуть ниже. Также в свойствах проекта нужно указать аргументы, передаваемые программе при ее запуске из IDE. Для этого идем в Debugging → Command Arguments и пишем там, например, «.txt».

Если теперь скомпилировать и запустить программу, вы увидите примерно следующее:

Поиск файлов на Windows API

В приведенном выше коде есть несколько тонких моментов. Во-первых, как вы могли заметить, в нем активно используется макрос CONST. Он определен в файле windef.h как:

#define CONST const

Как большие специалисты по Си (вы же прочитали Кернигана и Ритчи, как я совевтовал?), вы, конечно же, прекрасно знаете, для чего нужно ключевое слово const. Но чисто на всякий случай освежим эти знания. Ключевое слово const говорит о том, что переменная является неизменяемой. Например, если написать:

HANDLE CONST hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

… а затем попытаться присвоить переменной hStdOut новое значение, компилятор откажется компилировать программу. Когда в дело вступают указатели, все становится чуточку интереснее. Проще всего показать это на примерах:

// неизменяемый указатель на массив неизменяемых LPWSTR
LPWSTR CONST * CONST lpArgs = CommandLineToArgvW(lpCmd, &nArgs);

// неизменяемый указатель на массив изменяемых TCHAR'ов
TCHAR * CONST lpLastChar = szPath + lstrlen(szPath);

// изменяемый указатель на массив неизменяемых TCHAR'ов
// TCHAR CONST * lpLastChar = ...

Вместо «HANDLE CONST» также можно писать «CONST HANDLE». Учитывая, что в Си, как известно, можно с легкостью прострелить себе ногу, по возможности следует использовать const как можно чаще. Даже несмотря на то, что из-за него становится несколько сложнее читать код.

Во-вторых, вы могли обратить внимание на использование процедуры StrStrI. Как нетрудно догадаться, она предназначена для сравнения строк без учета регистра. Данная процедура экспортируется динамической библиотекой shlwapi.dll, поэтому нам пришлось написать в коде #include <shlwapi.h> и в свойствах проекта указать, чтобы линковщик использовал библиотеку shlwapi.lib.

Наконец, в-третьих, наверняка вам не дает покоя следующая проверка:

if((0 == lstrcmp(fdFindData.cFileName, L".")) ||
   (0 == lstrcmp(fdFindData.cFileName, L".."))) {
  continue;
}

Так сделано, потому что наряду с обычными файлами и каталогами, FindFirstFile и FindNextFile возвращают информацию о специальных каталогах «.» и «..», которые являются ссылками на текущий и родительский каталоги соответственно. Такая проверка гарантирует, что наш рекурсивный алгоритм не скушает весь стек, обращаясь к каталогам типа «C:\.\.\…и так много раз…». Следует обратить внимание, что в Windows имена файлов могут начинаться с точки (cmd.exe → echo 123 > .test), поэтому имя нужно проверять целиком, а не только его первый символ.

Велика вероятность, что во всем остальном вы сможете разобраться своими силами. В качестве домашнего задания можете попробовать добавить в программу поддержку аргументов, указывающих, на каких именно дисках нужно искать файлы, или с какого каталога следует начать. Также можете попробовать добавить аргумент, задающий строку, которая должна содержаться внутри файла. Тут вам пригодятся процедуры, с которыми мы познакомились в заметке Учимся работать с файлами через Windows API.

Как всегда, приведенный код может быть скомпилирован MinGW и запущен под Wine. Если после прочтения заметки у вас остались вопросы, я буду рад на них ответить.

Дополнение: Как выяснилось, у приведенного кода есть ряд недостатков. Во-первых, при запуске под Wine длина имени файла может превосходить MAX_PATH. Во-вторых, код не учитывает существование симлинков и junction points. В продакшн коде все это должно учитываться.

Дополнение: Получаем список запущенных процессов на Windows API

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.