Шаг 46.
Поиск файлов по дереву каталогов

    На этом шаге мы рассмотрим программы поиска файлов по дереву каталогов.

    Программа из предыдущего шага осуществляет поиск файлов в указанном или текущем каталоге. Если бы программа была написана на языке высокого уровня, например С, ее легко можно было бы видоизменить так, чтобы она осуществляла поиск по дереву каталогов. Собственно, небольшая модификация потребовалась бы только для процедуры 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.




Предыдущий шаг Содержание Следующий шаг