На этом шаге мы рассмотрим программы поиска файлов по дереву каталогов.
Программа из предыдущего шага осуществляет поиск файлов в указанном или текущем каталоге. Если бы программа была написана на языке высокого уровня, например С, ее легко можно было бы видоизменить так, чтобы она осуществляла поиск по дереву каталогов. Собственно, небольшая модификация потребовалась бы только для процедуры FIND, которая должна была бы вызываться рекурсивно. Можно видеть, что эта легкость произрастает из наличия в языках высокого уровня такого элемента, как локальная переменная. А можно осуществить это без использования локальных переменных?
Программа, приведенная на этом шаге, немного похожа на предыдущую программу. Но поиск она осуществляет по дереву каталогов, начиная с заданного каталога. Эта программа - одна из самых сложных, поэтому советуем читателю скрупулезно в ней разобраться. Может быть, вам удастся ее усовершенствовать. Можем дать и направление, в котором возможно такое усовершенствование. Дело в том, что вторым параметром командной строки можно указать маску поиска. Если, например, указать маску *.ЕХЕ, по этой маске будет осуществляться поиск не только файлов, но и каталогов. Этот недостаток и следовало бы устранить в первую очередь.
Поиск по дереву каталогов достаточно просто реализуется с использованием рекурсии, однако для этого необходимы локальные переменные. Смысл использования локальной переменной в рекурсивном алгоритме заключается в том, что часть данных должна сохраняться при возврате из процедуры.
В данной программе мы отказались от процедуры LENSTR и используем функцию API lstrlen. Кроме того, мы изменили вывод так, чтобы на экран выводилось полное имя файла.
.386P ;Плоская модель памяти. .MODEL FLAT, STDCALL ;Константы. STD_OUTPUT_HANDLE equ -11 STD_INPUT_HANDLE equ -10 ;Прототипы внешних процедур. EXTERN wsprintfA: NEAR EXTERN CharToOemA@8:NEAR EXTERN GetStdHandle@4:NEAR EXTERN WriteConsoleA@20:NEAR EXTERN ReadConsoleA@20:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetCommandLineA@0: NEAR EXTERN lstrcatA@8:NEAR EXTERN lstrcpyA@8:NEAR EXTERN lstrlenA@4:NEAR EXTERN FindFirstFileA@8:NEAR EXTERN FindNextFileA@8:NEAR EXTERN FindClose@4:NEAR ;----------------------------------------- ;Структура, используемая для поиска файла ;при помощи функций FindFirstFile и FindNextFile. _FIND STRUC ATR DWORD ? ;Атрибут файла. CRTIME DWORD ? ;Время создания файла. DWORD ? ACTIME DWORD ? ;Время доступа к файлу. DWORD ? WRTIME DWORD ? ;Время модификации файла. DWORD ? SIZEH DWORD ? ;Размер файла. SIZEL DWORD ? DWORD ? ;Резерв. DWORD ? NAM DB 260 DUP(0) ;Длинное имя файла. ANAM DB 14 DUP(0) ;Короткое имя файла. _FIND ENDS ;----------------------------------------- ;Директивы компоновщику для подключения библиотек includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib ;----------------------------------------- ;Сегмент данных. _DATA SEGMENT DWORD PUBLIC USE32 'DATA' BUF DB 0 DB 100 dup(0) LENS DWORD ? ;Количество выведенных символов. HANDL DWORD ? HANDL1 DWORD ? MASKA DB "*.*" DB 50 dup(0) AP DB "\",0 FIN _FIND <0> TEXT DB "Для продолжения нажмите клавишу ENTER",13,10,0 BUFIN DB 10 DUP(0) ; Буфер ввода. NUM DB 0 NUMF DWORD 0 ;Счетчик файлов. NUMD DWORD 0 ;Счетчик каталогов. FORM DB "Число найденных файлов: %lu",0 FORM1 DB "Число найденных каталогов: %lu",0 DIRN DB " <DIR>",0 PAR DD 0 ;Количество параметров. PRIZN DB 0 _DATA ENDS ;Сегмент кода. _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ;Получить HANDLE вывода. PUSH STD_OUTPUT_HANDLE CALL GetStdHandle@4 MOV HANDL,EAX ;Получить HANDL1 ввода. PUSH STD_INPUT_HANDLE CALL GetStdHandle@4 MOV HANDL1,EAX ;Преобразовать строки для вывода. PUSH OFFSET TEXT PUSH OFFSET TEXT CALL CharToOemA@8 PUSH OFFSET FORM PUSH OFFSET FORM CALL CharToOemA@8 PUSH OFFSET FORM1 PUSH OFFSET FORM1 CALL CharToOemA@8 ;Получить количество параметров. CALL NUMPAR MOV PAR,EAX ;Если параметр один, то искать в текущем каталоге. CMP EAX,1 JE NO_PAR ;---------------------------------------- ;Получить параметр c номером EDI. MOV EDI,2 LEA EBX,BUF CALL GETPAR CMP PAR,3 JB NO_PAR ;Получить параметр - маску поиска. MOV EDI,3 LEA EBX,MASKA CALL GETPAR NO_PAR: ;---------------------------------------- PUSH OFFSET BUF CALL FIND ;Вывести количество файлов. PUSH NUMF PUSH OFFSET FORM PUSH OFFSET BUF CALL wsprintfA LEA EAX,BUF MOV EDX,1 CALL WRITE ;---------------------------------------- ;Вывести количество каталогов. PUSH NUMD PUSH OFFSET FORM1 PUSH OFFSET BUF CALL wsprintfA LEA EAX,BUF MOV EDX,1 CALL WRITE _END: PUSH 0 CALL ExitProcess@4 ;**************************** ;Область процедур. ;**************************** ;Вывести строку (в конце перевод строки). ;EAX - на начало строки. ;EDX - с переводом строки или без. WRITE PROC ;Получить длину параметра. PUSH EAX PUSH EAX CALL lstrlenA@4 MOV ESI,EAX POP EBX CMP EDX,1 JNE NO_ENT ;В конце - перевод строки. MOV BYTE PTR [EBX+ESI],13 MOV BYTE PTR [EBX+ESI+1],10 MOV BYTE PTR [EBX+ESI+2],0 ADD EAX,2 NO_ENT: ;Вывод строки. PUSH 0 PUSH OFFSET LENS PUSH EAX PUSH EBX PUSH HANDL CALL WriteConsoleA@20 RET WRITE ENDP ;Процедура определения количества параметров в строке. ;Определить количество параметров (->EAX) NUMPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ;Указатель на строку. XOR ECX,ECX ;Счетчик. MOV EDX,1 ;Признак. L1: CMP BYTE PTR [ESI],0 JE L4 CMP BYTE PTR [ESI],32 JE L3 ADD ECX,EDX ;Номер параметра. MOV EDX,0 JMP L2 L3: OR EDX,1 L2: INC ESI JMP L1 L4: MOV EAX,ECX RET NUMPAR ENDP ;Получить параметр из командной строки. ;ЕВХ - указывает на буфер, куда будет помещен параметр ;В буфер помещается строка с нулем на конце. ;EDI - номер параметра. GETPAR PROC CALL GetCommandLineA@0 MOV ESI,EAX ;Указатель на строку. XOR ECX,ECX ;Счетчик. MOV EDX,1 ;Признак. L1: CMP BYTE PTR [ESI], 0 JE L4 CMP BYTE PTR [ESI], 32 JE L3 ADD ECX,EDX ;Номер параметра. MOV EDX,0 JMP L2 L3: OR EDX,1 L2: CMP ECX,EDI JNE L5 MOV AL, BYTE PTR [ESI] MOV BYTE PTR [EBX], AL INC EBX L5: INC ESI JMP L1 L4: MOV BYTE PTR [EBX],0 RET GETPAR ENDP ;Поиск в каталоге файлов и их вывод. ;Локальные переменные. FINDH EQU [EBP-4] ;Дескриптор поиска. DIRS EQU [EBP-304] ;Полное имя файла. DIRSS EQU [EBP-604] ;Для хранения каталога. DIRV EQU [EBP-904] ;Для временного хранения. DIR EQU [EBP+8] ;Параметр - имя каталога. FIND PROC PUSH EBP MOV EBP,ESP SUB ESP,904 ;Инициализация локальных переменных. MOV ECX,300 MOV AL,0 MOV EDI,0 CLR: MOV BYTE PTR DIRS+[EDI],AL MOV BYTE PTR DIRSS+[EDI],AL MOV BYTE PTR DIRV+[EDI],AL INC EDI LOOP CLR ;Определить длину пути. PUSH DIR CALL lstrlenA@4 MOV EBX,EAX MOV EDI,DIR CMP BYTE PTR [EDI],0 JE _OK ;Если в конце нет "\" - добавим. CMP BYTE PTR [EDI+EBX-1],"\" JE _OK PUSH OFFSET AP PUSH DWORD PTR DIR CALL lstrcatA@8 _OK: ;Запомним каталог. PUSH DWORD PTR DIR LEA EAX,DIRSS PUSH EAX CALL lstrcpyA@8 ;Путь с маской. PUSH OFFSET MASKA PUSH DWORD PTR DIR CALL lstrcatA@8 ;Здесь начало поиска. PUSH OFFSET FIN PUSH DWORD PTR DIR CALL FindFirstFileA@8 CMP EAX,-1 JE _ERR ;Сохранить дескриптор поиска. MOV FINDH,EAX LF: ;Исключить "файлы" "." и "..". CMP BYTE PTR FIN.NAM,"." JE _FF ;---------------------------------- LEA EAX,DIRSS PUSH EAX LEA EAX,DIRS PUSH EAX CALL lstrcpyA@8 ;---------------------------------- PUSH OFFSET FIN.NAM LEA EAX,DIRS PUSH EAX CALL lstrcatA@8 ;Не каталог ли? TEST BYTE PTR FIN.ATR,10H JE NO_DIR ;Добавить в строку <DIR> PUSH OFFSET DIRN LEA EAX,DIRS PUSH EAX CALL lstrcatA@8 ;Увеличим счетчики. INC NUMD DEC NUMF ;Установим признак каталога. MOV PRIZN,1 ;Вывести имя каталога. LEA EAX, DIRS PUSH EAX CALL OUTF JMP _NO NO_DIR: ;Вывести имя файла. LEA EAX,DIRS PUSH EAX CALL OUTF ;Признак файла (не каталога). MOV PRIZN,0 _NO: CMP PRIZN,0 JZ _F ;Каталог, готовимся в рекурсивному вызову. LEA EAX,DIRSS PUSH EAX LEA EAX,DIRV PUSH EAX CALL lstrcpyA@8 PUSH OFFSET FIN.NAM LEA EAX,DIRV PUSH EAX CALL lstrcatA@8 ;Оосуществляем вызов. LEA EAX, DIRV PUSH EAX CALL FIND ;Продолжение поиска. _F: INC NUMF _FF: PUSH OFFSET FIN PUSH DWORD PTR FINDH CALL FindNextFileA@8 CMP EAX,0 JNE LF ;Закрыть дескриптор поиска. PUSH DWORD PTR FINDH CALL FindClose@4 _ERR: MOV ESP, EBP POP EBP RET 4 FIND ENDP ;----------------------------------------- ;Страничный вывод имен найденных файлов. STRN EQU [EBP+8] OUTF PROC PUSH EBP MOV EBP,ESP ;Преобразовать строку. PUSH DWORD PTR STRN PUSH DWORD PTR STRN CALL CharToOemA@8 ;Здесь вывод результата. MOV EAX,STRN MOV EDX,1 CALL WRITE INC NUM ;Конец страницы? CMP NUM,22 JNE NO MOV NUM,0 ;Ждать ввод строки. MOV EDX,0 LEA EAX,TEXT CALL WRITE PUSH 0 PUSH OFFSET LENS PUSH 10 PUSH OFFSET BUFIN PUSH HANDL1 CALL ReadConsoleA@20 NO: POP EBP RET 4 OUTF ENDP _TEXT ENDS END START
Выясним, какую роль играют локальные переменные в процедуре FIND. Переменная FINDH - здесь хранится дескриптор поиска в данном каталоге. Рекурсивный вызов процедуры FIND может происходить и тогда, когда поиск в текущем каталоге еще не закончился. Следовательно, после возврата из рекурсии поиск должен быть продолжен. Это можно обеспечить только старым значением дескриптора. Локальная переменная обеспечивает такую возможность, поскольку она разрушается только при переходе на более низкий уровень (к родительскому каталогу).
Аналогичную роль играет переменная DIRSS. В ней хранится текущий каталог. Это важно, т.к. с помощью этой переменной формируется полное имя файла.
Переменные DIRS И DIRV играют вспомогательную роль. В принципе, вместо них можно было бы использовать и глобальные переменные. Хотя, с точки зрения эффективности рекурсивных алгоритмов, чем меньше объем локальных переменных - тем лучше.
Еще один вопрос мы здесь обсудим. Для передачи имени каталога при вызове процедуры используется переменная DIRV. Почему же для этой цели нельзя использовать переменную DIRSS? Причина вот в чем. В процедуру передается не само значение, а указатель (адрес). Следовательно, любые изменения с параметром DIR приведет к аналогичным изменениям с переменной DIRSS на нижнем уровне рекурсии. В чем мы, разумеется, не заинтересованы.
На следующем шаге мы рассмотрим трансляцию программ поиска средствами TASM.