Продолжаем дружно вспоминать 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 = ‘ ‘ ;
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 = ‘ ‘ ;
} 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 .
Как всегда, приведенный код может быть скомпилирован MinGW и запущен под Wine. Если после прочтения заметки у вас остались вопросы, я буду рад на них ответить.
Дополнение: Как выяснилось, у приведенного кода есть ряд недостатков. Во-первых, при запуске под Wine длина имени файла может превосходить MAX_PATH. Во-вторых, код не учитывает существование симлинков и junction points . В продакшн коде все это должно учитываться.
Дополнение: Получаем список запущенных процессов на Windows API