Рекурсивный поиск файлов с использованием WinAPI
4 декабря 2013
Продолжаем дружно вспоминать WinAPI. В отличие от всех предыдущих заметок сегодня мы напишем первую программу, которая делает что-то действительно полезное. А именно, позволяет найти файл на жестком диске по части его имени.
В этом нам помогут следующие процедуры и структуры данных.
Процедура GetLogicalDrives возвращает битовую маску, по которой можно судить, какие диски есть в системе. Например, если 0-ой бит установлен в единицу, значит диск «A:» есть, иначе его нет. Аналогично, 1-ый бит соответствует диску «B:», 2-ой — диску «C:», и так далее до 25-го бита.
В качестве аргументов принимает строку типа «C:\windows\*.exe» и указатель на структуру WIN32_FIND_DATA. В случае успеха процедура возвращает хэндл, соответствующий поиску, а в fdFindData записывается информация о первом найденном файле — его имя, размер, атрибуты, время создания и так далее. В случае ошибки возвращается INVALID_HANDLE_VALUE.
Принимает хэндл, полученный от FindFirstFile, и указатель на WIN32_FIND_DATA. Если FindNextFile вернул TRUE, значит в fdFindData записана информация о следующем файле. Если FALSE, значит все файлы, соответствующие маске, были перечислены.
Когда поиск завершен, FindClose закрывает хэндл, полученный от FindFirstFile.
Теперь с помощью перечисленных процедур, а также процедур, с которыми мы с вами познакомились в заметке Пишем простое консольное приложение на чистом WinAPI, не представляет труда написать программу, которая рекурсивно ищет файлы на всех дисках в системе, выводя те имена файлов, в которых содержится заданная в качестве аргумента подстрока:
#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».
Если теперь скомпилировать и запустить программу, вы увидите примерно следующее:
В приведенном выше коде есть несколько тонких моментов. Во-первых, как вы могли заметить, в нем активно используется макрос CONST. Он определен в файле windef.h как:
Как большие специалисты по Си (вы же прочитали Кернигана и Ритчи, как я совевтовал?), вы, конечно же, прекрасно знаете, для чего нужно ключевое слово const. Но чисто на всякий случай освежим эти знания. Ключевое слово const говорит о том, что переменная является неизменяемой. Например, если написать:
… а затем попытаться присвоить переменной hStdOut новое значение, компилятор откажется компилировать программу. Когда в дело вступают указатели, все становится чуточку интереснее. Проще всего показать это на примерах:
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.
Наконец, в-третьих, наверняка вам не дает покоя следующая проверка:
(0 == lstrcmp(fdFindData.cFileName, L".."))) {
continue;
}
Так сделано, потому что наряду с обычными файлами и каталогами, FindFirstFile и FindNextFile возвращают информацию о специальных каталогах «.» и «..», которые являются ссылками на текущий и родительский каталоги соответственно. Такая проверка гарантирует, что наш рекурсивный алгоритм не скушает весь стек, обращаясь к каталогам типа «C:\.\.\…и так много раз…». Следует обратить внимание, что в Windows имена файлов могут начинаться с точки (cmd.exe → echo 123 > .test
), поэтому имя нужно проверять целиком, а не только его первый символ.
Велика вероятность, что во всем остальном вы сможете разобраться своими силами. В качестве домашнего задания можете попробовать добавить в программу поддержку аргументов, указывающих, на каких именно дисках нужно искать файлы, или с какого каталога следует начать. Также можете попробовать добавить аргумент, задающий строку, которая должна содержаться внутри файла. Тут вам пригодятся процедуры, с которыми мы познакомились в заметке Учимся работать с файлами через Windows API.
Дополнение: Как выяснилось, у приведенного кода есть ряд недостатков. Во-первых, при запуске под Wine длина имени файла может превосходить MAX_PATH. Во-вторых, код не учитывает существование симлинков и junction points. В продакшн коде все это должно учитываться.
Дополнение: Получаем список запущенных процессов на Windows API
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.